Try “Claude Code skills”, “MCP servers for Cursor” or “Codex” · Esc to close

Agent Hooks Explained: Running Code When Your Agent Acts

By Kelvin · 28 September 2026 · Updated 28 Sep 2026 · 8 min read

hooks claude-code cursor automation security

Agent Hooks Explained: Running Code When Your Agent Acts
Photo by Tima Miroshnichenko on Pexels

Agent Hooks Explained: Running Code When Your Agent Acts

Dark room with multiple monitors displaying code, evoking automated scripts running unattended
Photo by Tima Miroshnichenko on Pexels

Your agent just ran npm test in a project that uses pnpm. It formatted a file with the wrong linter config. It ran rm -rf build when you only wanted build/tmp cleared. None of this is a hallucination in the usual sense — the agent read your instructions correctly, it just doesn't share the muscle memory you built up over years of working in this specific repository. You can fix each mistake by correcting the agent in chat, but that fix lives only in that conversation. The next session, or the next teammate who runs the same agent, hits the same wall.

Hooks exist to close that gap without rewriting your prompt every time. A hook is your own script, wired to a specific moment in the agent's lifecycle, that runs automatically — no matter who is driving the agent or what they typed. It's the difference between asking an assistant nicely and installing a guardrail.

What a hook actually is

Strip away the vendor-specific JSON and every hook implementation has the same three parts:

  1. An event — a name for the moment in the agent's turn when the hook fires: before a tool runs, after a file changes, when a session starts, when the agent thinks it's done.
  2. A matcher — an optional filter so the hook only fires for the right tool, file pattern, or command, instead of on every single event.
  3. A command — the script, binary, or HTTP call that actually runs, plus a way for it to report back a decision.

The reporting-back part is what separates a hook from a plain shell alias. A hook can inspect what the agent is about to do — the exact command, the file path, the diff — and then allow it, block it, or hand back a message the agent has to read before continuing. That loop (agent proposes an action → your script inspects it → your script decides) is what turns "please don't do X" from a suggestion into an enforced rule.

Two apps in this directory document hook support in enough depth to compare directly: Claude Code and Cursor. Both converged on nearly the same shape independently, which is a reasonable signal that this is close to the "right" design for the problem, not just one vendor's preference.

Claude Code: settings.json and exit codes

Claude Code hooks live in settings.json, at the user level (~/.claude/settings.json), the project level (.claude/settings.json, meant to be committed and shared), a project-local gitignored file, or bundled inside a plugin. The structure nests an event name, a matcher, and a list of handlers:

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

The event catalog covers the full turn: SessionStart and SessionEnd per session; UserPromptSubmit, Stop and StopFailure per turn; PreToolUse, PostToolUse, PostToolUseFailure and PermissionRequest per tool call; plus narrower events like FileChanged, SubagentStart, SubagentStop, TaskCreated and TaskCompleted.

A command hook receives a JSON object on stdin describing the moment — for PreToolUse that includes tool_name, tool_input (the actual command or file edit being proposed), the session id, and the current working directory. The hook talks back through its exit code: 0 means proceed, and any JSON printed to stdout is read as a structured decision; 2 is a hard block — for PreToolUse it stops the tool call outright, for UserPromptSubmit it rejects the prompt, for Stop it keeps the agent going instead of letting it end the turn. A hook that wants to actively deny an action rather than merely inspect it prints something like:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook"
  }
}

Handlers aren't limited to shell commands, either — the same configuration shape supports HTTP endpoints, MCP tool calls, prompts sent back to Claude for a judgment call, and even spawning a subagent to evaluate something more involved than a regex can handle.

Cursor: hooks.json and the same idea, different verbs

Cursor's implementation lives in hooks.json rather than its general settings file, at the enterprise, team, project (.cursor/hooks.json) or user (~/.cursor/hooks.json) level, in that priority order. The shape is close enough to Claude Code's that switching mental models between the two takes minutes, not hours:

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      { "command": "./hooks/audit.sh" }
    ],
    "afterFileEdit": [
      { "command": "./hooks/format.sh" }
    ]
  }
}

Where Claude Code has four or five tool-call events, Cursor names more than a dozen: lifecycle events (sessionStart, sessionEnd), tool-use events (preToolUse, postToolUse, postToolUseFailure), subagent events (subagentStart, subagentStop), shell- and file-specific events (beforeShellExecution, afterShellExecution, beforeReadFile, afterFileEdit), MCP-specific events (beforeMCPExecution, afterMCPExecution), and a few others (beforeSubmitPrompt, preCompact, stop). Each hook definition can set a matcher regex, a timeout, and a failClosed flag that decides whether a crashing hook script should block the action it was supposed to review (the default is to fail open, i.e. let the action through). Cursor also supports a "prompt" type alongside "command", which routes the decision through an LLM judgment call instead of a script.

