A rule asks. A hook blocks. Most coding agents run hooks now: scripts that fire before or after a tool call, outside the model. The model can't skip one or talk its way past it. But each agent has its own config file, its own event names and its own way to say no.
A generated repo writes each guard once and wires it into 7 agents: Claude Code, Codex, Cursor, GitHub Copilot, Antigravity, Gemini CLI and Windsurf. Below: what the guards block, how one script serves every agent, each agent's config, and the limits.
What the guards block
block-destructive: recursive force deletes,DROPandTRUNCATEsent to a database, force pushes,git reset --hard,git clean,dd, and truncating redirects onto tracked files.env-leak-detector: reads of local.envfiles,echoof secret variables, environment dumps, and live values from your.envfiles on a command line.env-leak-detector-write: secrets written into files, private env vars read in client components, and secrets passed to log calls.guard-neon-sql(Neon only): raw DDL throughpsql, migrations through the Neon pooler, and schema pushes that skip migration files.enforce-typecheck: a baretsc, which reads the nearesttsconfig.jsonand reports different errors from the build.auto-lintandenforce-doc-meta: after an edit, Biome on the changed file, and a frontmatter check on solution docs and plans.
Team mode adds two more: enforce-git-tracked and plan-gate. The scripts in the example repo, with the event and tools each one watches, in Claude Code's terms:
auto-lint.ts:PostToolUseonEdit|Write|MultiEditblock-destructive.ts:PreToolUseonBashenforce-doc-meta.ts:PostToolUseonEdit|Write|MultiEditenforce-typecheck.ts:PreToolUseonBashenv-leak-detector-write.ts:PostToolUseonEdit|Write|MultiEdit|Bash|Read|Grepenv-leak-detector.ts:PreToolUseonBash|Read|Grepguard-neon-sql.ts:PreToolUseonBash
One script, every agent: the launcher
All guard code lives in .agents/hooks/:
<guard>.ts: one script per guard, written in Claude Code's hook format. JSON on stdin, exit 2 to block.run.mjs: the launcher every agent's config calls.guards.json: which event and tools each guard watches.README.md: which agents are wired, and what works where.
Every config runs the same line: bun .agents/hooks/run.mjs <guard> --from <agent>. The launcher explains itself at the top:
#!/usr/bin/env node
/**
* run.mjs: runs one guard from .agents/hooks/ for whichever agent called it.
*
* Every agent's hook config runs this file with a guard's name and the agent
* the config belongs to:
*
* node .agents/hooks/run.mjs block-destructive --from cursor
*
* The guards speak Claude Code's hook format: a JSON payload on stdin, exit 2
* to block. This file reads the calling agent's payload, rewrites it in that
* format, runs the guard, and answers in the calling agent's own format.
*
* Two rules it never breaks:
*
* - It never blocks because of its own failure. A missing runner, a guard
* that crashes or a payload it cannot read lets the call through, with a
* warning on stderr. Copilot denies a tool call when a hook exits with
* anything but 0 or 2, so a broken guard must not lock the agent out.
* - A guard runs once per call. Cursor, Copilot and Devin also run Claude
* Code's hooks from .claude/settings.json. When the agent running this file
* has its own config in the repo, a run from another agent's config steps
* aside and lets the agent's own config do the work.
*
* Which guards exist, and which event and tools each one watches, is in
* guards.json next to this file. The generator writes both.
*/
First 27 of 552 lines of .agents/hooks/run.mjs.
So a fix to a guard lands in every agent at once. Each agent's config stays small: its event names, its tool names, one command per guard.
Each agent's hook config
| Agent | Config file | Before a shell command | How a block comes back |
|---|---|---|---|
| Claude Code | .claude/settings.json | PreToolUse on Bash | Exit 2 with the reason on stderr, or JSON with permissionDecision: "deny" |
| Codex | .codex/hooks.json | PreToolUse on Bash | Exit 2, reason on stderr |
| Cursor | .cursor/hooks.json | beforeShellExecution | JSON with permission: "deny" |
| GitHub Copilot | .github/hooks/agentic-guards.json | preToolUse on bash and powershell | JSON with permissionDecision: "deny" |
| Antigravity | .agents/hooks.json | PreToolUse on run_command | JSON with decision: "deny" |
| Gemini CLI | .gemini/settings.json | BeforeTool on run_shell_command | Exit 2, reason on stderr |
| Windsurf (Devin Local) | .devin/hooks.v1.json | PreToolUse on exec | Exit 2, reason on stderr |
Here is Cursor's, as the generator writes it. It runs a guard before each shell command and each file read:
{
"version": 1,
"hooks": {
"beforeReadFile": [
{
"command": "bun .agents/hooks/run.mjs env-leak-detector --from cursor",
"timeout": 60
}
],
"beforeShellExecution": [
{
"command": "bun .agents/hooks/run.mjs block-destructive --from cursor",
"timeout": 60
},
{
"command": "bun .agents/hooks/run.mjs enforce-typecheck --from cursor",
"timeout": 60
},
{
"command": "bun .agents/hooks/run.mjs env-leak-detector --from cursor",
"timeout": 60
},
{
"command": "bun .agents/hooks/run.mjs guard-neon-sql --from cursor",
"timeout": 60
}
],
First 27 of 53 lines of .cursor/hooks.json.
The other agents' files, each quoted and explained: Codex, GitHub Copilot, Gemini CLI and Antigravity, Claude Code.
Agents that read Claude Code's hooks too
Cursor (by default), GitHub Copilot (in the CLI and VS Code) and Windsurf's Devin Local also run the hooks in .claude/settings.json. Left alone, every guard would run twice in those agents.
The launcher reads the payload to see which agent really called it. If that agent has its own config in the repo, a call that came through another agent's config steps aside. One run per guard.
The limits
Be honest about what a shared guard can and can't do:
- Rewrites become blocks outside Claude Code. In Claude Code,
enforce-typecheckswaps a baretscforbun run typecheckand lets it run. Elsewhere the launcher blocks the call and names the command to run instead. The agent runs it on its next turn. - Redaction is Claude Code only. Claude Code lets a hook replace what the agent sees after a command. The other agents don't, so there the check before the command is what stops a leak.
- Some guards watch fewer tools. Codex runs
env-leak-detectoron shell commands only. Outside Claude Code,env-leak-detector-writechecks edits only. - Trust comes first. Codex, Cursor and Gemini CLI run a repo's hooks only in a folder you trust. Codex also asks you to review each hook once in
/hooks. - Fail open, on purpose. A guard that crashes, or a repo with no dependencies installed yet, lets the call through with a warning. In Copilot, any non-zero exit from a pre-tool hook denies the call, a crash included, so a launcher that failed closed would lock you out of every tool.
- Three agents get no hooks. OpenCode runs hooks as plugins, and its plugin API changed between v1 and v2. Junie, in JetBrains, reads hooks from user config only. Kiro doesn't document its tool names. The rules in AGENTS.md still apply there.
Prove it: verify:hooks
Every generated repo ships bun run verify:hooks. It feeds each guard the actions it must block and a control it must allow, through the launcher, in Claude Code's format. Then it replays the same probes in each wired agent's own payload format. On the Indie SaaS preset, 374 probes pass. The probes are strings inside a JSON payload, so nothing destructive ever runs.
We also checked two agents live. Codex CLI 0.161 refused rm -rf ./x through .codex/hooks.json with "blocked by the block-destructive hook". Claude Code 2.1.285 blocked rm -rf in a generated repo too.
Run it after any change under .agents/hooks/ or to an agent's hook config. A guard that stopped working is worse than none, because you keep trusting it.
Add your own guard
- Write it once, in Claude Code's format. Read the JSON payload on stdin. Exit 2 with a reason on stderr to block. Exit 0 to allow.
- Register it. Put the script in
.agents/hooks/and add an entry toguards.json: its event, its tools, its file. - Wire it per agent. Add one entry to each hook config your repo has, in that agent's event and tool names. Copy an existing guard's entry and change the id.
- Prove it. Add its cases to
scripts/verify-hooks.ts, then runverify:hooks.
The long version, with the right and wrong way to write one: what each guard hook blocks.
Get the guards in every agent
Pick your stack at /build. All 10 agents are on by default, and every one that runs hooks gets its config wired to the same guards. Neon adds guard-neon-sql (Neon battery). $99 once, with lifetime updates. How hooks fit with rules, skills and subagents: the agentic layer.
FAQ
Does Codex support hooks?
Yes. Codex runs project hooks from .codex/hooks.json in a trusted project, after you review each one in /hooks. A PreToolUse hook that exits 2 blocks the tool call, the same way Claude Code's do.
Does Cursor support hooks?
Yes. Project hooks go in .cursor/hooks.json, on events like beforeShellExecution, beforeReadFile, preToolUse and postToolUse. A hook can deny a command by answering with permission: "deny". Cursor also loads Claude Code hooks from .claude/settings.json by default.
Do GitHub Copilot hooks block commands?
Yes. A preToolUse hook in .github/hooks/*.json can deny a tool call with permissionDecision: "deny". Any non-zero exit denies it too, so a broken hook blocks everything.
Does Gemini CLI have hooks?
Yes. They live in .gemini/settings.json under hooks. A BeforeTool hook that exits 2 blocks the tool call. Gemini CLI runs project hooks only in a trusted folder.
Can one hook script work in every agent?
Yes, with a launcher in between. Write the guard once, in one format, and give each agent a config entry that calls the launcher. The launcher turns each agent's payload into that format and answers in the agent's own.
Which agents can't run the guards?
OpenCode, JetBrains AI and Kiro. OpenCode's plugin API changed between v1 and v2, Junie reads hooks from user config only, and Kiro doesn't document its tool names. In those agents, the rules in AGENTS.md are all you have.
Sources
Tool behavior is from the official docs, checked on October 8, 2026. Tools change. The docs win.
Keep reading
- CLAUDE.md template for Next.js, with real examplesA CLAUDE.md template for Next.js you can copy, where the file goes, what belongs in it, and how a repo shared with other agents loads AGENTS.md instead.
- AGENTS.md examples, from a real Next.js repoWhat AGENTS.md is, a template to copy, real root and nested files from a generated Next.js repo, and how Codex, Cursor, Copilot and Claude Code load them.
- CLAUDE.md vs AGENTS.md: which one you needCLAUDE.md is Claude Code's file. AGENTS.md is the one Codex, Cursor, Copilot and most other agents read. Who reads what, and how to keep both in sync.
- Cursor rules for Next.js, with real .mdc examplesHow Cursor rules work: the .mdc format, globs, alwaysApply and the four rule types, when a nested AGENTS.md is enough, and real Next.js rules to copy.