The short answer
To stop Codex CLI, Gemini CLI, GitHub Copilot or Cursor running a destructive shell command, register a hook that runs before each command and answers deny when it matches. Codex uses PreToolUse on Bash, Gemini CLI BeforeTool on run_shell_command, Copilot preToolUse, Cursor beforeShellExecution. Only Copilot and Cursor can also ask you first; Codex and Gemini can only refuse.
That last difference decides how you write the hook. The rest of this post is the pattern, where each file goes, what each agent does with the answer, and what no hook can catch. The examples use our open-source shell guard because its output is real, but the pattern works with any script.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
Write a hook that reads the whole command
Every one of these agents hands the hook a JSON object on stdin and reads a JSON answer on stdout. The command sits in a different place in each: tool_input.command in Codex and Gemini, command in Cursor, and inside toolArgs, a JSON string, in Copilot.9
The hard part isn’t the plumbing. It’s that agents rarely run one clean command. The command from claude-code #80868 chains a cd, a variable, a mkdir and a prisma migrate diff whose --shadow-database-url comes from a grep of .env.1 A hook that checks only the first word sees cd and lets it through.
So a useful hook:
- splits the command on
&&,||,;and pipes, and openssh -cstrings, subshells and heredocs; - compares each part with a short list of commands that destroy data, each one sourced from that tool’s own docs:
prisma migrate reset,dropdb,rails db:drop, and SQL thatpsqlwould run withDROP,TRUNCATE, or anUPDATEorDELETEwith noWHERE; - never answers allow. When nothing matches it stays silent, so the agent’s own permission rules still decide.1
The last rule matters more than it looks. In Claude Code, a hook that exits 0 with no output has no decision, and the call goes on to the normal permission flow.7 A hook that answers allow would skip that flow, which turns a safety check into a hole.
Put it where each agent looks
| Agent | File | Event and match |
|---|---|---|
| Codex CLI | .codex/hooks.json or ~/.codex/hooks.json |
PreToolUse, matcher ^Bash$ |
| Gemini CLI | .gemini/settings.json or ~/.gemini/settings.json |
BeforeTool, matcher ^run_shell_command$ |
| GitHub Copilot | .github/hooks/*.json, or ~/.copilot/hooks/ for the CLI |
preToolUse, tool bash |
| Cursor | .cursor/hooks.json or ~/.cursor/hooks.json |
beforeShellExecution |
Codex runs a non-managed hook only after you review and trust it with /hooks, and loads a project’s .codex/ hooks only when the project is trusted.3 Gemini CLI defines hooks in the hooks object of settings.json.4 Copilot’s cloud agent reads only the repository’s .github/hooks/.5 Cursor runs project hooks in any trusted workspace.6
Here is the Codex block, printed by the guard (it writes no file; merge it with any hooks you already have):
Code What the screen or file shows, copied as is.
$ deedglass guard shell --print-config --client codex
{
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{
"type": "command",
"command": "deedglass guard shell --client codex",
"timeout": 10,
"statusMessage": "Deedglass shell guard"
}
]
}
]
}
}
Codex CLI goes in .codex/hooks.json (or ~/.codex/hooks.json; the same keys work as [[hooks.PreToolUse]] in config.toml). Merge it with any hooks already there.
The Gemini, Copilot and Cursor blocks are in the install guide.2
Decide ask or deny per agent
Ask is the better answer for a command that’s sometimes intended: a reset on a scratch database is fine, on production it isn’t, and only you know which. But ask only helps where the agent supports it.
| Agent | Can a hook ask? | What happens with ask | What our guard answers on a match |
|---|---|---|---|
| Claude Code | Yes | You get a prompt with the reason | ask |
| Copilot CLI (interactive) | Yes | You get a prompt | ask |
| Copilot cloud agent, or CLI in autopilot | No one to ask | Treated as deny | ask, which becomes deny |
| Cursor | Yes | You get a prompt | ask |
| Codex CLI | Not yet | Hook marked failed; the command runs | deny |
| Gemini CLI | No | allow and deny only | deny |
Sources for the agent columns: Codex parses ask but doesn’t support it yet, marks the hook failed and runs the command3; Gemini CLI’s decisions are allow and deny, with block an alias4; Copilot’s cloud agent and CLI autopilot treat ask as deny.5
The Codex row is the trap. A hook written for Claude Code that answers ask will, under Codex today, let the dangerous command run. So for Codex and Gemini the guard answers deny, and its reason tells the agent to hand the command to you. On the #80868 command it answers ask in Claude Code, Copilot and Cursor, and deny in Codex and Gemini.9 That comes from the agents’ docs and from tests on their documented input shapes, not from a run inside each agent.9
Test it before you trust it
Feed the hook the JSON the agent would send, and read what comes back. This is the guard on v0.9.0, as Codex would call it:
Code What the screen or file shows, copied as is.
$ echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"psql \"$DATABASE_URL\" -c \"DROP TABLE orders\""}}' \
| deedglass guard shell --client codex
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Deedglass denied this command because psql would run DROP TABLE, which deletes the table and every row in it; if it is intended, ask the user to run it themselves."}}
As Cursor would call it, with a Prisma reset:
Code What the screen or file shows, copied as is.
$ echo '{"command":"npx prisma migrate reset --force","cwd":"/app","hook_event_name":"beforeShellExecution"}' \
| deedglass guard shell --client cursor
{"permission":"ask","user_message":"Deedglass asks first: prisma migrate reset drops the database, or every table in it, and applies your migrations again, so its data is gone.","agent_message":"Deedglass asks first: prisma migrate reset drops the database, or every table in it, and applies your migrations again, so its data is gone."}
And the ordinary case, which must stay quiet. For Codex, npm test gives no output at all. Gemini CLI parses stdout as JSON, so there the guard writes {}:
Code What the screen or file shows, copied as is.
$ echo '{"hook_event_name":"BeforeTool","tool_name":"run_shell_command","tool_input":{"command":"ls -la"}}' \
| deedglass guard shell --client gemini
{}
Run a handful of your team’s everyday commands through your hook the same way. A hook that asks about git push will be switched off within a day.
Know what a hook can’t see
A before-command hook is a second check, not a sandbox. Be clear about its gaps:
- It reads text. A value known only at run time passes untouched: a variable, an alias, a Makefile target, encoded text, code an interpreter runs, a database reached over HTTP.1
- It fails open. If the hook can’t run (no binary on the agent’s
PATH, a timeout), the command goes ahead unchecked.1 Copilot treats a timed-outpreToolUsehook as fail-open, but denies the call when the hook exits with anything other than 0 or 2.55 That’s why the Copilot snippet ends in|| true: a missing binary would otherwise block every command. - Cursor’s failure mode is yours to set. With
failClosedfalse, its default, a hook that crashes or times out lets the action through.6 Cursor started from the macOS Dock may not have your shell’sPATH; use an absolute path to the binary. - VS Code has no hook here. Its hooks docs say tool names and input schemas differ between harnesses, so there’s no named terminal tool to match. For VS Code, put the database tool behind a gateway instead.8
- Shell only. A hook on the shell tool doesn’t see an MCP tool call to your database. For that you need rules on the tool calls themselves; see require approval for MCP tool calls.
A checklist
- List every agent your team uses, and which ones run unattended (cloud agents, autopilot).
- For each, put the hook in the file and event from the table above, merged with existing hooks.
- Answer ask only where the agent supports it; answer deny in Codex and Gemini.
- Never answer allow. Stay silent on no match.
- Split compound commands before you match them.
- Test with each agent’s real input shape, including ordinary commands that must pass.
- Use an absolute path to the hook binary in GUI-launched editors.
- Re-read each agent’s hooks page when you upgrade it. These formats change between releases.2
- Remember the hook is the second line. The first is that the agent’s database credentials can’t do the damage at all: see safe Postgres access for AI agents and the pre-production checklist.
Try it. The guard is one binary with no server, account or network.1 With deedglass on your PATH, run deedglass guard shell --print-config --client codex (or gemini, copilot, cursor) and paste the block where the table says. If you also want each database write checked after it runs, the Claude Code and Cursor guide covers that, and the PreToolUse glossary entry has the short version of this post. To see the write checks on a real Postgres, open the playground, or run npx deedglass scan --postgres-dsn-env DATABASE_URL to grade what your agent can reach (the grade, explained).
Written with AI assistance and checked against our claims register.