---
title: "08 — Hooks — Deterministic Triggers at the Loop's Lifecycle Points"
uid: hooks
tags: ["lifecycle", "roadmap:claude-code", "automation", "ci", "hooks", "claude-code"]
excerpt: "A hook is a deterministic script fired at a lifecycle event that can approve, block, or enrich the next step — a hard guarantee where CLAUDE.md gives soft instructions."
date: 2026-08-13T03:28:21+0000
source: https://www.aveshina.my.id/en/blog/hooks
---

"Git hooks but for AI" was the nearest hook to hand, and it captured the deterministic part but not the integration. The cleaner frame: **a hook is a deterministic script or check fired at a specific lifecycle event of the agent, communicating via JSON to approve, block, or enrich the next step.** [1] Where CLAUDE.md gives soft instructions the model might ignore, hooks give hard guarantees — something that runs _every single time_ a certain event occurs, regardless of what the model "decides."

The framing that clicked is the contrast with skills and subagents. Skills add knowledge the model loads on demand; subagents isolate work into bubbles. Hooks are different: they sit _outside_ the model entirely, on the lifecycle events of the loop itself. The model doesn't choose to call a hook — the harness fires it when the event happens, and the hook's output feeds back into what the model does next [1]. That determinism is the whole point, and it's why hooks are the right tool for enforcement (lint after every edit, block risky commands) rather than suggestion.

## The lifecycle events

The roadmap names the lifecycle moments where a hook can fire, and reading them as points on the loop made them click [1][2]:

- **SessionStart** — fires at the very beginning, before the first prompt. Bootstraps the environment and injects high-priority context.
- **UserPromptSubmit** — fires the moment I press Enter, before the text reaches the model. Lets me programmatically rewrite, validate, or enhance the prompt on the fly.
- **PreToolUse** — fires after the model decides to use a tool but before it runs. The validation gate: security checks, policy enforcement, input sanitization.
- **PostToolUse** — fires immediately after a tool completes. For cleanup and quality control — format the file after an edit, run a check after a command.
- **Stop** — fires once at the very end of a turn, when the model is about to return control to me. A final check or notification.
- **SessionEnd** — fires on exit. Cleanup, archiving, final reporting.

Two ideas make this powerful. First, **events have matchers** — regex filters that decide which specific actions within an event fire the hook (e.g. a PreToolUse hook that only triggers on Bash, not on Read) [3]. Second, the placement lets me intercept at exactly the right granularity: prompt-level (UserPromptSubmit), tool-level (Pre/PostToolUse), or turn-level (Stop).

```figure
<svg viewBox="0 0 740 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="The hook lifecycle events placed around the agentic loop. A central circle labelled Loop. Six pegs on its edge, clockwise from top: SessionStart (bootstrap), UserPromptSubmit (rewrite/validate prompt), PreToolUse (validation gate, can block), PostToolUse (cleanup/quality), Stop (end-of-turn check), SessionEnd (teardown). Each peg has a small script icon. The PreToolUse peg is highlighted with a JSON packet showing stdin input and a stdout decision that blocks a risky action with exit code 2.">
  <defs>
    <marker id="hk1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- loop circle -->
    <circle cx="370" cy="170" r="80" fill="none" stroke="#6366f1" stroke-width="1.8"/>
    <text x="370" y="166" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Loop</text>
    <text x="370" y="184" font-size="9.5" fill="#475569" text-anchor="middle">perceive · decide · act</text>

    <!-- SessionStart top -->
    <rect x="300" y="20" width="140" height="34" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="370" y="42" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">SessionStart</text>

    <!-- UserPromptSubmit right-top -->
    <rect x="510" y="80" width="180" height="34" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="600" y="102" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">UserPromptSubmit</text>

    <!-- PreToolUse right-bottom (highlighted) -->
    <rect x="510" y="200" width="180" height="40" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="2"/>
    <text x="600" y="218" font-size="11" font-weight="700" fill="#7f1d1d" text-anchor="middle">PreToolUse</text>
    <text x="600" y="232" font-size="9" fill="#7f1d1d" text-anchor="middle">validation gate · can block</text>

    <!-- PostToolUse bottom -->
    <rect x="300" y="280" width="140" height="34" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="370" y="302" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">PostToolUse</text>

    <!-- Stop left-bottom -->
    <rect x="50" y="200" width="120" height="34" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="110" y="222" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">Stop</text>

    <!-- SessionEnd left-top -->
    <rect x="50" y="80" width="120" height="34" rx="8" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5"/>
    <text x="110" y="102" font-size="11" font-weight="700" fill="#334155" text-anchor="middle">SessionEnd</text>

    <!-- JSON packet near PreToolUse -->
    <rect x="500" y="250" width="200" height="58" rx="6" fill="#1e293b" stroke="#dc2626" stroke-width="1"/>
    <text x="600" y="266" font-size="8.5" font-family="ui-monospace,monospace" fill="#e2e8f0" text-anchor="middle">stdin → {tool:"Bash", cmd:"rm -rf"}</text>
    <text x="600" y="280" font-size="8.5" font-family="ui-monospace,monospace" fill="#e2e8f0" text-anchor="middle">stdout ← {decision:"block"}</text>
    <text x="600" y="296" font-size="8.5" font-family="ui-monospace,monospace" fill="#fca5a5" text-anchor="middle">exit code 2 · action denied</text>
  </g>
</svg>
```

## The three hook types

For each event I can configure one of three handler types, and the roadmap is explicit about the distinction [4]:

