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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/data/cc-writing-hooks/SKILL.md
source content

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

EventWhenCommon Use
PreToolUse
Before tool runsValidate, block
PostToolUse
After tool succeedsFormat, lint
UserPromptSubmit
User sends promptAdd context
SessionStart
Session beginsLoad context
Stop
Agent finishesCleanup

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

CodeMeaningBehavior
0SuccessContinue, stdout shown in transcript (Ctrl-R)
2BlockStop tool, stderr shown to Claude
OtherErrorContinue, 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 --debug
    shows hook execution details

References