---
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.

```json
{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "AskUserQuestion",
        "hooks": [{ "type": "command", "command": "your-script.sh" }]
      }
    ]
  }
}
```

**Tracked issue:** [#15872](https://github.com/anthropics/claude-code/issues/15872) - Feature request: Add hook support for AskUserQuestion

Source: [#15872 comment](https://github.com/anthropics/claude-code/issues/15872#issuecomment-3701824730)

### 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 mode** — `claude --debug` shows hook execution details

## References

- [Hooks reference - Claude Code Docs](https://docs.anthropic.com/en/docs/claude-code/hooks)
- [Hook control flow explained](https://stevekinney.com/courses/ai-development/claude-code-hook-control-flow)
- [#15872](https://github.com/anthropics/claude-code/issues/15872) - Feature request: hook support for AskUserQuestion
