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 -
PostToolUseon file edits runsprettier/eslint/ruffand 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 -
SessionStarthook that runsgit status/git log -5and feeds the output in, so the agent always knows the current branch state. - Secret scanning -
PreToolUseon writes that scans content for API keys before they hit disk. - Compaction control -
PreCompacthook 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.
Every event, per tool
Section titled “Every event, per tool”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.
The lifecycle
Section titled “The lifecycle”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:
Two kinds of hook live on this timeline, and the difference is whether the hook gets a vote:
| Gating hooks | Reactive hooks | |
|---|---|---|
| Events | PreToolUse and equivalents | PostToolUse, SessionStart, Stop |
| Fires | Before the action runs | After something happens |
| Can it decide? | Yes - allow, modify the arguments, or block outright | No - it reacts, it doesn’t decide |
| What it’s for | Enforcing a supported runtime decision | Lint 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.
A worked example: blocking pushes to main
Section titled “A worked example: blocking pushes to main”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: PreToolUsematch: 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:
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.
Why this and not…
Section titled “Why this and not…”| You want to… | Reach for | Not |
|---|---|---|
| Enforce a supported gate before an action | Command hook | Memory rule |
| Give context the model uses on every turn | Rules | Hook |
| Encode a procedure the model picks up when relevant | Skill | Hook |
| Restrict what tools/commands are allowed at all | Permissions | Hook |
| Run a side effect (lint, notify, log) on a CLI event | Hook | Skill |
| Isolate noisy work in its own context | Subagent | Hook |
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.
How it works in each tool
Section titled “How it works in each tool”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.
Configuration: Codex hooks load from either an inline [hooks] table in config.toml or a standalone hooks.json, at user (~/.codex/) and project (.codex/) levels. Enterprise admins can also ship managed hooks via requirements.toml.
Events: PreToolUse, PostToolUse, PermissionRequest, SessionStart, UserPromptSubmit, Stop.
Hook actions: only type = "command" handlers actually run today (the schema parses prompt and agent types but skips them). The script receives a JSON event payload on stdin (session_id, cwd, hook_event_name, model, permission_mode, plus a turn_id for turn-scoped events).
Example (~/.codex/config.toml):
[[hooks.PreToolUse]]matcher = "Bash"
[[hooks.PreToolUse.hooks]] type = "command" command = "scripts/check-bash-command.sh"OpenCode’s hook model is plugin-based. You write a JS/TS plugin module that exports a hook map, and OpenCode wires the hooks in.
A plugin function receives a context object (project, client, $, directory, worktree) and returns an object whose keys are event names:
export default ({ project, client, $ }) => ({ 'file.edited': async (event) => { await $`pnpm lint ${event.path}`; },});Available events include command.executed, file.edited, file.watcher.updated, LSP events, message events, permission.asked, permission.replied, session/server events, shell/tool execution events (including env injection), and TUI prompts/commands/notifications.
The event taxonomy is richer than Claude Code’s, but you need to author a plugin to use any of it - there’s no settings.json-style declarative hook list.
Cursor has a deterministic Hooks primitive (introduced in Cursor 1.7). Hooks are spawned subprocesses that communicate via stdin/stdout JSON.
Config files:
- User:
~/.cursor/hooks.json - Project:
<repo>/.cursor/hooks.json - Enterprise / Team: platform-managed and dashboard-distributed
Schema: {"version": 1, "hooks": { "<event>": [{ "command": "..." }] }}.
Event surface is the broadest in the foundations set - 21+ events, wider than Claude Code’s.
Agent hooks: sessionStart, sessionEnd, preToolUse, postToolUse, postToolUseFailure, subagentStart, subagentStop, beforeShellExecution, afterShellExecution, beforeMCPExecution, afterMCPExecution, beforeReadFile, afterFileEdit, beforeSubmitPrompt, preCompact, stop, afterAgentResponse, afterAgentThought.
Tab hooks (inline completion): beforeTabFileRead, afterTabFileEdit.
App lifecycle: workspaceOpen.
Enforcement: exit codes and JSON output drive policy - exit 0 + JSON for nuanced responses, exit 2 to block (equivalent to "permission": "deny"); any other non-zero is fail-open. Hook responses can return "permission": "allow" | "deny" | "ask" plus optional user_message and agent_message fields.
The combination of beforeShellExecution, beforeReadFile, and beforeMCPExecution covers most policy-enforcement use cases (secret scanning, command allowlists, audit logging) - beforeReadFile and beforeMCPExecution have no direct analog in Claude Code or Codex.
A separate Third-Party Hooks doc covers reusable hook scripts.
GitHub’s current Copilot hooks documentation documents hooks for Copilot CLI and cloud agent. It does not list a stable VS Code Chat IDE hooks surface, so do not generalize the CLI/cloud-agent hook behavior to the IDE.
Hook events that the preview / other surfaces expose:
sessionStart- agent session begins or resumessessionEnd- session completes or is terminateduserPromptSubmitted- every user promptpreToolUse- fires before any tool call; can approve or deny the call (the deterministic gate)
Surfaces that support hooks today:
- VS Code - Agent hooks in preview (mid-2026)
- Copilot CLI - file-based hooks
- Copilot Coding Agent (“cloud agent hooks”) - configured per-repo
- Copilot SDK - programmatic hooks for extension authors
preToolUse is the powerful one where supported: it runs an external command before any tool invocation, receives the tool call payload, and decides allow/deny - the policy-enforcement primitive.
copilot-setup-steps.yml is not a hook. It’s a dedicated GitHub Actions workflow at .github/workflows/copilot-setup-steps.yml that runs once per Coding Agent task before the agent starts, to install dependencies and prep the sandbox. Use it for environment prep on the Coding Agent surface only - it doesn’t gate runtime tool calls and doesn’t apply to VS Code Chat.
No separate hooks.json. Pi folds hooks into its extension system: TypeScript modules subscribe to lifecycle events through the ExtensionAPI (pi.on(...)) and can also register tools, commands, and UI.
Where extensions live:
~/.pi/agent/extensions/- global (always loaded).pi/extensions/- project (trust-gated; see Configuration)
Hot reload: /reload picks up extension edits without restarting the session.
Verified event families (from current ExtensionAPI docs - not exhaustive):
project_trust- before project-local resources load; a handler can own the trust decisionsession_start/session_shutdown- session lifecycle (startup, resume, fork, switch)tool_call- before a tool runs; can block (the enforcement point for permission gates)session_before_compact/session_compact- compaction hooks- Turn/agent/message events (
turn_start,agent_end,message_end, …) for observation
The stock example is a permission-gate extension that intercepts bash on tool_call and confirms before dangerous patterns run - hand-rolling the approval prompt Pi doesn’t ship by default (see Permissions).
Comparison
Section titled “Comparison”| Aspect | Claude Code | Codex | OpenCode | Cursor | Copilot | Pi |
|---|---|---|---|---|---|---|
| Where defined | settings.json | config.toml [hooks] table or hooks.json | JS/TS plugin module | hooks.json | Per-surface (CLI files / Coding Agent repo / VS Code preview) | TypeScript extensions (ExtensionAPI lifecycle) - not a separate hooks.json |
| Declarative vs code | Declarative | Declarative | Code (JS/TS) | Declarative (commands) | Declarative | Code (TypeScript) |
| Event taxonomy | ~10+ named events | PreToolUse, PostToolUse, PermissionRequest, SessionStart, UserPromptSubmit, Stop | Richest among the CLIs (plugin events) | 21+ events incl. beforeReadFile, beforeMCPExecution, beforeShellExecution | sessionStart, sessionEnd, userPromptSubmitted, preToolUse | ExtensionAPI lifecycle events (tool-call, session, message) |
| Action types | Shell / HTTP / prompt / subagent | Shell (type = "command") | JS code | Subprocess (stdin/stdout JSON) | External command | TypeScript code inside the extension |
| Output lands in context | Yes | Yes - hook script emits JSON on stdout with fields like additionalContext (PreToolUse) or systemMessage; exit 2 + stderr blocks the action | Plugin-controlled | Yes - JSON response with user_message / agent_message | Yes (where supported) | Extension-controlled |
| Layered (project + user + managed) | Yes - all merge | Yes (user + project, managed available) | Yes (global + project + managed) | Yes (user + project + enterprise) | Per-surface | Yes (~/.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 reference | Yes (extension can intercept and block) |
Name collisions
Section titled “Name collisions”- 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
beforeReadFileandbeforeMCPExecutionhave 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.ymlis 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.