AI Coding Agents

Claude Code Hooks in 2026: A Production Playbook (PreToolUse, PostToolUse, Stop, SubagentStop)

Francesc14 min read

Claude Code hooks lifecycle timeline showing SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Notification, PreCompact, Stop, and SessionEnd events with a guard rail shield between PreToolUse and PostToolUse

Hooks also gate tool execution inside Anthropic-managed sandboxes; see our Anthropic Managed Agents production playbook for the schedule + sandbox + MCP-tunnel setup.

Claude Code hooks are event-driven shell commands or scripts that run deterministically at fixed points in Claude Code's tool-call lifecycle. They let you block destructive commands, enforce style rules, redact secrets, and run tests without trusting the model to remember instructions. In 2026 the surface has grown to ten plus events including PreToolUse, PostToolUse, Stop, SubagentStop, UserPromptSubmit, Notification, PreCompact, and SessionStart, with new fields like hookSpecificOutput.additionalContext, background_tasks, and session_crons. This guide is the production playbook we use at Totalum to ship hooks that survive real engineering work, paired with a Skills plus Hooks recipe and a Totalum MCP example you can copy.

See our Claude Code subagents production playbook for the full pattern of spawning subagents in parallel and gating their returns with a SubagentStop hook.

If you want to wire your Claude Code agent to a real production stack (auth, database, payments, deploys) the same day you finish reading, you can connect it to Totalum at totalum.app and use the hook patterns below from day one.

Quick Answer

  • Claude Code hooks are JSON-configured shell commands that fire on lifecycle events: PreToolUse, PostToolUse, Stop, SubagentStop, UserPromptSubmit, Notification, PreCompact, SessionStart, SessionEnd, and a few more.
  • PreToolUse can block actions before the tool runs by exiting with code 2. PostToolUse can format, lint, or test after the tool succeeds, but cannot undo the call.
  • Stop and SubagentStop now return hookSpecificOutput.additionalContext to extend a turn with new context, and receive background_tasks and session_crons in their input.
  • Use hooks for deterministic guard rails (security, style, tests). Use Skills for procedural knowledge the model loads when relevant. Use Subagents for parallel sub-conversations with their own context windows.
  • Combine a Skill plus a Hook to make the model both know what to do and be forced to do it. The pattern below shows how.

Hooks and Auto Mode now sit inside a much larger Claude Code platform shape, surfaced at Code with Claude Tokyo on June 10. See the Code with Claude Tokyo 2026 recap for the 11 Day 1 launches.

What Claude Code hooks actually are

A hook is a shell command Claude Code runs synchronously when a specific lifecycle event fires. The runtime sends JSON on stdin, reads stdout and stderr, and obeys the exit code. The model never decides whether to invoke the hook, and the model cannot bypass it. That is the whole point. Anything the model is asked to "always remember" is something a hook should enforce instead.

Hooks live in .claude/settings.json (team-shared, commit to the repo) or .claude/settings.local.json (personal, gitignored). A minimal block looks like this:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/block_rm_rf.sh" }
        ]
      }
    ]
  }
}

The matcher is a regex against the tool name. The command is anything your shell can run: a bash script, a Python file, a Node script, a binary, even a curl call to an internal service. Keep it fast, because every Bash tool call now waits on this hook to return.

The 2026 lifecycle in one table

Event When it fires Can it block? Typical use
SessionStart A new Claude Code session begins No Inject project context, set env, warm caches
UserPromptSubmit User sends a prompt Yes (exit 2 = reject) Reject prompts that mention prod secrets, log audit trail
PreToolUse Just before a tool call executes Yes (exit 2 = block) Block rm -rf, force code review, require ticket id
PostToolUse After a tool call returns success No (tool already ran) Auto format, run tests, push to CI, redact diffs
Notification Claude emits a notification No Mirror to Slack, log to observability stack
PreCompact Right before context compaction No Snapshot state, save artifact pointers
Stop Top-level agent turn ends Special: additionalContext can continue Add follow-up checklist, fetch missing data
SubagentStop A spawned subagent finishes Special: additionalContext can continue Summarize result back to parent, gate merge
SessionEnd Session terminates No Flush logs, persist memory, post a summary

Two events are special. Stop and SubagentStop received an upgrade where the hook can return { "hookSpecificOutput": { "additionalContext": "..." } } to feed new content back into the turn without being treated as an error. Their input payload now includes background_tasks and session_crons, so the hook can see what is still scheduled before deciding whether the turn really should stop.

Exit codes and the contract

