Files
Codeman/docs/claude-code-hooks-reference.md
lior 5f775b1ab1 fix(hooks): guard subagent stops and rewake from the parent transcript
Two defects in the background-task hook scripts.

SubagentStop had no handler at all. When a subagent launched background work and
one watcher ended while others were still running, Claude could publish the
worker's last progress sentence as its final result, abandoning the live tasks.
A new guard pairs launched task IDs against completed ones and confirms liveness
by scanning /proc/<pid>/fd for an open tasks/<id>.output handle, blocking the
stop only while genuinely-live work remains. It fails open — allowing the stop —
when /proc is unavailable, nothing was launched, or everything finished.

The rewake helper watched only input.transcript_path. A subagent has its own
transcript, but Claude writes the completion queue-operation to the PARENT
transcript, so the record it waited for never appeared and the wake never fired.
It now watches both paths, but only when the relationship is provable: the
transcript's parent directory is subagents/ and its grandparent basename equals
input.session_id. It also now requires operation === 'enqueue'.

The rewake marker moves V2 -> V3; refreshStaleCodemanHooks treats absence of the
current marker as stale, so existing cases self-heal on next launch (the same
mechanism as the V1 -> V2 bump). Ownership matches on marker PREFIXES, so a
future bump still recognises older Codeman handlers and never adopts a user's.

12 tests fail on unmodified master, e.g.
  expected '[{"matcher":"Bash",…' to contain 'CODEMAN_BACKGROUND_REWAKE_V3'
  expected 'Background command bg-report-1 comple…' to contain '<codeman-background-result>'
2026-08-08 22:31:38 +03:00

677 lines
15 KiB
Markdown

