How do you stop a coding agent running destructive commands?

To stop a coding agent running destructive commands, deny them at the hook that the agent fires before each tool call. Arcjet does this with one policy for Claude Code, Copilot, Cursor, and Codex, installed through managed settings and decided before the shell runs anything. This guide shows the starter Rego policies for rm -rf, sudo, force pushes, and CI workflow edits, the spellings to test, and how to roll out in dry run.

12 min read
In short: To stop a coding agent running destructive commands, deny them at the hook that the agent fires before each tool call. Arcjet does this with one policy for Claude Code, Copilot, Cursor, and Codex, installed through managed settings and decided before the shell runs anything. This guide shows the starter Rego policies for rm -rf, sudo, force pushes, and CI workflow edits, the spellings to test, and how to roll out in dry run.

How do you stop a coding agent running destructive commands?

Deny the command at a hook that runs before each tool call. Claude Code, GitHub Copilot, Cursor, and OpenAI Codex each fire a hook – a call to a command or HTTP endpoint that you configure – before a tool runs. A policy that reads the command at that hook can deny it, and the agent's own process then refuses to run the call, whatever the model decided.

Arcjet coding agent hooks do this with one policy for all four agents. The agent posts each pending tool call to Arcjet, Arcjet decides it at the edge in over 300 data centers, and a denied command never reaches the shell. You install the hook through managed settings, so developers can't remove it without administrator access, and you don't change any code.

An instruction in AGENTS.md or CLAUDE.md that says "never run rm -rf" works differently. The model reads it as context and weighs it against everything else in the conversation, including text that an attacker planted in a README or an issue. For more information about that distinction, see AGENTS.md is not a security control.

The commands worth a hard deny are the ones you can't undo: recursive deletes, disk and filesystem tools such as dd and mkfs, privilege escalation with sudo, and rewrites of shared history such as git push --force and git reset --hard. A coding agent runs with the developer's credentials, so a database it deletes or a main branch it force-pushes is really gone or really rewritten. For the wider picture, see coding agent security.

Where can you intercept a command before it runs?

Each agent has a hook that fires before a tool call. In the Arcjet Console, a policy with Execute on set to Tool call runs on the following events:

AgentHook before a tool callHow the hook is installed
Claude Code

PreToolUse, PermissionRequest

HTTP hook in managed settings
GitHub Copilot

preToolUse, permissionRequest

HTTP hook in the CLI and the cloud coding agent
OpenAI Codex

PreToolUse, PermissionRequest

Command wrapper that posts the payload to Arcjet
CursorpreToolUseCommand wrapper that posts the payload to Arcjet

Managed settings are configuration that an administrator deploys to developer machines, usually through mobile device management (MDM), and that a developer can't override. A hook installed there stays in place when a developer edits their own settings.

Arcjet normalizes each payload into a fixed set of inputs, so one policy covers all four agents and you don't maintain per-vendor rules. The following inputs matter for commands:

  • tool_kind is shell for Bash, exec_command, powershell, and Cursor's Shell.
  • command is the full command text.
  • command_tokens is the command split on whitespace and the shell's chaining operators.
  • paths holds the file paths from a file tool's structured arguments.

Compare tool_kind, not tool_name. The agents spell their tools differently, and a rule over tool_name == "Bash" misses Codex and Cursor.

What does a destructive-command policy look like?

Arcjet policies are written in Rego, the policy language of Open Policy Agent, which gives you fine-grained control over each rule. The Arcjet Console includes starter policies that you can publish as they are or adapt. The coding-agent.destructive-command starter policy denies a shell call when any word in the command is on a list:

destructive := {"rm", "rmdir", "shred", "dd", "mkfs", "sudo", "doas", "trash"}
deny contains "destructive-command" if {
input.values.tool_kind == "shell"
some token in input.values.command_tokens
lower(token) in destructive
}

The rule matches on tokens rather than on a substring, so it fires on rm but not on npm run format, which contains the letters rm. Because the tokenizer also splits on chaining operators, ls && rm -rf build produces an rm token and the rule fires.