Hooks communicate with three channels: exit code, stdout, and stderr. The contract is simple and worth memorizing.

  • Exit 0: success. Stdout is appended to the model's context if the event allows it. Stderr is shown to the user only.
  • Exit 2: hard block. The action is canceled and stderr is fed back to Claude as the reason. This is the magic code for guard rails.
  • Any other non-zero code: error. The tool call fails, the model sees the failure, but the action is not specifically blocked.

If you want to inject structured data, write JSON to stdout. Hooks now accept a top-level hookSpecificOutput object, and Stop / SubagentStop read additionalContext from it. PreToolUse reads permissionDecision, permissionDecisionReason, and modifyInput from the same object, so you can also rewrite a tool input on the fly instead of blocking it outright.

Pattern 1: PreToolUse guard rails for destructive commands

This is the first hook every Claude Code team should add, today. It prevents the model from rm-ing your home directory, dropping a production database, or force-pushing main. The script:

#!/usr/bin/env bash
# .claude/hooks/block_destructive.sh
set -euo pipefail
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')

deny='(^|[^A-Za-z])(rm[[:space:]]+-rf?|drop[[:space:]]+database|truncate[[:space:]]+table|git[[:space:]]+push[[:space:]]+.*--force|psql.*--command=.*delete[[:space:]]+from)'

if echo "$cmd" | grep -qiE "$deny"; then
  echo "Blocked by guard rail: command matches destructive pattern. Reword the request, or ask a human to run this manually." >&2
  exit 2
fi
exit 0

Wire it into PreToolUse with a Bash matcher and it runs on every shell call the agent attempts. The reason string in stderr is what the model sees, so make it actionable: tell the model what to do next, not just what went wrong. We learned that the hard way after Claude tried the same blocked command nine times in a loop because the rejection message did not suggest an alternative.

Pattern 2: PostToolUse format and test

PostToolUse runs after a tool succeeds. It is your auto-format hook, your linter, and your fast test runner. Because the tool has already run, your hook cannot block, but it can:

  • Run ruff format, prettier --write, or gofmt on changed files.
  • Run unit tests for the changed module only.
  • Append findings into the model's next turn via stdout.

A small Python wrapper that formats and reports back:

#!/usr/bin/env python3
# .claude/hooks/post_edit.py
import json, subprocess, sys

data = json.load(sys.stdin)
paths = data.get("tool_response", {}).get("written_files", [])

py = [p for p in paths if p.endswith(".py")]
if py:
    subprocess.run(["ruff", "format", *py], check=False)
    r = subprocess.run(["ruff", "check", *py], capture_output=True, text=True)
    if r.returncode != 0:
        print(json.dumps({
            "hookSpecificOutput": {
                "additionalContext": f"Lint findings:\n{r.stdout}\nFix before continuing."
            }
        }))
sys.exit(0)

The pattern is: format silently, surface only what the model needs to act on. Do not flood the context with successful lint output; the model will start summarizing your noise instead of solving the task.

Pattern 3: UserPromptSubmit secret scrubbing

This hook prevents secrets from ever entering a Claude conversation in the first place. It inspects the user prompt and rejects it (or rewrites it) when it detects an obvious credential.

#!/usr/bin/env bash
# .claude/hooks/scrub_secrets.sh
input=$(cat)
prompt=$(echo "$input" | jq -r '.user_prompt // ""')

if echo "$prompt" | grep -qE '(AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9]{32,}|-----BEGIN [A-Z ]+PRIVATE KEY-----)'; then
  echo "Prompt contains what looks like a credential. Redact it and resend." >&2
  exit 2
fi
exit 0

A more advanced version of the same hook rewrites the prompt by emitting { "hookSpecificOutput": { "modifyInput": { "user_prompt": "..." } } }, replacing the secret with a placeholder before the model ever sees it. This is the safest UX, because the agent still answers the user's intent.

Pattern 4: Stop hook that extends the turn

The Stop hook is your "are you sure you are done" check. The 2026 contract lets it return hookSpecificOutput.additionalContext to add a new instruction without raising a hook error. This is how you implement a soft completion gate.

#!/usr/bin/env python3
# .claude/hooks/stop_gate.py
import json, sys
data = json.load(sys.stdin)
bg = data.get("background_tasks", [])
crons = data.get("session_crons", [])

if bg or crons:
    print(json.dumps({
        "hookSpecificOutput": {
            "additionalContext": (
                f"{len(bg)} background task(s) and {len(crons)} cron(s) are still scheduled. "
                "Confirm they are intentional, or cancel them before ending the turn."
            )
        }
    }))
sys.exit(0)

The model now sees the additional context and either explains the leftover background work or cleans it up. The user experience is that Claude wraps up cleanly instead of orphaning a half-spawned task.

Pattern 5: SubagentStop hook that gates a merge

