Skip to content

Hooks

You told the agent, in CLAUDE.md, “never push to main.” You said it again at the start of the session: “don’t push to main, the release branch is release/2026.04.” Two hours later, mid-flow, you ask it to “ship the fix.” It runs git push origin main.

The agent didn’t ignore you on purpose. It read the instruction, weighed it against the goal, and the goal won. That’s what models do - they’re persuadable, including by their own reasoning. A CLAUDE.md rule is a strong suggestion to a probabilistic system. And probabilistic systems, sometimes, push to main.

For a supported event, a command hook can move a decision out of the model’s reasoning. A PreToolUse hook that pattern-matches git push.*main and blocks the call doesn’t ask the model nicely. It can refuse that call before execution. That’s the gap hooks can close: turning “please don’t” into a runtime check.

A hook, then, is a piece of code or a command that fires automatically when a specific lifecycle event happens inside the CLI - before a tool call, after a file edit, when a session starts, when the model wants to compact context, when a permission is requested. You declare what event and what to run, and the CLI takes it from there.

The defining property is runtime triggering, not a universal guarantee. Command hooks can make a supported gate deterministic, while prompt- and agent-based hooks still involve a model and some surfaces have fail-open behavior. Where a memory rule is a request and a skill is opt-in by the model, a command hook is enforcement by the runtime when the tool documents that decision path.

