← Back to Writing

Dangerous Command Detection for AI Agents: What Claude Code Got Right

Claude Code showed how useful pre-execution dangerous-command checks can be before shell access. An open-source framework just adopted the same approach. The lesson: agent safety needs pattern recognition at the command level, not just sandboxes.

Dangerous Command Detection for AI Agents: What Claude Code Got Right

On May 16, an open-source agent framework merged a pull request titled "Inspired by Claude Code: tighten dangerous-command detection." The PR closed three bypasses that let an agent slip destructive shell commands past the safety checker. A macOS symlink trick, a killall signal flag, and a find -execdir pattern. All three evaded the existing deny list. All three now blocked.

The PR description says it plainly: "inspired by Claude Code's expanded deny rules." Not inspired by a security paper. Not inspired by a sandbox product. Inspired by another agent framework that ships the same feature.

That is worth paying attention to. When two independent agent frameworks converge on pattern-based command detection, it stops being a feature. It becomes a layer you either have or you do not. Unlike a full sandbox, a pre-execution command check is cheap to run. In simple runtimes it can be no more than normalization plus compiled pattern checks before subprocess launch. The same cannot be said of the recovery cost when an uncaught destructive command lands.

What Claude Code gets right

Claude Code splits terminal commands into two categories. Safe commands like echo, pwd, and whoami usually run without asking in default modes. Restricted commands require explicit approval: rm, dd, sudo, kill. Before every shell execution, Claude Code runs the command through a pattern matcher that checks for destructive operations.

This is not a sandbox. A sandbox restricts what a process can do after it starts. Pattern detection stops the command from running at all. The difference matters because sandboxes have seams. A sandbox that allows rm inside /tmp does not help when the agent writes to /etc/sudoers through a symlink path the sandbox does not know about.

In practice this layer usually classifies commands into allow, ask, or deny outcomes; the exact behavior depends on the agent runtime and the operator's approval mode. The important boundary is that classification happens before shell execution, while sandbox enforcement limits damage after execution starts.

Claude Code's approach does not rely on filesystem isolation. It reads the command string and asks: does this look like something that should not run? If yes, block it before it touches a process.

The three bypasses that got closed

The merged pull request is a salvage of an earlier attempt that sat in review for months. The author cherry-picked it forward through merge conflicts and landed it with a clean design. The three bypasses tell a clear story about why deny lists drift. The examples below illustrate the bypass shapes; do not run them.

macOS /private/ paths. On macOS, system paths such as /etc, /var, and /tmp are exposed through /private/... aliases; the detector also accounts for /private/home where applicable. The framework had patterns blocking writes to /etc/. But echo ... > /private/etc/<system-file> worked identically and sailed past the check. The fix extracts a shared _SYSTEM_CONFIG_PATH fragment so /etc/ and /private/etc/ stay in sync across all six patterns that write into system-config paths. One-line edit now, not six separate regex updates.

killall with SIGKILL and regex. killall was already blocked. killall -KILL <process-name> was not. Neither was killall -r '<regex>'. The -KILL flag sends SIGKILL, which cannot be caught or ignored by the target process. The -r flag lets killall match processes by regex pattern. Both bypass the plain killall check.

find -execdir rm. find <tree> -execdir rm <target> \; removes files from every directory in a tree. find itself is not blocked; it is a read-only traversal tool. The -execdir flag makes it execute destructive commands in each directory it visits. The pattern is obscure enough that a simple deny list misses it.

Each bypass is a reminder that regex deny lists are not configuration. They are code, and code drifts. The shared _SYSTEM_CONFIG_PATH fragment is the right fix: define the protected set once, reference it everywhere. Less surface for drift, less surface for bypass.

Sandboxes are not enough

The operator instinct, when you first give an agent shell access, is to Docker it. Put the agent in a container, mount only the directories it needs, limit network access, run as a non-root user. That is good. Do it.

But a sandbox is a perimeter. It does not understand commands. A sandbox that gives the agent write access to /home/agent/workspace will let it delete every file in its own workspace. If your agent runs in a container that has access to your repo, the sandbox will not stop git push --force origin main. If the container shares a volume with your vault, the sandbox will not stop rm -rf /vault/.

Sandboxes reduce blast radius. They do not eliminate it. Pattern detection at the command level catches operations that are legal inside the sandbox but destructive to the system.

The two layers work together. The sandbox constrains what is possible. The pattern matcher constrains what is attempted. If either fails, the other catches it.

The threat model is not just a clumsy model typing rm; it is also prompt injection through files, issues, webpages, logs, or tool output that induces the agent to delete data, alter credentials, rewrite git history, exfiltrate secrets, or weaken future shell sessions.

Why this layer matters for agent operators

