Claude-skill-registry cc-writing-hooks
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cc-writing-hooks" ~/.claude/skills/majiayu000-claude-skill-registry-cc-writing-hooks && rm -rf "$T"
skills/data/cc-writing-hooks/SKILL.mdWriting 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 |
|---|---|---|
| Before tool runs | Validate, block |
| After tool succeeds | Format, lint |
| User sends prompt | Add context |
| Session begins | Load context |
| 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 —
shows hook execution detailsclaude --debug
References
- Hooks reference - Claude Code Docs
- Hook control flow explained
- #15872 - Feature request: hook support for AskUserQuestion