Agent skill

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.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/cc-writing-hooks

SKILL.md

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.

json
{
  "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.

json
// ✅ 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:

bash
# 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

json
{
  "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)

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

bash
#!/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 modeclaude --debug shows hook execution details

References

Didn't find tool you were looking for?

Be as detailed as possible for better results