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

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

{
  "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_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:

{
  "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 /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:

#!/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:

  • clear
  • logout
  • prompt_input_exit
  • other

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 Stop is converted to SubagentStop

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

  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