DEV Community

Zhengxin
Zhengxin

Posted on

Claude Code Agent Loop Deep Dive (2): Hooks as Programmable Intervention Points

The previous article explained permission approval in the loop: after the LLM emits tool_use but before the tool actually executes, an interception layer lets the user decide.

But users may want to add custom logic to the loop for far more than tool approval:

  • inspect every Bash command before it runs;
  • run a formatter after every Edit;
  • load shared project rules at session start;
  • snapshot the conversation before compaction;
  • prevent the loop from stopping until it has performed another check.

These are all hooks. Hooks are the general mechanism for placing custom logic inside an agent loop. Permission approval is one specialized use of that general capability.

This article asks:

  • How many hook events exist, and where do they sit in the loop?
  • What input and output does a hook have?
  • Can a hook block an action or modify its result?
  • What happens when a hook stalls or fails?

Twenty-six hook events

Claude Code has far more hook locations than a basic event system might suggest. In Claude Code v2.1.220, the 26 events group naturally by lifecycle stage.

Session lifecycle

  • SessionStart — a new session begins
  • SessionEnd — a session exits
  • Setup — initial configuration
  • ConfigChange — a configuration file changes

User input and elicitation

  • UserPromptSubmit — before the user’s submitted prompt enters the loop
  • Elicitation / ElicitationResult — when user clarification is requested and then received

Tool lifecycle

  • PreToolUse — before every tool execution
  • PostToolUse — after a successful tool execution
  • PostToolUseFailure — after a tool failure
  • PermissionRequest — when approval is required
  • PermissionDenied — after approval is denied

Turn completion

  • Stop — when the loop intends to finish because the model returned no tool use
  • StopFailure — when stop handling itself fails

Task lifecycle

  • TaskCreated — a task is created
  • TaskCompleted — a task completes

Context compaction

  • PreCompact — before compaction
  • PostCompact — after compaction
  • InstructionsLoaded — after instructions such as CLAUDE.md load

Subagents and teams

  • SubagentStart — a subagent starts
  • SubagentStop — a subagent stops
  • TeammateIdle — a teammate becomes idle in collaborative work

Files and workspace

  • FileChanged — a file changes outside the current action
  • CwdChanged — the working directory changes
  • WorktreeCreate / WorktreeRemove — a worktree is created or removed

Miscellaneous

  • Notification — a notification is triggered

Every event maps to a specific point in the loop. Users register handlers under the event name in settings.json; Claude Code invokes them automatically when that point is reached.

Four executor types

The hooks configuration supports four ways to implement a handler.

1. command: a shell command

This is the most common form. Claude Code launches a subprocess at the hook point.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{ "type": "command", "command": "npx prettier --write $CLAUDE_FILE_PATHS" }]
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

The hook receives input through environment variables such as $CLAUDE_FILE_PATHS, and communicates its decision through stdout and its exit code.

2. prompt: an LLM judgment

This executor sends a prompt to the model:

{ "type": "prompt", "prompt": "Assess whether this change introduces a security issue. Reply only YES or NO." }
Enter fullscreen mode Exit fullscreen mode

It is useful when the rule is complex enough that the user would rather ask a model than write deterministic logic.

3. agent: a subagent task

An agent hook starts a full subagent:

{ "type": "agent", "agentType": "general-purpose", "prompt": "..." }
Enter fullscreen mode Exit fullscreen mode

It is heavier than a prompt hook, but the subagent can execute a complete loop of its own.

4. http: a webhook

An HTTP hook sends the event to an external service:

{ "type": "http", "url": "https://internal-hooks.company.com/pre-tool-use" }
Enter fullscreen mode Exit fullscreen mode

This supports cross-machine automation. For example, a security team can maintain a central policy service that evaluates PreToolUse for every Claude Code user.

Together, the executors span four levels of complexity: shell script, one-shot LLM judgment, complete subagent, and remote policy service.

Hooks can block and modify

Hooks are not merely observers. Depending on the event, they can alter loop behavior.

Blocking

A hook can return decision: "block" or exit with code 2. The loop follows a blocking branch. For example, a blocked PreToolUse prevents the tool from executing and returns an is_error tool result to the LLM.

Modifying context

A hook can return additional_context. Claude Code adds it to the tool result as an <attachment>, so the LLM sees the annotation on its next decision. This is especially useful after tool execution: a hook can inspect a result and attach an extra warning or explanation.

