Every coding agent I've picked up this year has quietly grown its own hooks system. I didn't notice until I tried to port a “block rm -rf” script from Claude Code into Cursor and nothing worked. PreToolUse isn't a thing in Cursor's docs. beforeShellExecution isn't a thing in Claude Code's. I spent twenty minutes assuming I'd typo'd a JSON key before realizing the two tools just don't agree on vocabulary — and neither agrees with Windsurf, or Copilot, or Gemini CLI, or OpenCode.

The problem

Hooks are how you put a deterministic guardrail around a non-deterministic agent — block a dangerous command, auto-format a file after an edit, log every tool call for an audit trail. Every major agent supports this now. But each one invented its own event names, its own config file location, and its own way of saying “block this.” Work across more than one agent, and that's eight vocabularies for the same three ideas.

The three moments every agent has

Strip away the naming and every agent's hook system reduces to three moments:

  1. Pre — right before the agent does something: runs a command, edits a file, calls an MCP tool. Your script inspects it and usually can block it, typically by exiting non-zero or returning "decision": "deny".
  2. Post — right after. Almost always observe-only: log it, lint it, ping Slack. You can't undo what already happened.
  3. Session — the bookends: start, stop, end, sometimes compaction. Fire once per run, not once per tool call.

Wait, where's the commit hook?

This is the part that trips people up, because “commit” implies something git-specific. None of these agents ship a dedicated PreCommit/PostCommit event. As far as the agent is concerned, git commit -m "..." is just another shell command, so it flows through the same pre/post-tool hook as ls or npm test — you catch it by matching on the command text, not a special event name.

Want an actual git-level guarantee, one that holds even when a human commits? You still want a real .git/hooks/pre-commit. Agent hooks and git hooks are complementary, not the same thing.

The command

A Claude Code PreToolUse hook that blocks any git commit unless the last test run passed. It matches on the Bash tool, inspects the command text, and denies with a reason the agent can see:

.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/gate-commit.sh" }
        ]
      }
    ]
  }
}

.claude/hooks/gate-commit.sh

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q '^git commit' && [ ! -f .last-test-pass ]; then
  jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"Run tests before committing"}}'
else
  exit 0
fi

Same idea, eight different ways to do it.

The trigger table

AgentPre-toolPost-toolSession startSession end / stopConfig
Claude CodePreToolUsePostToolUseSessionStartStop / SessionEnd.claude/settings.json
CursorpreToolUsepostToolUsesessionStartstop / sessionEnd.cursor/hooks.json
Windsurf Cascadepre_run_commandpost_run_commandpre_user_promptpost_cascade_response.windsurf/hooks.json
GitHub CopilotpreToolUsepostToolUsesessionStartsessionEnd / agentStop.github/hooks/*.json
Gemini CLIBeforeToolAfterToolSessionStartSessionEndsettings.json
ClinePreToolUsePostToolUseTaskStartTaskCancel.clinerules/hooks/
Codex CLIPreToolUse (shell only)PostToolUse~/.codex/hooks.json
OpenCodetool.execute.beforetool.execute.aftersession.createdsession.idle.opencode/plugin/*.ts

Two things worth flagging: Codex CLI's PreToolUse only fires for the shell tool — apply_patch and MCP calls slide right past it, and hooks are off by default until you set codex_hooks = true in config.toml. And OpenCode is the odd one out structurally, not just lexically: a plugin is a TypeScript module that subscribes to whichever of its 25+ events it wants, closer to an event bus than a hooks file, with no dedicated “stop” event — session.idle is what you reach for instead.

Once I stopped treating this as “learn eight hook systems” and started treating it as “learn three moments, then look up the local dialect,” switching agents got a lot less annoying. The table above is my cheat sheet now — I'll update it as the vocabularies keep drifting, which, given how fast this space moves, they will.

HOW TO : Use hooks in coding agents