The coding-agent.rewrite-history starter policy handles Git. It pairs a verb with a flag:

deny contains "force-push" if {
input.values.tool_kind == "shell"
contains(input.values.command, "git push")
some flag in input.values.command_tokens
flag in {"--force", "-f"}
}
deny contains "hard-reset" if {
input.values.tool_kind == "shell"
contains(input.values.command, "git reset")
"--hard" in input.values.command_tokens
}

When a rule fires, Arcjet denies the call before the tool runs, and the agent receives the rule ID and nothing else, for example Blocked by Arcjet policy: destructive-command. The policy's own descriptions never reach the agent.

How do you stop an agent editing CI workflows or Git internals?

Some damage comes from a file write rather than a command. An agent that edits .github/workflows/ changes what runs in CI with your repository's secrets. The coding-agent.protected-paths starter policy denies writes to a list of path markers:

protected := {".github/workflows/", ".git/", "/etc/", "node_modules/"}
deny contains "protected-path-write" if {
input.values.tool_kind == "file_write"
some path in input.values.paths
some marker in protected
contains(path, marker)
}

The rule is scoped to file_write, so the agent can still read those files. Arcjet maps Edit, Write, MultiEdit, apply_patch, create, str_replace_editor, and Cursor's Write and Delete to that kind. The coding-agent.piped-installer starter policy covers a related case: a curl or wget download piped into sh, bash, zsh, or dash.

How do agents get around a command deny list?

A deny list over command text matches the spellings that it names. The destructive-command starter catches chained and re-cased forms of the commands on its list, and because the starters are Rego, you can extend them to the forms that matter in your repositories. Test the following spellings, which the Rego in the preceding sections doesn't match as written:

  • Absolute paths. /bin/rm -rf build produces the token /bin/rm, which isn't in the destructive set. Add path forms or strip a directory prefix in your own rule.
  • Other spellings of the same Git operation. The force-push rule looks for the text git push and the flags --force or -f. It doesn't match --force-with-lease, a +main refspec, or git -C repo push --force, because the text git push doesn't appear in the last one.
  • Case. The destructive rule lowercases each token, so it catches RM. The path and substring rules use contains without lowercasing, so they are case-sensitive.
  • Unlisted tool names. A tool name that Arcjet doesn't recognize is tool_kind: other, and a rule over the kind never matches it.

The tokenizer is designed for chaining: splitting on the shell's chaining operators is what puts the rm in make test; rm -rf . into command_tokens. For each rule that you publish, add stored tests for the chained, re-cased, and absolute-path forms of the command that you meant to deny, and assert the allowed cases too. A stored test input is the contract's values:

{
"values": {
"tool_kind": "shell",
"command": "git push --force origin main",
"command_tokens": ["git", "push", "--force", "origin", "main"]
}
}

Should you use a deny list or an allowlist for shell commands?

A deny list names only what you have thought of. An agent that is denied one command often tries another route to the same result, such as find . -delete instead of rm. An allowlist, a list of the only values that a rule permits, denies anything that you haven't named, which makes it closer to a hard boundary.

For shell commands, an allowlist is harder to write well. A rule that checks only the first token allows ls && rm -rf ., so an allowlist has to check every program in a chained command. It also denies a lot of legitimate work until you tune it. A practical split is to use allowlists where the set of legitimate values is small and stable, such as Model Context Protocol (MCP) servers and outbound hosts, and to use deny lists plus stored tests for shell commands. For the allowlist patterns, see restrict MCP servers and egress allowlists for AI agents.

How do you roll out a command policy without breaking developers?

Start in dry run, a mode in which Arcjet records what a rule would have denied without denying it. In Arcjet, every new rule starts in dry run, so you see what a rule would deny against real work before it goes live. Roll out in the following order:

  1. Publish the policy with every rule in dry run.
  2. Let developers work, then open Activity in the Arcjet Console and read the tool calls that each dry-run rule would have denied.
  3. Set the rule live in a second edit. The change takes effect in real time, with no redeploy to developer machines.
  4. Watch the denial reasons that developers see. Give rules IDs that a developer can understand, so they know which rule stopped them and why.