# Claude Code Hooks Reference
> Official documentation for Claude Code hooks system, extracted from [code.claude.com](https://code.claude.com/docs/en/hooks).
**Last Updated**: 2026-07-25
**Source**: [Claude Code Hooks Documentation](https://code.claude.com/docs/en/hooks)
> This is a maintained summary, not an exhaustive copy of the upstream reference.
> Check the source link for event-specific schemas before adding a new hook.
---
## Overview
Hooks are automated scripts that execute at specific events during your Claude Code session. They allow you to:
- Validate, modify, or block tool usage
- Add context to prompts
- Implement custom workflows
- Control agent behavior
---
## Configuration
Hooks are configured in settings files:
| File | Scope |
| ----------------------------- | -------------------------- |
| `~/.claude/settings.json` | User (global) |
| `.claude/settings.json` | Project |
| `.claude/settings.local.json` | Local project (gitignored) |
| Plugin hook files | Plugin-specific |
### Basic Structure
```json
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
]
}
}
```
**Key Fields**:
- `matcher`: Pattern to match tool names (case-sensitive, supports regex like `Edit|Write` or `*` for all)
- `type`: `"command"`, `"http"`, `"mcp_tool"`, `"prompt"`, or `"agent"` where the event supports it
- `command`: Bash command to execute
- `prompt`: LLM prompt for evaluation (prompt-based hooks only)
- `timeout`: Optional timeout in seconds (default: 60)
---
## Hook Events
Claude Code's current event surface is broader than the detailed subset below. In
particular, `TeammateIdle` and `TaskCompleted` are supported lifecycle events used
by Codeman; they are not stale or plugin-defined event names.
### PreToolUse
**When**: After Claude creates tool parameters, before processing the tool call.
**Use Cases**: Approval, denial, or modification of tool calls.
**Common Matchers**:
- `Bash` - Shell commands
- `Write` - File writing
- `Edit` - File editing
- `Read` - File reading
- `Agent` - Subagent tasks
- `WebFetch`, `WebSearch` - Web operations
- `mcp__<server>__<tool>` - MCP tools
**Output Control**:
```json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "string",
"updatedInput": {
"field_to_modify": "new value"
},
"additionalContext": "Context for Claude"
}
}
```
### PermissionRequest
**When**: When the user is shown a permission dialog.
**Use Cases**: Auto-approve or deny permissions.
**Output Control**:
```json
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow|deny",
"updatedInput": {},
"message": "deny reason",
"interrupt": false
}
}
}
```
### PostToolUse
**When**: Immediately after a tool completes successfully.
**Use Cases**: Provide feedback, run formatters/linters, log operations.
**Output Control**:
```json
{
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Additional information"
}
}
```
#### Asynchronous Rewake
Command hooks can set `"asyncRewake": true` to run asynchronously and wake an
idle Claude turn when the hook exits with code 2. The hook's stderr is delivered
to Claude as a system reminder. This implies `"async": true`; ordinary async
hooks do not wake an idle turn, and their output waits for the next interaction.
Codeman uses this on `PostToolUse(Bash)`: a self-contained Node helper extracts
the background task ID from the Bash result, watches the originating transcript
and, for subagents, the top-level parent transcript for the matching completion
notification, and exits 2. Claude records a subagent's Bash result in its
`subagents/agent-*.jsonl` file but queues completion in the lead session JSONL.
The task ID keeps each wake targeted. The helper does not send terminal input,
so it cannot submit a user's partially written prompt.
For script-dispatched Codex work, `codex-run.sh` writes the final response
between `CODEMAN_RESULT_BEGIN/END` markers in the background task output. The
rewake helper includes a maximum of 64 KiB of that report in its feedback. UI
subagent discovery and dispatcher result delivery are separate contracts.
### Notification
**When**: When Claude Code sends notifications.
**Matchers**:
- `permission_prompt`
- `idle_prompt`
- `auth_success`
- `elicitation_dialog`
- `elicitation_complete`
- `elicitation_response`
### UserPromptSubmit
**When**: When the user submits a prompt, before Claude processes it.
**Use Cases**: Add context, validate, or block prompts.
**Output Control**:
```json
{
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "My additional context"
}
}
```
### Stop
**When**: When the main Claude Code agent finishes responding.
**Important**: Does NOT run on user interrupt.
**Use Cases**: **Ralph Wiggum loops** - block exit and refeed prompt.
**Output Control**:
```json
{
"decision": "block",
"reason": "Must provide when blocking"
}
```
Or to allow exit:
```json
{
"continue": true,
"stopReason": "optional message"
}
```
**Note**: For Stop events, `"continue": false` takes precedence over `"decision": "block"`.
### SubagentStop
**When**: When a subagent (Agent tool call) finishes responding.
**Use Cases**: Control nested loops, verify subagent output.
The hook input includes `agent_id`, `agent_transcript_path`, and
`last_assistant_message`. Like `Stop`, a command hook can return
`{"decision":"block","reason":"..."}` to keep the subagent running and feed
the reason back to it.
Codeman uses this to prevent premature reports from workers that still own live
Monitor or background-Bash processes. It derives candidate task IDs from the
subagent transcript, but requires a matching live Linux process descriptor for
`tasks/<id>.output`; historical task text by itself is not treated as active.
### TeammateIdle
**When**: When an agent-team teammate is about to go idle.
**Use Cases**: Reassign work, continue a teammate loop, or notify an orchestrator.
**Matcher Support**: None. The hook fires for every occurrence.
### TaskCompleted
**When**: When a task is about to be marked completed.
**Use Cases**: Validate completion or forward team progress to an external UI.
**Matcher Support**: None. The hook fires for every occurrence.
### PreCompact
**When**: Before a compact operation.
**Matchers**:
- `manual` - Invoked from `/compact`
- `auto` - Invoked from auto-compact
### SessionStart
**When**: When Claude Code starts or resumes a session.
**Matchers**:
- `startup` - Fresh start
- `resume` - From `--resume`, `--continue`, or `/resume`
- `clear` - From `/clear`
- `compact` - From auto or manual compact
**Use Cases**: Load development context, set environment variables.
**Persisting Environment Variables**:
```bash
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export API_KEY=your-api-key' >> "$CLAUDE_ENV_FILE"
fi
exit 0
```
**Output Control**:
```json
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Context to load"
}
}
```
### SessionEnd
**When**: When a session ends.
**Reason Values**:
- `clear`
- `logout`
- `prompt_input_exit`
- `other`
**Use Cases**: Cleanup tasks, logging.
---
## Hook Input
Hooks receive JSON via stdin with common fields:
```json
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/directory",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {},
"tool_use_id": "toolu_01ABC123..."
}
```
### Tool-Specific Input
**Bash**:
```json
{
"tool_name": "Bash",
"tool_input": {
"command": "psql -c 'SELECT * FROM users'",
"description": "Query the users table",
"timeout": 120000
}
}
```
**Write**:
```json
{
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
}
}
```
**Edit**:
```json
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/file.txt",
"old_string": "original text",
"new_string": "replacement text"
}
}
```
---
## Hook Output
### Exit Codes
| Code | Behavior |
| ----- | --------------------------------------------------------------------- |
| 0 | Success. `stdout` processed (shown in verbose or added as context) |
| 2 | Blocking error. Only `stderr` used. Blocks tool/prompt based on event |
| Other | Non-blocking error. `stderr` shown in verbose, execution continues |
### JSON Output (Exit Code 0)
```json
{
"continue": true,
"stopReason": "optional message",
"suppressOutput": true,
"systemMessage": "optional warning"
}
```
---
## Prompt-Based Hooks
Prompt and agent handlers are supported by decision-oriented events including
`PreToolUse`, `PermissionRequest`, `PostToolUse`, `PostToolUseFailure`,
`PostToolBatch`, `UserPromptSubmit`, `Stop`, `SubagentStop`, `TaskCreated`, and
`TaskCompleted`. Check the upstream reference before choosing a handler type.
For example, a Stop event can use LLM-based evaluation:
```json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Should Claude stop? Context: $ARGUMENTS\n\nCheck if all tasks are complete.",
"timeout": 30
}
]
}
]
}
}
```
**LLM Response Format**:
```json
{
"ok": true,
"reason": "Explanation when ok is false"
}
```
---
## Component-Scoped Hooks
Hooks can be defined in Skills, Agents, and Slash Commands using frontmatter:
```markdown
---
name: secure-operations
hooks:
PreToolUse:
- matcher: 'Bash'
hooks:
- type: command
command: './scripts/security-check.sh'
---
```
These hooks:
- Are scoped to the component's lifecycle
- Only run when that component is active
- Support all hook events; a subagent-scoped `Stop` is converted to `SubagentStop`
---
## MCP Tools
MCP tools follow the pattern `mcp__<server>__<tool>`:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "echo 'Memory operation' >> ~/mcp.log"
}
]
},
{
"matcher": "mcp__.*__write.*",
"hooks": [
{
"type": "command",
"command": "/home/user/scripts/validate-mcp-write.py"
}
]
}
]
}
}
```
---
## Examples
### Bash Command Validation
```python
#!/usr/bin/env python3
import json
import re
import sys
VALIDATION_RULES = [
(r"\bgrep\b(?!.*\|)", "Use 'rg' instead of 'grep'"),
(r"\bfind\s+\S+\s+-name\b", "Use 'rg --files' instead of 'find -name'"),
]
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
command = tool_input.get("command", "")
if tool_name != "Bash" or not command:
sys.exit(1)
issues = []
for pattern, message in VALIDATION_RULES:
if re.search(pattern, command):
issues.append(message)
if issues:
for message in issues:
print(f"- {message}", file=sys.stderr)
sys.exit(2)
```
### Auto-Approve Documentation Reads
```python
#!/usr/bin/env python3
import json
import sys
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name == "Read":
file_path = tool_input.get("file_path", "")
if file_path.endswith((".md", ".mdx", ".txt", ".json")):
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Documentation file auto-approved"
},
"suppressOutput": True
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)
```
### Post-Write Formatter
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
}
]
}
]
}
}
```
### Ralph Wiggum Stop Hook
```bash
#!/bin/bash
# ralph-stop-hook.sh
STATE_FILE=".claude/ralph-loop.local.md"
# Check if state file exists
if [ ! -f "$STATE_FILE" ]; then
exit 0 # No active loop, allow exit
fi
# Read state from YAML frontmatter
ENABLED=$(grep -m1 "^enabled:" "$STATE_FILE" | cut -d' ' -f2)
ITERATION=$(grep -m1 "^iteration:" "$STATE_FILE" | cut -d' ' -f2)
MAX_ITER=$(grep -m1 "^max-iterations:" "$STATE_FILE" | cut -d' ' -f2)
PROMISE=$(grep -m1 "^completion-promise:" "$STATE_FILE" | cut -d' ' -f2-)
# Check if disabled
if [ "$ENABLED" = "false" ]; then
exit 0
fi
# Check max iterations
if [ -n "$MAX_ITER" ] && [ "$ITERATION" -ge "$MAX_ITER" ]; then
exit 0
fi
# Check for completion promise in output
if [ -n "$PROMISE" ]; then
if echo "$CLAUDE_OUTPUT" | grep -q "<promise>$PROMISE</promise>"; then
exit 0
fi
fi
# Block exit, increment iteration
NEW_ITER=$((ITERATION + 1))
sed -i "s/^iteration:.*/iteration: $NEW_ITER/" "$STATE_FILE"
# Output block decision
echo '{"decision": "block", "reason": "Completion promise not found. Iteration '"$NEW_ITER"'."}'
exit 0
```
---
## Environment Variables
| Variable | Description |
| -------------------- | ------------------------------------------------ |
| `CLAUDE_PROJECT_DIR` | Project root directory |
| `CLAUDE_CODE_REMOTE` | `"true"` for web, empty for CLI |
| `CLAUDE_ENV_FILE` | Path to write persistent env vars (SessionStart) |
---
## Debugging
Use `claude --debug` to see detailed hook execution:
```
[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Write"
[DEBUG] Executing hook command: <command> with timeout 60000ms
[DEBUG] Hook command completed with status 0: <stdout>
```
Use `/hooks` command to view registered hooks and make changes.
---
## Execution Details
- **Timeout**: 60-second default per hook, configurable
- **Parallelization**: All matching hooks run in parallel
- **Deduplication**: Identical commands deduplicated automatically
- **Matchers**: Only apply to tool-based hooks (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest)
---
## Security Best Practices
1. **Validate and sanitize inputs** - Never trust input data blindly
2. **Always quote shell variables** - Use `"$VAR"` not `$VAR`
3. **Block path traversal** - Check for `..` in file paths
4. **Use absolute paths** - Specify full paths for scripts (use `$CLAUDE_PROJECT_DIR`)
5. **Skip sensitive files** - Avoid `.env`, `.git/`, keys, etc.
---
_Source: [Claude Code Hooks Documentation](https://code.claude.com/docs/en/hooks)_