name: cc-writing-hooks description: Use when debugging hook issues, working around known bugs (PreToolUse+AskUserQuestion), or configuring user hooks in settings.json. For plugin hook development, use plugin-dev:hook-development.
Writing Claude Code Hooks
Scope
This skill covers user hooks in settings.json and known bugs. For different purposes, use:
- Plugin hooks.json format:
plugin-dev:hook-development - This skill: User settings.json hooks, bug workarounds, matcher gotchas
Create and configure hooks in .claude/settings.json.
CRITICAL
PreToolUse Hooks Break AskUserQuestion
Known bug: When PreToolUse hooks are active, AskUserQuestion returns empty responses without showing UI to the user.
Root cause: Stdin/stdout conflict between hook JSON processing and AskUserQuestion's interactive terminal input.
Workaround: Use PermissionRequest hook instead of PreToolUse for AskUserQuestion logic.
Both hooks fire for permission-required tools, but PermissionRequest is semantically correct for user-input scenarios. Match on tool_name within PermissionRequest handler.
{
"hooks": {
"PermissionRequest": [
{
"matcher": "AskUserQuestion",
"hooks": [{ "type": "command", "command": "your-script.sh" }]
}
]
}
}
Tracked issue: #15872 - Feature request: Add hook support for AskUserQuestion
Source: #15872 comment
Matcher Syntax
Matchers match TOOL NAMES only, not file paths.
// ✅ CORRECT - tool name regex
"matcher": "Write|Edit"
// ❌ WRONG - glob patterns don't work
"matcher": "Edit(**/*.md)"
"matcher": "Write(docs/*.ts)"
File path filtering must happen inside your hook script by parsing tool_input.file_path.
Absolute Paths
Tools pass absolute paths in tool_input.file_path. Your script must handle this:
# Strip project dir to get relative path
rel_path="${file_path#$CLAUDE_PROJECT_DIR/}"
# Now match against relative path
if [[ "$rel_path" =~ ^docs/.*\.md$ ]]; then
# ...
fi
Hook Structure
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/my-hook.sh",
"timeout": 10
}
]
}
]
}
}
Hook Events
| Event | When | Common Use |
|---|---|---|
PreToolUse | Before tool runs | Validate, block |
PostToolUse | After tool succeeds | Format, lint |
UserPromptSubmit | User sends prompt | Add context |
SessionStart | Session begins | Load context |
Stop | Agent finishes | Cleanup |
Hook Input (stdin JSON)
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/dir",
"hook_event_name": "PostToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "/absolute/path/to/file.ts",
"old_string": "...",
"new_string": "..."
}
}
Exit Codes
| Code | Meaning | Behavior |
|---|---|---|
| 0 | Success | Continue, stdout shown in transcript (Ctrl-R) |
| 2 | Block | Stop tool, stderr shown to Claude |
| Other | Error | Continue, stderr shown to user |
Script Template
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# Convert absolute to relative
rel_path="${file_path#$CLAUDE_PROJECT_DIR/}"
# Filter by extension/path
if [[ -z "$rel_path" || ! "$rel_path" =~ \.(ts|tsx|md)$ ]]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR"
# Your logic here
exit 0
Notes
- Changes require restart — Hook edits don't take effect until CC restarts
- Parallel execution — Multiple matching hooks run in parallel
- 60s default timeout — Override with
"timeout": <seconds> - Debug mode —
claude --debugshows hook execution details
References
- Hooks reference - Claude Code Docs
- Hook control flow explained
- #15872 - Feature request: hook support for AskUserQuestion