Skip to content

Hooks & Lifecycle

Hooks run commands on Claude Code events, letting plugins inject context, validate changes, and react to tool use. Each event in the session lifecycle can trigger zero or more hooks.

Session Lifecycle

Hook Events

SessionStart

Fires once when a new Claude Code session opens. Typical use: inject ambient context (project structure, conventions, current task status).

Timeout: 2-5 seconds typical. Default 60 seconds.

Example: The steering plugin injects .agents/steering/ file structure at start so Claude knows to check for context.md files.

UserPromptSubmit

Fires every time the user submits a message. Typical use: add nudges, reminders, or status updates to the model context for every user input.

Timeout: 2-5 seconds typical.

Example: Echo a reminder that system-prompts should be consulted.

PreToolUse

Fires before a tool is invoked (e.g., Read, Write, Bash). Typical use: validate tool use, inject context about the specific tool, or prevent certain operations.

Timeout: 2-5 seconds typical.

Example: Inject steering context before a Task tool is used.

PostToolUse

Fires after a tool completes. Typical use: react to results, trigger cleanup, or record tool use.

Timeout: 2-5 seconds typical.

Example: Run linting after a Write tool creates a file.

Stop

Fires when Claude Code session ends. Typical use: cleanup, final reporting, or state persistence.

Timeout: 5-10 seconds typical.

Hook Configuration

Hooks live in plugins/<name>/hooks/hooks.json:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/scripts/lint.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Matchers

Scope hooks to specific tools:

json
{
  "matcher": "Write|Edit|Bash"
}

Omit matcher to run on all tool invocations of that event type.

Paths

Use ${CLAUDE_PLUGIN_ROOT} to reference plugin files:

json
{
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}

This expands to the plugin's root directory at runtime.

Timeouts

  • SessionStart, UserPromptSubmit, PreToolUse: 2-5 seconds
  • PostToolUse, Stop: 5-10 seconds

If a hook exceeds its timeout, Claude Code skips it.

SessionStart Context Injection

SessionStart hooks can inject context in two ways: plain text output or JSON additionalContext.

Plain Text Output

bash
#!/bin/bash
# Inject context as plain text
cat << EOF
VERY IMPORTANT: Always check .agents/steering/ for context before answering.

.agents/steering/
├── documentation/
├── patterns/
└── overview/
EOF
exit 0

Plain text stdout is injected as part of the session prompt.

JSON additionalContext

bash
#!/bin/bash
# Inject context via JSON structure
echo '{"additionalContext": "Project structure: .agents/steering/"}'
exit 0

Both methods work. Use whichever fits your hook's output format.

Common Hook Patterns

These patterns show up across plugin hooks often enough to be worth naming.

Conditional Execution

Run only when a directory or file is present:

bash
#!/bin/bash
if [[ -d ".agents/steering" ]]; then
  cat << EOF
Context here...
EOF
fi
exit 0

External Command Dependency

If a hook depends on a tool that may not be installed, fall back gracefully:

bash
#!/bin/bash
if command -v tree &> /dev/null; then
  tree .agents/steering
else
  find .agents/steering -type f -name "*.md"
fi
exit 0

Timeout-Safe Operations

Cap long operations and exit cleanly even if the inner command times out:

bash
#!/bin/bash
timeout 4s find . -name "*.md" | head -20
exit 0

User-Level Hooks

In addition to plugin hooks, users can add hooks to their own Claude Code settings:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'You just created a file'"
          }
        ]
      }
    ]
  }
}

See .claude/settings.json in your project or home directory. User hooks run after plugin hooks.

Best Practices

Hooks should finish in 2–5 seconds because they block the session lifecycle. Use matchers to scope hooks to the tools that actually matter — running on every tool slows sessions unnecessarily. For SessionStart context injection, plain text and JSON work equally well; pick the format that fits your hook's output. Failed hooks don't break Claude Code — their output is lost, so write hooks that exit cleanly even when they encounter errors.

Built with VitePress and powered by Claude Code