·6 min read·Ofir, Marshal Team

Command Allowlists for AI Agents: Why a Command Name Isn't a Policy

An AI agent command allowlist answers 'which binary?' The real question is which verb, which arguments, against which target. A command name is not a policy.

In October, a write-up from Threat Frontier catalogued a week of flaws in Model Context Protocol software. One of them is the cleanest illustration of a problem every coding agent now shares. An MCP shell server called mac-shell-mcp (CVE-2026-108953) kept an allowlist of commands it would run without asking. The allowlist matched on the command name. A prompt injection could append a shell metacharacter and a second command, and the second command ran with no approval, because the first token still matched. Every major coding agent ships some version of that allowlist: a set of commands it may run in auto-run mode without stopping to ask. It is a sensible feature. It is also a weak control, for a reason that has nothing to do with any one tool's implementation.

An allowlist of command names answers one question: which binary is about to run? That is not the question that decides whether an action is safe. The question that decides safety is which verb, with which arguments, against which target. aws is not dangerous. aws s3 rm against your production bucket is. A control that cannot tell those two apart is not a policy. It is a spelling check.

An agent's allowlist matches command names like pwd, aws and kubectl, each with a red bypass arrow, while a control layer judges the full invocation: aws s3 cp allowed, aws s3 rm denied, kubectl get allowed, kubectl delete denied

How allowlists and auto-run modes work today

Most coding agents have converged on a similar permission model. Some actions run freely, some prompt for approval, and a configurable list moves actions from the second group to the first. Teams populate that list with the commands they are tired of approving: ls, cat, git status, the test runner, the linter. The intent is reasonable, to stop asking about the things that are obviously fine.

The trouble is that "obviously fine" is a property of the whole invocation, not the binary. git is fine until git push --force to a shared branch. cat is fine until cat ~/.aws/credentials. The moment the allowlist keys on the name, every argument the agent can attach to that name inherits the approval.

Four ways a harmless-looking command isn't

The gap between the name and the action is not theoretical. It has at least four well-documented shapes.

Shell metacharacters

The mac-shell-mcp case above is the archetype. An allowlisted pwd is harmless. pwd ; rm -rf something passes a name check and then runs a second command the allowlist never saw. Semicolons, pipes, &&, backticks and $() all turn one approved token into an arbitrary sequence.

Environment poisoning

A command can be redirected without changing a single character of the command line. Cursor's advisory CVE-2026-22708 described an allowlist bypass through environment variables: the agent sets a variable that changes what the "approved" command actually does when it runs. The name matches. The behavior does not.

Dangerous targets

Some commands are safe or catastrophic depending only on where they point. curl to a documentation site is routine. The same curl to an internal metadata endpoint, or a POST of a file to an external host, is exfiltration. psql against a scratch database is fine; against production it is the nine-second-database story waiting to happen. The verb and the binary are identical. The target is the whole risk.

Filesystem tricks

A cleanup command pointed at a directory can do far more than the author intended when symlinks or junctions redirect it to live files. An agent that "tidies up" a path can follow a link into a real tree and delete it. Autonomous coding agents have deleted large numbers of files this way in minutes.

Policy has to judge verb, arguments and target

The fix is not a longer allowlist. It is a control that classifies the full invocation.

That means reading the verb, the arguments and the destination together, and deciding on the combination. The public shape of it is easy to state:

  • aws s3 cp, allowed. aws s3 rm, denied.
  • kubectl get, allowed. kubectl delete, denied.
  • A read against a scratch host, allowed. The same read against a production target, escalated for approval.

This is the same principle that governs database access by statement rather than by connection, covered in the statement-level policy article: do not ask who is connecting, ask what is about to run. A binary name is an identity. A verb against a target is an action. You authorize actions.

It has to run where the agent can't edit it

There is a second requirement that teams discover the hard way. The policy cannot live in a file the agent can write.

Coding agents edit configuration as part of normal work. If the allowlist, the permission settings or the hook definitions sit in the repository the agent is working in, or in a dotfile in its home directory, then the agent, or a malicious package it installed, or an instruction it absorbed from untrusted content, can change the rules before acting. A control the subject can rewrite is a suggestion.

Enforcement has to sit outside the agent's process, in a place the agent cannot reach to modify, evaluating each invocation as it happens. That is the difference between a config file and a control plane.

A starter policy you can adapt

You do not need a product to start thinking in verbs and targets. The following is a generic shape, not any specific implementation, that you can translate into whatever your stack enforces:

# Judge the full invocation: verb + arguments + target, not the binary name.
default: ask
rules:
  - match: { command: aws,     args_startswith: "s3 cp" }   # copy out
    decision: allow
  - match: { command: aws,     args_startswith: "s3 rm" }   # delete
    decision: deny
  - match: { command: kubectl, args_startswith: "get" }
    decision: allow
  - match: { command: kubectl, args_startswith: "delete" }
    decision: deny
  - match: { command: "*",     target_host: "*.prod.internal" }
    decision: ask

Three properties matter more than the syntax. It keys on the verb and arguments, not the binary alone. It treats the target as part of the decision. And it defaults to ask, so a command nobody classified is escalated rather than waved through. Build those three in wherever your enforcement lives, and keep the rules out of the agent's reach.


An allowlist of command names is a convenience feature that teams mistake for a security control. It answers "which binary?" when the question that decides safety is "which verb, with which arguments, against which target?" Metacharacters, environment poisoning, dangerous targets and filesystem tricks all walk straight through a name check. A real policy reads the whole invocation, treats the target as part of the decision, defaults to asking, and runs somewhere the agent cannot edit it.

This is the action-layer companion to statement-level policy for production databases →, which applies the same idea to SQL.


Marshal inspects every tool invocation by verb, arguments and target, and enforces the decision outside the agent's reach. Book a demo →

securityai-agentsplatform-engineeringdevsecops

Marshal runs AI agent sessions in isolated Kubernetes pods: credential isolation, audited egress, per-verb tool policy.

Book a demo →