Real-world examples of what teams put in hooks:

  • Hard guardrails - block rm -rf, block edits to .env, block writes to /etc, block pushes to protected branches. The model can attempt; the hook says no.
  • Auto-run linters and formatters - PostToolUse on file edits runs prettier/eslint/ruff and feeds errors back to the agent as context it can fix.
  • Auto-run tests on touched modules - when the agent edits src/auth/*.ts, fire the auth test suite. Catches regressions inside the session, not at PR time.
  • Notifications and observability - post to Slack when a long session ends, log every shell command to a file, ping yourself when the agent’s been idle for 5 minutes.
  • Context injection at session start - SessionStart hook that runs git status / git log -5 and feeds the output in, so the agent always knows the current branch state.
  • Secret scanning - PreToolUse on writes that scans content for API keys before they hit disk.
  • Compaction control - PreCompact hook that snapshots the conversation to a file before the model summarises it away.

The test: if “the agent forgot” or “the agent did the thing I told it not to” is a failure mode you can’t accept, you want a hook, not a memory rule.

The lifecycle events, in the concrete: each tool’s actual event names, where they sit in a session, and which ones can block the action they fire on. Click any event for what it fires on and a working config.

Declared in settings.json (project, user, or managed) under the hooks key. Hooks from all levels merge; every matching hook fires.

Session opens
Each turn
Each tool call
Special moments
Session ends

Hooks fire on events along the loop. The exact event names differ across tools (the comparison table below has the per-tool list), but the rough timeline is the same:

session opens
SessionStart
one-shot: inject context, log who/where
per turn
UserPromptSubmit
inspect or modify your prompt before model
per tool call
PreToolUse
gate: allow, deny, modify
[ tool runs ]
PostToolUse
side effects: lint, test, log
Stop
notify, snapshot
session closes

Two kinds of hook live on this timeline, and the difference is whether the hook gets a vote:

Gating hooksReactive hooks
EventsPreToolUse and equivalentsPostToolUse, SessionStart, Stop
FiresBefore the action runsAfter something happens
Can it decide?Yes - allow, modify the arguments, or block outrightNo - it reacts, it doesn’t decide
What it’s forEnforcing a supported runtime decisionLint the file you just wrote, fire a notification, append to a log

A hook’s superpower is being inside the loop but outside the model’s reasoning. The model can want to push to main all it likes; the gating hook simply doesn’t let the tool call complete.

The classic. You’ve told the agent not to push to main. Three sessions in a row, it pushed to main. A PreToolUse hook on shell commands fixes it forever.

The shape, in pseudocode (real syntax in the per-tool tabs below):

event: PreToolUse
match: tool == "Bash" AND command matches /git\s+push.*\smain(\s|$)/
action: { "decision": "block",
"reason": "Pushes to main are blocked. Use a release branch." }

Walked through one turn:

you: ship the fix
model: I'll run: git push origin main
[ proposes the Bash tool call ]
PreToolUse hook fires
regex matches "git push origin main"
hook returns: decision=block, reason="…"
tool call NEVER runs
model: [ sees the block + reason in its next turn ]
model: Blocked - pushes to main aren't allowed.
Pushing to release/2026.05 instead: git push origin release/2026.05
PreToolUse hook fires again
regex does NOT match - release branch
tool call runs normally

Notice two things. First: the hook fires between “the model proposes a tool call” and “the tool actually runs.” That’s the only place a deterministic gate can live. Second: when the hook blocks, it returns a reason the model can read on its next turn. The model isn’t punished or kicked out - it’s told no, gets the reason in its context, and adapts. The combination is what makes hooks safer than just denying via permissions - permissions say no; hooks say no and explain.

The same pattern generalises. Replace the match condition and you get: block rm -rf outside the project, block edits to .env, block npm install without a lockfile, run prettier after every write, post to Slack on Stop. Same lifecycle, same shape.

You want to…Reach forNot
Enforce a supported gate before an actionCommand hookMemory rule
Give context the model uses on every turnRulesHook
Encode a procedure the model picks up when relevantSkillHook
Restrict what tools/commands are allowed at allPermissionsHook
Run a side effect (lint, notify, log) on a CLI eventHookSkill
Isolate noisy work in its own contextSubagentHook

Hooks are for every time event X happens, do Y, no exceptions. Skills are for when the situation looks like X, the model might decide to do Y. Memory is for the model should know X. If you’ve been frustrated that the agent “knew the rule but did it anyway,” you wanted a hook.

Configuration: hooks live in settings.json (project, user, or managed levels). All registered hooks merge - Claude Code fires every loaded hook that matches a given event. --bare and other configuration choices can prevent project hooks from being discovered.

Events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, SubagentStop, PreCompact, Notification, and more.

Hook actions can be:

  • A shell command
  • An HTTP request
  • A prompt sent to Claude
  • A subagent invocation

Example (.claude/settings.json):

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "scripts/check-bash-command.sh"
}
]
}
]
}
}

Hook output can land in the conversation as a message - a PostToolUse hook running your linter feeds errors back to Claude as text it can read and act on.

AspectClaude CodeCodexOpenCodeCursorCopilotPi
Where definedsettings.jsonconfig.toml [hooks] table or hooks.jsonJS/TS plugin modulehooks.jsonPer-surface (CLI files / Coding Agent repo / VS Code preview)TypeScript extensions (ExtensionAPI lifecycle) - not a separate hooks.json
Declarative vs codeDeclarativeDeclarativeCode (JS/TS)Declarative (commands)DeclarativeCode (TypeScript)
Event taxonomy~10+ named eventsPreToolUse, PostToolUse, PermissionRequest, SessionStart, UserPromptSubmit, StopRichest among the CLIs (plugin events)21+ events incl. beforeReadFile, beforeMCPExecution, beforeShellExecutionsessionStart, sessionEnd, userPromptSubmitted, preToolUseExtensionAPI lifecycle events (tool-call, session, message)
Action typesShell / HTTP / prompt / subagentShell (type = "command")JS codeSubprocess (stdin/stdout JSON)External commandTypeScript code inside the extension
Output lands in contextYesYes - hook script emits JSON on stdout with fields like additionalContext (PreToolUse) or systemMessage; exit 2 + stderr blocks the actionPlugin-controlledYes - JSON response with user_message / agent_messageYes (where supported)Extension-controlled
Layered (project + user + managed)Yes - all mergeYes (user + project, managed available)Yes (global + project + managed)Yes (user + project + enterprise)Per-surfaceYes (~/.pi/agent/extensions + .pi/extensions)
Deterministic gate (allow/deny)Yes (PreToolUse)Yes (PreToolUse, exit 2)Yes (plugin permission.ask)Yes ("permission": "allow" | "deny" | "ask")CLI and cloud-agent hooks are documented; stable IDE support is not listed in the current hooks referenceYes (extension can intercept and block)
  • An OpenCode “hook” lives inside a plugin - you can’t have a hook without writing a plugin. In Claude Code, Codex, and Cursor, hooks are a standalone primitive declared in config.
  • Don’t confuse Claude Code’s PreCompact (model-level context compaction) with shell-level “pre-commit” hooks. Despite the name they have nothing in common.
  • Cursor’s beforeReadFile and beforeMCPExecution have no direct analog in any other tool in the foundations set - they let you gate file reads and individual MCP tool calls before they happen.
  • Copilot’s copilot-setup-steps.yml is sometimes called a “pre-flight hook” but it isn’t one. It’s a GitHub Actions workflow that runs once before a Coding Agent task to prep the sandbox - not a runtime gate, not part of the hook taxonomy, and not applicable to the VS Code IDE surface.

Start with one. Pick the rule your agent has already broken on you - pushes to main, edits to .env, rm -rf outside the project - and wire that gate before anything else. The pseudocode above is the whole shape: an event, a match, a block with a reason. Your tool’s syntax is in the tabs. Then trigger it once on purpose and read what comes back.