What people actually build with them

The use cases cluster into a handful of recurring patterns:

  • Formatting on save — an afterFileEdit or PostToolUse hook that runs prettier or black on whatever file the agent just touched, so style stays consistent regardless of which model wrote the code.
  • Blocking destructive commands — a PreToolUse/beforeShellExecution hook that greps the proposed shell command for rm -rf, git push --force, or DROP TABLE and denies it before it runs.
  • Enforcing package manager conventions — a hook that checks the command against package.json's packageManager field and rejects an npm install in a pnpm repo instead of relying on the agent to notice a lockfile.
  • Audit logging — a hook on every tool call that appends a line to a log file, giving a team a record of what an agent actually did in a session, independent of the chat transcript.
  • Desktop notifications — a Stop hook that fires a system notification when a long agent turn finishes, so you don't have to keep a terminal tab in view.

None of these require the agent's cooperation. That's the point: a prompt instruction is a request the model might forget under context pressure; a hook runs every time, deterministically, in your own code.

Complex industrial machinery with interlocking parts, evoking a chain of automated triggers
Photo by Yetkin Ağaç on Pexels

Who else documents hook support

Beyond Claude Code and Cursor, hooks show up as a documented capability on several other apps in the ecosystem overview, including Cline, Kiro, Devin Desktop (formerly Windsurf), Factory, Google Antigravity and Gemini CLI. Configuration file names and event lists differ per app, so treat any specific event name or JSON field in this piece as Claude Code- or Cursor-specific unless you've checked that app's own documentation — the concept transfers, the exact syntax does not.

AppHooks documentedConfig lives in
Claude CodeYessettings.json (user, project, or plugin-scoped)
CursorYeshooks.json (enterprise, team, project, or user)
ClineYesCheck Cline's own docs for current file/event names
KiroYesCheck Kiro's own docs for current file/event names
Devin Desktop (Windsurf)YesCheck Devin's own docs for current file/event names
FactoryYesCheck Factory's own docs for current file/event names

The safety case for treating hooks carefully

A hook that can block a destructive command can also become the destructive thing itself, and this is where hooks stop being a pure convenience and start being a real part of your threat model. A command hook runs with your own shell permissions, not some sandboxed subset — if you paste in a hook script from a blog post or a shared plugin without reading it, you've granted it the same access to your filesystem, credentials and network that you have. Project-level hook configuration files are usually meant to be committed and shared with a team, which means anyone who can open a pull request against your repository can potentially add or modify a hook that will silently execute on every teammate's machine the next time they run the agent there — the same review discipline you'd apply to a new CI step or a postinstall script belongs here too.

Two settings are worth knowing before you turn hooks on for a shared project. Claude Code's plugin-distributed hooks run automatically as soon as a plugin is enabled, with no separate opt-in step, so only install plugins from sources you trust. Cursor's failClosed flag defaults to false, meaning a hook script that errors out fails open and lets the underlying action through — flip it to true for anything security-critical, like the destructive-command blocker, where a silent script crash should not silently remove your safety net.

A padlock securing a metal gate with a chain, representing the safety review hooks deserve before running
Photo by Larkin Hammond on Pexels

Getting started without overbuilding

You don't need a hooks framework on day one. A reasonable on-ramp:

  1. Start with one PostToolUse/afterFileEdit hook that runs your formatter after every edit — low risk, immediately visible payoff.
  2. Add one PreToolUse/beforeShellExecution hook that blocks the one destructive command pattern that has actually bitten you or a teammate before (force-pushes, rm -rf, dropping a table).
  3. Only then consider audit logging or notification hooks — useful, but not the reason hooks exist.
  4. Commit project-scoped hook configs to the repository so the whole team gets the same guardrails, and review any hook someone else adds the same way you'd review a new script in package.json.

The takeaway

A hook is not a smarter prompt — it's code that runs whether or not the agent remembers to ask. If you've corrected the same agent mistake more than twice, that's the signal it's worth turning into a hook instead of a reminder. Start with formatting and one destructive-command guard, treat any hook script — yours or a teammate's — with the same scrutiny as a CI step that runs on every push, and check the deals page and glossary if you're still deciding which app's ecosystem to build this habit in.

Mentioned in this post

More from the blog

02 Oct 2026 · 8 min read

A Vibe Coding Stack for a DevOps Engineer

Claude Code, a DevOps skill pack, a scanning layer, an agent dashboard, and three scoped MCP servers for errors, clusters and tickets.

devops claude-code mcp-servers

Building your vibe coding stack?

Browse 296 apps, skills, subagents and MCP servers, mapped to the apps they work with.

See the ecosystem map