- **Command hooks (type: "command")** — deterministic shell scripts. They run a standard command (like npm run lint) and use exit codes to approve or block. Exit 0 approves; a non-zero code blocks with the error message shown to the model. This is the "I want a hard guarantee" option.
- **Prompt hooks (type: "prompt")** — a lightweight Claude model makes a single-turn judgement call ("is this commit message descriptive?") and returns a JSON yes/no. Use when the check itself needs interpretation, not just a fixed command.
- **Agent hooks (type: "agent")** — the most sophisticated: spawns a multi-turn subagent with tool access (Read, Grep) to conduct deep, autonomous verification before deciding whether the main agent should proceed.

The rule of thumb I use: start with a command hook (deterministic, cheap, no model involved). Reach for a prompt hook when the check is a judgement ("is this good?"). Reach for an agent hook only when verification needs real investigation across files.

## Inputs, outputs, and the JSON contract

Hooks communicate through a standardized JSON interface, and getting this straight was what made them usable [5]:

- **Inputs arrive via stdin** — a context object with session metadata and event-specific data, such as the tool name and its arguments (the exact code being written or command being run).
- **Outputs return via stdout** — a JSON object that influences the agent's next move. The shape of the output decides whether the action proceeds, gets blocked with a message, or gets enriched with extra context.

For command hooks specifically, the exit code is the blunt instrument: 0 to allow, non-zero (commonly 2) to block. This is what makes a PreToolUse command hook a real security gate — it can refuse a dangerous command before it ever runs, deterministically, every time.

## The slash commands that manage them

- **/hooks** — opens the interactive management menu for configuring automated workflows at lifecycle points [6].
- **/config** — the central customization menu (global and project settings, defaults, permissions) [7].
- **/permissions** — the security interface for granting permanent trust to specific tools or commands, reducing interruptions [8].
- **/model** — switches the model mid-session (relevant because prompt/agent hooks use a model) [9].
- **/agents** — manages subagents (relevant because agent hooks spawn one) [10].
- **/mcp** — the MCP integration hub (covered in its own post) [11].

## How I use this

The habit these notes left me with is a simple rule: _if a standard must hold every single time, it's a hook; if it's a preference, it's CLAUDE.md._ So PostToolUse command hook to run the formatter after every edit (the codebase is always formatted), PreToolUse command hook to block commands matching destructive patterns (the dangerous action never runs), Stop hook to notify me when a long turn ends. Soft preferences ("prefer named exports") stay in the memory file where the model can weigh them. Hard guarantees become hooks because the model cannot choose to skip them.

## References

[1] Anthropic, "Automate workflows with hooks," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/hooks-guide](https://code.claude.com/docs/en/hooks-guide)

[2] Anthropic, "Hooks reference," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)

[3] Anthropic, "Event Matchers," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/hooks#matcher-patterns](https://code.claude.com/docs/en/hooks#matcher-patterns)

[4] Anthropic, "Hook Types," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)

[5] Anthropic, "Hook Inputs & Outputs," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/hooks#hook-input-and-output](https://code.claude.com/docs/en/hooks#hook-input-and-output)

[6] Anthropic, "The /hooks menu," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/hooks#the-hooks-menu](https://code.claude.com/docs/en/hooks#the-hooks-menu)

[7] Anthropic, "Claude Code settings," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/settings](https://code.claude.com/docs/en/settings)

[8] Anthropic, "Manage permissions," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/permissions](https://code.claude.com/docs/en/permissions)

[9] Anthropic, "Model configuration — /model," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/model-config#setting-your-model](https://code.claude.com/docs/en/model-config#setting-your-model)

[10] Anthropic, "Create custom subagents — /agents," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/sub-agents#use-the-agents-command](https://code.claude.com/docs/en/sub-agents#use-the-agents-command)

[11] Anthropic, "Connect Claude Code to tools via MCP — /mcp," Claude Code Docs, 2025. [Online]. Available: [https://code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp)

```quiz
Q: What is the core difference between a CLAUDE.md rule and a hook?
- They are the same; both are soft instructions
- CLAUDE.md is a soft instruction the model may ignore; a hook is a deterministic script that fires every time its event occurs
correct: 1
explain: CLAUDE.md gives preferences the model weighs. Hooks fire on lifecycle events regardless of the model's decision — they sit outside the model, which is why they're the right tool for hard guarantees.

Q: Which lifecycle event fires AFTER the model decides to use a tool but BEFORE the tool runs?
- PostToolUse
- PreToolUse
correct: 1
explain: PreToolUse is the validation gate — it can block a risky action via exit code before it ever executes. PostToolUse fires after the tool completes.

Q: You need a check that decides whether a commit message is "descriptive enough." Which hook type fits?
- Command hook (shell script with exit codes)
- Prompt hook (a lightweight model makes a single-turn yes/no judgement)
correct: 1
explain: "Descriptive enough" is a judgement, not a fixed command. A prompt hook uses a lightweight model for a single-turn evaluation and returns a JSON decision. Command hooks suit deterministic checks like running a linter.

Q: How do hooks receive and return data?
- Via environment variables only
- Inputs arrive on stdin as JSON; outputs return via stdout as JSON (command hooks also use exit codes to approve/block)
correct: 1
explain: The JSON contract is standardized: a context object on stdin, a decision object on stdout. For command hooks, exit 0 allows and non-zero (often 2) blocks with a message.

Q: What do event matchers do?
- They replace the need for a hook script
- They are regex filters that decide which specific actions within an event fire the hook (e.g. only Bash, not Read)
correct: 1
explain: Matchers scope a hook to the relevant actions. A PreToolUse hook with a Bash matcher fires only on shell commands, leaving Read/Edit untouched.
```