Arcjet runs every policy set to Tool call in one round trip and applies the most restrictive decision. Every session and decision is recorded in the Arcjet Console, and you can export decisions to Datadog, Splunk, SentinelOne, Panther, and Amazon S3 (Enterprise plan) for detection and alerting. Keep each policy to one concern, so that you can take one live without waiting on the others. For the full starter catalog and the input contract, see the coding agent policies documentation.

What do you pair with a command policy?

Arcjet decides each tool call that the agent's hook sends. The following platform behaviors shape what the hook sees, and each one has a control to pair with it:

  • Claude Code and Copilot HTTP hooks fail open. These vendors let the tool call go ahead on a timeout, a network error, or a non-2xx response. Arcjet evaluates at the edge to keep the added latency small, and for Codex and Cursor, the command wrapper fails closed: it denies the call on any transport failure. For more information, see coding agent hooks fail open.
  • Codex hosted tools skip PreToolUse. Codex runs WebSearch and other hosted tools outside its local hook path, so those calls don't reach the hook.
  • Scripts hide their steps. The hook sees the command that the agent asks to run, such as npm run clean or make reset, not the steps inside the script. Review those scripts as code, as you do the rest of the repository.
  • A hook covers one client. A developer who calls a model from another tool is outside it. Install hooks through managed settings, and use OpenTelemetry and Claude Compliance API activity to see sessions that the hooks don't reach. For more information, see observe agent activity.

Keep backups, branch protection on main, and scoped database credentials alongside the policy. Arcjet stops the agent from running the destructive action in the first place, and those controls limit the damage from anything that happens outside the agent.

How does Arcjet block destructive commands across coding agents?

With Arcjet, a coding agent that tries rm -rf, sudo, or git push --force gets a denial before the shell runs anything, on Claude Code, Copilot, Cursor, and Codex alike. The following capabilities apply:

  • One policy over normalized inputs such as tool_kind and command_tokens, decided from the hooks that the agents already fire, with no code change.
  • Hooks installed through managed settings, so developers can't remove them without administrator access.
  • Starter policies for recursive deletes, privilege escalation, history rewrites, protected paths, and piped installers, each starting in dry run.
  • A record of every session and decision in the Arcjet Console, with export to your security tooling.

The same policy engine protects custom agents that you build, so coding agents and custom agents share one approach. For that side, see prevent irreversible AI agent actions and block Bash in the Claude Agent SDK.

To start, publish coding-agent.destructive-command and coding-agent.rewrite-history in dry run, and review their would-be denials on the Activity page in the Arcjet Console. For setup, see the coding agent policies documentation.

Frequently asked questions

How do I stop Claude Code or Cursor from running rm -rf?

Deny it at the hook that the agent fires before each tool call. The Arcjet destructive-command starter policy reads the command's tokens and denies rm, dd, mkfs, and sudo before the shell runs them, and the agent's own process enforces the denial. An instruction in CLAUDE.md or AGENTS.md is advice to the model, not a block.

Can a coding agent get around a blocked command?

A deny list over command text matches the spellings that it names, so test the ones it doesn't, such as /bin/rm, git push --force-with-lease, or a destructive step inside an npm script. Extend the Rego to cover the forms that matter, add stored tests for them, and use allowlists where the set of legitimate values is small.

Does blocking git push --force need a separate policy for each agent?

No. Arcjet normalizes Claude Code, GitHub Copilot, Cursor, and OpenAI Codex tool calls into one set of inputs, so one rule over tool_kind and command_tokens covers all four agents.

Will a command policy break developers' work?

Not if you start in dry run, a mode in which Arcjet records the calls that each rule would have denied without denying them. You can tune the rule in the Arcjet Console before you set it live.

AI runtime security in your code

Protect your AI agent workflows with Arcjet

Start from the destructive-command and rewrite-history starter policies and publish them in dry run.