When you delegate work to a subagent (via the Task tool or the Agent SDK), SubagentStop fires when that subagent's conversation ends. You can use it to enforce a contract: the subagent must have written a report file, opened a PR, or produced a structured artifact before its result is considered final. If the contract is not met, return additionalContext that tells the parent to redo the work or reject it.

This is the right place to enforce "every research subagent must cite at least two sources" or "every refactor subagent must include a test diff". Skills define the procedure, the Stop hook enforces the result. Compare this with a claude code subagents pattern that uses the Agent SDK to control concurrency.

Skills plus Hooks: the combo recipe

We covered Skills as the dynamic procedural knowledge layer in our Claude Code Skills guide. A Skill teaches the model how to do something; a Hook makes sure the model actually does it the right way.

The combo recipe:

  1. Write a Skill called database-migration that documents the team's migration procedure: format, reversibility, naming.
  2. Write a PostToolUse Hook that runs whenever a .sql file is written: it runs the team's migration linter, checks for irreversible operations, and rejects via additionalContext if anything is missing.
  3. Add a Stop Hook that requires the model to have run migrate dry-run once before claiming the migration is ready.

The Skill makes the model competent. The Hooks make the model accountable. Neither is enough on its own. A Skill alone is a polite suggestion; a Hook without a Skill leaves the model to guess what the rule actually wants. We use this exact pattern internally on the Totalum codebase, and our migration-related on-call incidents went to zero after we shipped it.

For when to reach for each, here is the same table we publish in our Skills guide:

Need Skills Hooks Subagents
Procedural knowledge the model loads when relevant Yes No No
Hard enforcement that cannot be skipped No Yes No
Parallel sub-conversations with isolated context No No Yes
Runs deterministically every time No Yes No
Survives a misbehaving model No Yes Partial
Composable across teams and repos Yes Yes Yes

Pattern 6: Hooking a Totalum MCP server

Totalum exposes an MCP server so Claude Code agents can read and write your project's tables, files, deployments, and secrets. When a Claude Code agent edits production data through that MCP, you want a hook to enforce two rules: only specific tables can be mutated from an agent session, and every mutation gets logged with the user who ran the session.

A PreToolUse hook with a mcp__totalum__.* matcher:

#!/usr/bin/env python3
# .claude/hooks/totalum_guard.py
import json, sys, os, urllib.request, time

data = json.load(sys.stdin)
tool = data.get("tool_name", "")
ti = data.get("tool_input", {})

if tool.endswith("createRecord") or tool.endswith("editRecordProperties") or tool.endswith("deleteRecordById"):
    table = ti.get("typeId", "")
    if table in {"users", "billing", "production_secrets"}:
        print("Totalum guard: this table is read-only from agent sessions. Ask a human.", file=sys.stderr)
        sys.exit(2)

    audit = {
        "ts": int(time.time()),
        "tool": tool,
        "table": table,
        "session_user": os.environ.get("CLAUDE_USER", "unknown"),
    }
    req = urllib.request.Request(
        os.environ["AUDIT_WEBHOOK_URL"],
        data=json.dumps(audit).encode(),
        headers={"Content-Type": "application/json"},
    )
    urllib.request.urlopen(req, timeout=2)
sys.exit(0)

The result is that your agents are productive against your real data, but the rails are made of shell code, not of the model's good intentions. For more MCP wiring patterns, see our list of the best MCP servers in 2026 and the Claude Code MCP tutorial.

Performance and timeouts

Every PreToolUse hook adds latency to every matched tool call. Keep them under 100 ms in the hot path. Tactics that work:

  • Match narrowly. Use a specific regex on tool_name, not .*.
  • Skip work cheaply. Read the input first, return success when nothing applies.
  • Push heavy checks to PostToolUse. The tool has already run; the model is waiting on a summary, not a permit.
  • Background slow work. Use the new background_tasks slot for things like uploading audit logs.

If your hook needs to call an external service, set a tight timeout (1 to 2 seconds) and fail open with a logged warning. Failing closed turns every Claude Code session into a hostage of your audit service uptime.

Security checklist

Hooks are the first line of defense, but they are not magic. Before you ship a hooks config to your team, walk through this list.

  • Treat .claude/settings.json as code: review changes in PRs, require approvals, sign commits.
  • Pin hook scripts inside the repo at .claude/hooks/ and audit them like any other build script. A malicious PR that adds a hook is the cheapest supply chain attack on a developer's machine.
  • Use .claude/settings.local.json (gitignored) only for personal overrides. Anything that protects production belongs in the shared file.
  • Sanitize the JSON you read on stdin. Treat fields like command, user_prompt, and tool_input as untrusted input.
  • Log hook decisions to your audit pipeline, including denials. You will want this when an engineer asks why a command was blocked.
  • Test your hooks. We keep a tests/hooks/ folder with golden inputs and expected exit codes, run on every CI build.

