AI Atlas
EN TR
Advanced · ~2 min read #hooks #claude-code #automation

Agent Hooks

Lifecycle hooks for agents

User-defined commands that the harness runs deterministically at specific points in an agent's lifecycle: before or after a tool call, at session start, when a response finishes.

HOOKS: GUARANTEES, NOT SUGGESTIONSSessionStartUserPromptSubmitPreToolUseTOOLrunsPostToolUseStopHOOKadd contextgit statusHOOKexit 2→ blockHOOKformatprettierHOOKdone yet?run testsexit 0 → proceedexit 2 → blockJSON → fine-grainedevent payload arrives as JSON on stdin; the decision returns via exit codethe model may forget — the harness never does
Definition

You can tell an agent in its system prompt to "run the linter after every file edit." It usually complies; sometimes it forgets. Hooks turn "usually" into "always": you hand the rule to the harness running the agent instead of to the model. The short version: prompts and skills are suggestions, hooks are guarantees.

A hook is a command bound to an event in the harness lifecycle. The most commonly used events in Claude Code: - SessionStart — when a session opens (e.g. inject git status) - UserPromptSubmit — before the user's message reaches the model - PreToolUse — right before a tool runs; can block the call - PostToolUse — after a tool succeeds (format, lint, test) - Stop — when the model is about to finish; can say "not done yet"

The mechanics are simple: the harness writes the event payload (tool name, input, session info) as JSON to the hook's stdin. A hook can be a shell script, an HTTP endpoint, or a small model evaluation. It answers through its exit code and stdout: exit 0 means "proceed", exit 2 means "block", and whatever it writes to stderr is fed back to the model. For finer control it returns JSON — for example permissionDecision: "deny" on PreToolUse.

The model may not even know the hook exists; it only sees the outcome. That's why hooks are the most reliable way to make agent behaviour deterministic.

Analogy

Like a safety interlock on a factory press. You train the operator to "keep your hands clear when the press comes down" (the prompt), but you also fit a two-handed button so the machine won't fire unless both hands are on it (the hook). Training guides behaviour; the interlock makes the accident physically impossible, however tired the operator is.

Real-world example

A team using Claude Code has two recurring problems: the agent occasionally runs an overly broad rm -rf, and it sometimes forgets to format the files it edits.

The fix is two hooks in .claude/settings.json: 1. PreToolUse with a Bash matcher: if the command contains rm -rf, the script returns permissionDecision: "deny"; Claude sees the reason and finds another way. 2. PostToolUse with an Edit|Write matcher: the formatter runs automatically after every edit.

Neither rule depends on a prompt, on the model's attention in the moment, or on how full the context window is. And because the file is committed, everyone on the team gets the same protection.

Code examples
Claude Code · .claude/settings.json json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write ."
          }
        ]
      }
    ]
  }
}
.claude/hooks/block-rm.sh · PreToolUse decision bash
#!/bin/bash
# The harness writes the tool input as JSON to stdin
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "rm -rf blocked by hook"
    }
  }'
else
  exit 0  # no decision; normal permission flow applies
fi
When to use
  • A rule must apply without exception (formatting, linting, never touching secrets)
  • You want to block dangerous tool calls regardless of what the model decides
  • Injecting dynamic context at session start (git status, open tickets)
  • Running tests the moment the agent claims it's done (Stop)
  • Side effects like audit logging or notifications
When not to use
  • The rule needs judgment and depends on context — that's a job for a prompt or a skill
  • Heavy work taking seconds on every tool call — it slows the whole agent loop
  • As your only security boundary — a hook is one layer, not a replacement for a sandbox
Common pitfalls

Treating silence as approval

A PreToolUse hook that exits 0 with no output doesn't approve anything; the call falls through to the normal permission flow. Blocking requires an explicit exit 2 or permissionDecision: "deny".

Trapping the Stop hook in a loop

A Stop hook that blocks every time keeps the agent from ever stopping. Check the stop_hook_active input field; Claude Code also force-ends the turn after 8 consecutive continuations.

Turning the hook into a vulnerability

A hook is a shell command running with your permissions. Quote variables, reject paths containing .., stay away from .env and key files, and reference scripts by absolute path via $CLAUDE_PROJECT_DIR.