{
  "continue": true,
  "decision": "block",
  "reason": "...",
  "additional_context": "..."
}
Enter fullscreen mode Exit fullscreen mode

continue: false ends the entire loop. The exact meaning of a block depends on its event:

Event Effect of a block
PreToolUse The tool does not execute; the LLM receives an is_error result
PostToolUse A hook_stopped_continuation attachment is added and the loop exits
Stop The intended completion is rejected and the loop must run again
UserPromptSubmit The user’s input is rejected before it enters messages

This is the key design insight: hooks are not simple publish-subscribe callbacks. They are programmable intervention points that can change which path the loop takes.

Synchronous, asynchronous, and rewaking hooks

Hooks are synchronous by default: the loop waits until the hook returns. The default limit is generous—TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 min—because a hook may involve an LLM call or a CI trigger. If it exceeds the limit, the hook is killed and the loop proceeds.

A hook can instead be asynchronous:

{ "type": "command", "command": "...", "async": true }
Enter fullscreen mode Exit fullscreen mode
  • async: true is fire-and-forget: the loop continues immediately.
  • asyncRewake: true is more subtle. If the asynchronous hook exits with code 2, it re-wakes the LLM.

asyncRewake lets a long background action notify the conversation that it now has something worth handling. A five-minute code analysis can run without blocking the user, then automatically return control to the model when its result is ready. It is an elegant event-driven wakeup mechanism for the loop.

What if a hook fails?

An unexpected hook failure—an exit code other than 0 or 2, invalid JSON, or an exception—does not crash the loop. Claude Code follows a non_blocking_error path:

  • it records the error;
  • the tool continues in a pre-tool case, or its existing result is used in a post-tool case;
  • the user usually sees no disruptive error.

The philosophy is that a hook is an optional enhancement, not the main path. A broken enhancement must not break the agent. The exception is an explicit, valid decision such as decision: "block" or continue: false: those decisions are honored.

Hooks and permission approval

The PermissionRequest hook from the previous article is one of the three competitors in an approval race, alongside user input and the built-in classifier.

{
  "hooks": {
    "PermissionRequest": [{
      "hooks": [{ "type": "command", "command": "./ci-safety-check.sh" }]
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

When approval is needed, Claude Code starts ci-safety-check.sh. The script can consult an internal policy database and quickly return allow or deny—possibly before the user has responded in the UI. The fastest valid answer wins.

This makes approval a pluggable problem. User clicks are slow but authoritative; hooks are programmable; the classifier is fast but may be conservative. Hooks turn one fixed product behavior into an extensible policy boundary.

The generality of hooks

Permission approval answers one narrow question: may a tool call proceed? Hooks generalize the idea across 26 locations. Without modifying Claude Code source, users can:

  • inject organization-wide project rules at session start;
  • write an audit log before every tool call;
  • back up conversation state before compaction;
  • prevent the loop from stopping before tests complete;
  • load different rules automatically when the working directory changes.

Each is a custom intervention on the loop itself.

Summary

  • 26 hook events cover the important stages of the loop lifecycle.
  • Four executors—command, prompt, agent, and http—range from scripts to remote services.
  • Hooks can block, modify context, and force loop state transitions.
  • Hooks support sync, async, and asyncRewake execution; the latter can wake an LLM after background work finishes.
  • Unexpected failures are non-blocking, while explicit decisions are respected.
  • Permission approval is a specialized hook use case: the PermissionRequest event makes it programmable.

The next article examines concrete tool execution: when several tool_use blocks arrive together, which can run in parallel, which must remain serialized, and how tool failures become model-visible results.


References

Primary implementation locations (Claude Code v2.1.220):

  • src/entrypoints/sdk/coreTypes.ts — hook-event enumeration
  • src/schemas/hooks.ts — the four executors (command, prompt, agent, http)
  • src/utils/hooks.ts — central dispatcher, executeHooks(), and executePreToolHooks()
  • src/services/tools/toolHooks.ts — pre/post tool hook triggers
  • src/query/stopHooks.ts — Stop-hook loop blocking
  • src/hooks/toolPermission/PermissionContext.ts — permission-hook participation in the approval race

Further reading: Claude Code hooks documentation.

Top comments (0)