# One set of guard hooks for 7 coding agents

> The same guard scripts block rm -rf, DROP TABLE, force pushes and .env reads in Claude Code, Codex, Cursor, Copilot, Gemini CLI and more. Configs and limits.

*Updated 2026-10-08.*

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, `DROP` and `TRUNCATE` sent to a database, force pushes, `git reset --hard`, `git clean`, `dd`, and truncating redirects onto tracked files.
- **`env-leak-detector`:** reads of local `.env` files, `echo` of secret variables, environment dumps, and live values from your `.env` files 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 through `psql`, migrations through the Neon pooler, and schema pushes that skip migration files.
- **`enforce-typecheck`:** a bare `tsc`, which reads the nearest `tsconfig.json` and reports different errors from the build.
- **`auto-lint` and `enforce-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`: `PostToolUse` on `Edit|Write|MultiEdit`
- `block-destructive.ts`: `PreToolUse` on `Bash`
- `enforce-doc-meta.ts`: `PostToolUse` on `Edit|Write|MultiEdit`
- `enforce-typecheck.ts`: `PreToolUse` on `Bash`
- `env-leak-detector-write.ts`: `PostToolUse` on `Edit|Write|MultiEdit|Bash|Read|Grep`
- `env-leak-detector.ts`: `PreToolUse` on `Bash|Read|Grep`
- `guard-neon-sql.ts`: `PreToolUse` on `Bash`

## 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:

```js
#!/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:

```json
{
  "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](/guides/codex-setup-nextjs), [GitHub Copilot](/guides/github-copilot-setup-nextjs), [Gemini CLI and Antigravity](/guides/gemini-cli-antigravity-setup), [Claude Code](/guides/claude-code-hooks).

## 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-typecheck` swaps a bare `tsc` for `bun run typecheck` and 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-detector` on shell commands only. Outside Claude Code, `env-leak-detector-write` checks 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

1. **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.
2. **Register it.** Put the script in `.agents/hooks/` and add an entry to `guards.json`: its event, its tools, its file.
3. **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.
4. **Prove it.** Add its cases to `scripts/verify-hooks.ts`, then run `verify:hooks`.

The long version, with the right and wrong way to write one: [what each guard hook blocks](/cookbook/nextjs-vercel/guard-hooks-and-how-to-extend-them).

## Get the guards in every agent

Pick your stack at [/build](/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](/with/neon)). $99 once, with lifetime updates. How hooks fit with rules, skills and subagents: [the agentic layer](/docs/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

- [Claude Code docs: Hooks reference](https://code.claude.com/docs/en/hooks)
- [Codex docs: Hooks](https://developers.openai.com/codex/hooks)
- [Cursor docs: Hooks](https://cursor.com/docs/hooks)
- [GitHub docs: Copilot hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference)
- [Gemini CLI docs: Hooks](https://geminicli.com/docs/hooks/)
- [Antigravity docs: Hooks](https://antigravity.google/docs/hooks)

## Generate it

[Build your repo](https://agenticboilerplate.com/build)

---

Agentic Boilerplate: A Next.js repo your agent already knows. $99 once. Lifetime access and updates.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
