In coding agents, hooks are lifecycle callbacks that run custom logic at defined execution points without modifying the agent core. They are the control layer for guardrails, formatting, compliance, and operational automation. Mechanically, the agent reaches a trigger point, pauses the main loop, passes event context as structured JSON to one or more registered handlers, and uses the handler result to continue, modify, or abort the operation. This is the same interceptor pattern as Git hooks and CI pipeline steps, adapted to the agent loop where the unit of work is a tool call rather than a commit.

flowchart LR
    A[Agent loop] --> B[Event fires]
    B --> C[Matcher checks]
    C --> D[Handler runs]
    D --> E{Decision}
    E -->|Allow| F[Agent resumes]
    E -->|Deny| G[Tool call blocked]

For reusable instruction bundles that shape agent behavior, see Skills. For MCP tool extensions, see Plugins.

How Hooks Work

The hook system has three layers: events define when hooks fire, matchers filter which firings are relevant, and handlers execute the logic.

Events are lifecycle moments grouped by purpose:

CategoryExamplesPurpose
Tool eventsPreToolUse, PostToolUse, PostToolUseFailureGuardrails and quality gates on tool calls
Session eventsSessionStart, Stop, SessionEndSetup, teardown, completion logic
Prompt eventsUserPromptSubmit, PreCompactInput validation, context management
Agent eventsSubagentStart, SubagentStopOrchestration and monitoring
Operational eventsNotification, ConfigChange, TaskCompletedAlerts, audit, workflow integration

Matchers are regex patterns that filter when a handler runs. For tool events, the matcher tests against the tool name — Edit|Write matches file-editing tools, mcp__.* matches any MCP tool. Some events like Stop fire unconditionally without matcher support.

Handlers come in four types (Claude Code, the most comprehensive implementation):

  • Command: shell script receives JSON on stdin, returns exit code and optional JSON on stdout. Most common type.
  • HTTP: POST to an endpoint with the same JSON contract. Good for external service integration.
  • Prompt: single-turn LLM evaluation for nuanced decisions that scripts cannot express as rules.
  • Agent: spawns a subagent with tool access to verify conditions before returning a decision.

Exit code semantics for command hooks on block-capable events like PreToolUse: 0 allows execution, 2 denies and blocks the tool call, other non-zero codes signal an error. Post-execution events like PostToolUse cannot retroactively undo completed actions.

Hooks Across Tools

Claude Code has the most mature hook implementation: 17 event types, 4 handler types, regex matchers, async execution, and configuration at user, project, managed-policy, plugin, and skill scope. JSON config lives in settings.json files at each scope level.

Cursor introduced hooks in v1.7 (October 2025) with a similar event model: beforeShellExecution, afterFileEdit, beforeMCPExecution, sessionStart, sessionEnd, stop, and others. Command-only handlers. Config lives in .cursor/hooks.json at project or global scope.

Other agents (Windsurf, Cline, Aider) have no formal lifecycle hooks yet. These tools use rules, auto-approve settings, or git hooks as their enforcement boundary.

Git hooks are the foundational model. pre-commit and commit-msg enforce standards before history is written; post-commit and server-side hooks support notifications and policy checks. Agents without native hooks typically fall back to git hooks for quality gates.

Concrete Example

A two-stage Claude Code policy: block edits to protected files up front, then auto-format accepted writes immediately after execution.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -I{} check-protected-files.sh \"{}\""
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -I{} npx prettier --write \"{}\""
          }
        ]
      }
    ]
  }
}

Cursor uses a similar structure in .cursor/hooks.json with event names like afterFileEdit instead of PostToolUse.

Pitfalls

Slow Hooks Stall the Agent Loop

  • What goes wrong: heavyweight commands (full test suites, whole-repo lint) run synchronously on every tool call, inflating loop latency.
  • Why it happens: hooks are blocking by default and developers add broad checks without scoping them to changed files.
  • How to avoid it: scope checks to changed files only, use matchers to limit which tools trigger which hooks, and mark non-critical hooks as async where the runtime supports it.

Silent Failures Pass Violations Through

  • What goes wrong: the hook script exits 0 even when it should have blocked, or writes errors to stderr that the agent runtime ignores.
  • Why it happens: scripts swallow non-zero exit codes in pipelines, or error handling defaults to continue instead of fail closed.
  • How to avoid it: test hooks against known-bad inputs, ensure the error path explicitly returns exit code 2, and mirror critical hook logic in CI as a safety net.

Post-Hooks Race with Agent Edits

  • What goes wrong: a PostToolUse formatter rewrites a file the agent is about to read or edit again in the next loop iteration, creating conflicting diffs or stale reads.
  • Why it happens: the agent does not re-read files after post-hooks run, or the formatter changes semantics beyond whitespace.
  • How to avoid it: limit post-hooks to deterministic formatters (Prettier, Black) that produce semantically equivalent output, and verify the agent re-reads modified files after hook execution.

Tradeoffs

ChoiceOption AOption BDecision criteria
Validation postureStrict blocking via PreToolUse denyAdvisory logging via PostToolUse warnStrict catches violations before damage but adds latency and can false-positive block valid operations. Start strict for destructive operations; use advisory for style and formatting.
Hook scopeBroad checks on every tool callTargeted checks scoped by matcherBroad gives complete coverage but makes the agent slow and brittle. Default to targeted; broaden only when you find gaps in coverage.
Handler typeCommand via shell scriptPrompt or Agent via LLM evaluationCommands are fast, deterministic, and debuggable. LLM handlers evaluate nuance but add inference latency and non-determinism. Use commands for clear policy rules; use LLM hooks for subjective judgment calls on high-stakes decisions.

Questions

References