← All packs

Scoping an agent's permissions — why "can run any command" is the bug

When an agent does something wrong, the instinct is to fix the prompt: add a warning, list the forbidden actions, explain the consequence. That's treating a permission problem as an instructions problem. If the agent technically could run rm -rf, push to main, or read .env, then sooner or later — a different phrasing, a longer context, a subtly adversarial input — it will exercise that capability, no matter how the prompt reads. The durable fix is almost never a better warning. It's a grant that doesn't include the dangerous action in the first place.

Allowlist the specific, don't denylist the dangerous

A denylist has to anticipate every bad command in advance and is always one rewrite away from a bypass — block rm -rf / by string match and the agent finds find . -delete or a Python shutil.rmtree that doesn't match the pattern. An allowlist inverts the burden: the agent can run git status, npm test, ls, and nothing outside that set, so a novel bypass has nothing to bypass into. Allowlists cost more up front — you have to enumerate what the agent actually needs — but that cost is paid once, while a denylist's maintenance cost is paid forever, every time someone finds a new way to spell the same dangerous action.

Scope the tool, not just the command string

A bare shell tool with a command allowlist is still one layer more permissive than it looks, because "allowed" commands can be composed into disallowed effects — an allowed git plus an allowed config subcommand can rewrite the remote a later allowed push goes to. Purpose-built tools close this gap by construction: a create_pr tool that only accepts a branch name and a body can't be composed into a force-push, because force-push isn't a parameter it exposes. Prefer a small number of narrow, single-purpose tools over one wide tool plus a list of restrictions on how to call it — the restrictions are a promise, the narrow interface is a fact.

Wide grantScoped equivalent
arbitrary shell, denylist of bad commandsfixed set of named tools, each with a narrow parameter shape
filesystem read/write anywhereread/write confined to a declared working directory
one API token with full account scopea token minted per-task with only the scopes that task needs
network egress to any hostegress allowlisted to the specific APIs the agent calls

Match grant width to task width, not to convenience

It's tempting to hand a new agent broad access once, up front, so nothing ever blocks it mid-task — but that grant persists long after the task that justified it, and every later task inherits the same width whether it needs it or not. Re-deriving a narrower grant per task — this run gets write access to one repo, that run gets read-only access to a database — costs a small amount of setup friction and buys back the guarantee that a compromised or confused agent on task B can't reach the blast radius that only task A actually needed.

A credential the agent never holds can't be exfiltrated by the agent

Permission scoping and secrets handling converge on the same point from different directions: the safest secret is the one the agent's process never has in memory or environment at all, brokered instead through a proxy that holds the real credential and only forwards the specific calls the agent is allowed to make. This is a stronger property than scoping the agent's own token, because it removes an entire class of leak — logging, prompt injection, a stray echo $TOKEN — by making the token absent rather than merely narrow.

Permission width is a decision to revisit, not a one-time setup step

The grant that was right for an agent's first task is rarely still right after six months of scope creep — one more directory here, one more API endpoint there, each addition individually reasonable and the sum considerably wider than anyone would have approved in one step. Treat the grant like a dependency: audit it periodically, ask what's actually used versus merely available, and remove the unused width rather than leaving it as a standing invitation.

The question that catches most over-broad grants: if this agent's output were driven by a malicious or simply badly-confused input on its worst run, what's the most damage the tools it holds allow — not what the prompt tells it to do, but what the grant physically permits? If that answer is broader than the task in front of it, the grant is the bug, and no amount of prompt tuning fixes it. One concrete source of that "malicious input": instructions hidden in content the agent reads, where a narrow grant is what caps the damage even when the model itself gets fooled.

Related: trust boundaries between agents — the same narrowing logic applied to what a subagent's output is allowed to trigger, not just what tools an agent can call directly. Scoping decides what an agent may do at all; escalation design decides which of those allowed actions should still pause for a human first.