When hooks are the wrong answer

Hooks are not a fix for an unclear product spec or a confused team. If the model is doing the wrong thing because it does not know what the right thing is, write a Skill or a CLAUDE.md file first and a hook second. If the model is doing the right thing inconsistently, a hook is correct. The mental model is: hooks fight noncompliance, not ignorance.

Hooks are also a poor place to encode business logic. If a rule needs to apply to humans as well as agents, put it in a CI check, a database trigger, or a deploy gate. Hooks are agent-only by definition; a duplicate rule that lives only in .claude/settings.json is a hidden production assumption that will surprise the next engineer.

A repo skeleton you can copy

The structure we use on every Totalum repo:

.claude/
  settings.json          # team-shared hooks config, in git
  settings.local.json    # personal, gitignored
  hooks/
    block_destructive.sh
    post_edit.py
    scrub_secrets.sh
    stop_gate.py
    subagent_gate.py
    totalum_guard.py
  skills/
    database-migration.md
    deploy-runbook.md
tests/
  hooks/
    test_block_destructive.sh
    test_post_edit.py

Commit the scripts. Commit the tests. Treat .claude/ like a first-class part of your engineering process, not a personal config corner. This is the same posture we recommend in our Claude Code pricing analysis, where the cost of an unsupervised agent comes from missing rails, not from the per-token billing.

If you want a side-by-side comparison with other agent runtimes that handle this differently, see Cline vs Claude Code for how the two products approach lifecycle interception.

FAQ

What is the difference between PreToolUse and PostToolUse?

PreToolUse fires before the tool call runs and can block it by exiting with code 2. PostToolUse fires after the tool call succeeds and can format, lint, or test the result. PostToolUse cannot undo the tool call. Use PreToolUse for guard rails; use PostToolUse for cleanup, validation, and feedback.

Can hooks rewrite a tool's input instead of blocking it?

Yes. A PreToolUse hook can write { "hookSpecificOutput": { "modifyInput": { ... } } } to stdout. The runtime applies the modified input to the tool call. This is how you redact secrets, normalize paths, or clamp parameters without rejecting the call.

What do Stop and SubagentStop hooks do in 2026?

Both fire when an agent (top-level or subagent) ends a turn. They receive background_tasks and session_crons in their input so they can see what is still scheduled. They can return hookSpecificOutput.additionalContext to add new context and keep the turn going instead of ending it. Use Stop for completion gates and SubagentStop for merge contracts.

How do hooks compare to Skills and Subagents?

Skills teach the model how to do something. Hooks enforce that it is done correctly. Subagents run isolated sub-conversations with their own context. They are complementary: a real production setup uses all three. See our Claude Code Skills guide for the side-by-side decision table.

Where do I put the hooks config?

Team-shared rules go in .claude/settings.json, committed to the repo. Personal overrides go in .claude/settings.local.json, which should be gitignored. Anything that protects production belongs in the shared file so it cannot be silently disabled by a teammate.

Will a slow hook slow down every Claude Code call?

Yes, if it matches every tool call. Keep PreToolUse hooks under 100 ms, match narrowly with a specific regex, and push slow checks to PostToolUse where the tool has already run. For external service calls, use a 1 to 2 second timeout and fail open with a warning, never fail closed on an audit service.

Can a hook call a Totalum MCP server?

Yes. Hook scripts are ordinary shell or Python code, so they can call any HTTP API, including Totalum's. The pattern in this guide uses a PreToolUse hook to enforce read-only access to sensitive tables and to write an audit record on every mutation. See the Claude Code MCP tutorial and the best MCP servers in 2026 for related wiring.

Ready to wire your Claude Code agent to a real backend?

Hooks are the second half of a productive agent loop. The first half is having a real production backend the agent can build against: authentication, database, payments, file storage, deploys, and custom domains. Totalum gives you all of that, with an MCP server your agent can call, and the hook patterns in this guide work against it from the first session.

Start free at totalum.app and connect your Claude Code agent today.

If your hook strategy routes subagents by model tier, factor in Claude Fable 5, released June 9, 2026. Fable 5 is the new senior tier at $10 / $50 per million tokens with 1M context, sitting above Opus 4.8 and Haiku 4.5 in the routing table.

Francesc

Writes for the Totalum blog about AI app building, no-code development, and product engineering.

Related posts

Start building with Totalum

Create your web app with AI in minutes. No code needed.

Try Totalum for free