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>'
15 KiB
Claude Code Hooks Reference
Official documentation for Claude Code hooks system, extracted from code.claude.com.
Last Updated: 2026-07-25 Source: Claude Code Hooks Documentation
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
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
]
}
}
Key Fields:
matcher: Pattern to match tool names (case-sensitive, supports regex likeEdit|Writeor*for all)type:"command","http","mcp_tool","prompt", or"agent"where the event supports itcommand: Bash command to executeprompt: 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 commandsWrite- File writingEdit- File editingRead- File readingAgent- Subagent tasksWebFetch,WebSearch- Web operationsmcp__<server>__<tool>- MCP tools
Output Control:
{
"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:
{
"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:
{
"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_promptidle_promptauth_successelicitation_dialogelicitation_completeelicitation_response
UserPromptSubmit
When: When the user submits a prompt, before Claude processes it.
Use Cases: Add context, validate, or block prompts.
Output Control:
{
"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:
{
"decision": "block",
"reason": "Must provide when blocking"
}
Or to allow exit:
{
"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/compactauto- Invoked from auto-compact
SessionStart
When: When Claude Code starts or resumes a session.
Matchers:
startup- Fresh startresume- From--resume,--continue, or/resumeclear- From/clearcompact- From auto or manual compact
Use Cases: Load development context, set environment variables.
Persisting Environment Variables:
#!/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:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Context to load"
}
}
SessionEnd
When: When a session ends.
Reason Values:
clearlogoutprompt_input_exitother
Use Cases: Cleanup tasks, logging.
Hook Input
Hooks receive JSON via stdin with common fields:
{
"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:
{
"tool_name": "Bash",
"tool_input": {
"command": "psql -c 'SELECT * FROM users'",
"description": "Query the users table",
"timeout": 120000
}
}
Write:
{
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
}
}
Edit:
{
"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)
{
"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:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Should Claude stop? Context: $ARGUMENTS\n\nCheck if all tasks are complete.",
"timeout": 30
}
]
}
]
}
}
LLM Response Format:
{
"ok": true,
"reason": "Explanation when ok is false"
}
Component-Scoped Hooks
Hooks can be defined in Skills, Agents, and Slash Commands using frontmatter:
---
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
Stopis converted toSubagentStop
MCP Tools
MCP tools follow the pattern mcp__<server>__<tool>:
{
"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
#!/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
#!/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
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
}
]
}
]
}
}
Ralph Wiggum Stop Hook
#!/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
- Validate and sanitize inputs - Never trust input data blindly
- Always quote shell variables - Use
"$VAR"not$VAR - Block path traversal - Check for
..in file paths - Use absolute paths - Specify full paths for scripts (use
$CLAUDE_PROJECT_DIR) - Skip sensitive files - Avoid
.env,.git/, keys, etc.
Source: Claude Code Hooks Documentation