Most agent frameworks give you a terminal tool and call it done. You configure a tool name, a description, a working directory if the framework supports it. The model decides what commands to run. If the model decides to run sudo rm -rf /, there is nothing between the model's token output and the shell process.

That is the gap Claude Code treats as a first-class control and the open-source framework followed. A pre-execution pattern check that says no.

The implementation is not complicated. It is a regex deny list with some path normalization. What matters is not the complexity. What matters is that it exists at all, and that it sits between the model and the process.

What a pattern matcher actually checks

The dangerous-command detector checks every shell command against a compiled set of patterns before the command reaches a subprocess. The patterns fall into a few categories.

System config writes. Redirections or writes to /etc/, /private/etc/, /var/, systemd paths, and shell RC files like .bashrc or .zshrc. An agent that can append to .bashrc can inject its own persistence.

Destructive filesystem operations. rm -rf, dd with raw devices, mkfs, wipefs, and format commands. These are not subtle. They are the commands an agent should never run without a human in the loop.

Process killing. kill, killall, pkill, with signal flags. SIGKILL is the big one. A process killed with SIGKILL cannot clean up. An agent that can SIGKILL your database process can cause data loss that no rollback script will recover.

Privilege escalation. sudo, su, chown, chmod 777, anything that changes ownership or opens permissions. An agent that can sudo effectively owns the host.

Network and data exfiltration. curl or wget piping into a shell, nc listeners, ssh with key forwarding, scp to unknown hosts.

A production list also needs environment-specific entries: destructive VCS actions, package-manager lifecycle hooks, Docker/Kubernetes deletion commands, cloud CLIs, database operations, and secret-store reads or writes. The right deny list is partly universal and partly shaped by the tools mounted into the agent.

Each category catches a different class of damage. Together they form a safety net that catches the obvious before it happens. When the PR added _MACOS_PRIVATE_SYSTEM_PATH, it extended that net to cover symlink aliases on macOS. When it added killall -KILL and killall -r, it closed a hole in the process-killing category. When it added find -execdir, it caught a destructive pattern hiding inside a traversal tool.

The key design decision in the PR is not the three new patterns. It is the shared _SYSTEM_CONFIG_PATH constant. Every dangerous-command pattern that touches system config paths now references the same fragment. If someone adds a new protected path tomorrow, they edit one line instead of six. That is the difference between a deny list that stays accurate and one that rots.

The detector is still a deny list, not a proof of safety. Treat it as an approval trigger for known-dangerous shapes, then test it with a bypass corpus: quoting variants, command substitutions, encoded payloads, wrapper commands, symlink aliases, destructive behavior hidden behind package scripts or Makefiles, and platform-specific command variants. Anything allowed through still needs sandboxing, least-privilege credentials, and logs.

Operators need this before they need orchestration

When I talk to people building agent systems, the conversation almost always starts with orchestration. How do you split work across specialists? How do you handle handoffs? How do you manage context across long runs?

Those questions matter. But they do not matter if the agent can destroy the system it runs on. Dangerous-command detection is earlier in the dependency tree than orchestration. You can run a single agent with a terminal tool. You cannot run it safely without a command filter.

The order should be: sandbox, approval gates, command detection, then orchestration. Not orchestration first, security later.

For operators running agents in production:

  • Run the agent in a sandbox or container with least-privilege mounts.
  • Put an approval gate before shell subprocess execution.
  • Treat obvious destructive commands as sensitive: rm -rf, dd, mkfs, wipefs, sudo, broad chmod/chown, process-kill commands, remote script execution, destructive VCS actions, and environment-specific cloud/database/container deletion commands.
  • Canonicalize protected paths so aliases like /etc, /var, /tmp, and /private/... map to the same protected set where applicable.
  • Test a bypass corpus: symlinks, quoting, chained commands, command substitution, find -exec/-execdir, xargs, base64-decoded payloads, shell pipes, package scripts, Makefiles, and platform variants.
  • Log blocked attempts and audit both misses and false positives after incidents or tool-surface changes. A false positive - blocking a safe command - is less damaging than a miss, but it still breaks automation if it happens often. Treat both as signal.

Dangerous-command detection is exactly the kind of control layer I build into production agent systems. If you need this baked into your agent operating system, Mimir Works builds that layer.

I have written before about the failure modes agents hit in production and what monitoring surfaces to watch. Dangerous-command detection belongs in the same category. It is not a security posture document. It is an operating control that stops the agent from doing something you cannot undo.

The convergence between Claude Code and the open-source framework does not make either derivative. It makes the pattern real. If you are running agents with shell access, this layer is not optional.

Read next: Why AI Agents Break in Production and Monitoring AI Agents in Production.

Some links on this site may be affiliate links. I only recommend tools I use. If you click through and make a purchase, I may earn a small commission at no extra cost to you.