返回 Skill 列表
extension
分类: 开发与工程无需 API Key

cc-writing-hooks

在调试钩子问题、绕过已知错误(PreToolUse+AskUserQuestion)或在settings.json中配置用户钩子时使用。对于插件钩子开发,请使用plugin-dev:hook-development。

person作者: jakexiaohubgithub

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 modeclaude --debug shows hook execution details

References