Hooks that block dangerous commands in Codex CLI, Gemini CLI, Copilot and Cursor

Each of these coding agents runs a hook of yours before a shell command. They differ on one thing that matters: whether the hook can ask you first, or only refuse.

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.

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

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 opens sh -c strings, 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 that psql would run with DROP, TRUNCATE, or an UPDATE or DELETE with no WHERE;
  • 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-out preToolUse hook 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 failClosed false, 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’s PATH; 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

  1. List every agent your team uses, and which ones run unattended (cloud agents, autopilot).
  2. For each, put the hook in the file and event from the table above, merged with existing hooks.
  3. Answer ask only where the agent supports it; answer deny in Codex and Gemini.
  4. Never answer allow. Stay silent on no match.
  5. Split compound commands before you match them.
  6. Test with each agent’s real input shape, including ordinary commands that must pass.
  7. Use an absolute path to the hook binary in GUI-launched editors.
  8. Re-read each agent’s hooks page when you upgrade it. These formats change between releases.2
  9. 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.

The takeaway

Codex, Gemini CLI, Copilot and Cursor all run your hook before a shell command; what differs is whether it can ask you first or only refuse.

Read nextA practical checklist before you let an AI agent near production Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1Deedglass docs: The shell guard · docs.deedglass.com · checked 25 October 2026
  2. 2Deedglass docs: Install in any client · docs.deedglass.com · checked 25 October 2026
  3. 3Codex documentation: Hooks · learn.chatgpt.com · checked 25 October 2026
  4. 4Gemini CLI documentation: Hooks reference · geminicli.com · checked 25 October 2026
  5. 5GitHub Docs: Copilot hooks reference · docs.github.com · checked 25 October 2026
  6. 6Cursor documentation: Hooks · cursor.com · checked 25 October 2026
  7. 7Claude Code documentation: Hooks reference · code.claude.com · checked 25 October 2026
  8. 8VS Code documentation: Hooks reference · code.visualstudio.com · checked 20 October 2026
  9. 9deedglass/deedglass #72: the shell guard in Codex CLI, Gemini CLI, Copilot and Cursor · github.com · checked 25 October 2026
  10. 10anthropics/claude-code #80868 · github.com · checked 25 October 2026

Published 25 October 2026, sources last checked 25 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing