twimco.Let’s talk ↗︎
Menu
Field notes / Product & engineering

Test the deny shapes before you trust the policy.

Your agent policy says no rm -rf. Has anyone tried TZ="$HOME" rm -rf, a command chained after &&, or a symlink to the file you blocked? Try the odd shapes before you call it enforced.

Original conceptual editorial illustration; not a client interface.

Before you trust a deny policy for an agent that can run shell commands, read files, or call destructive tools, run the denied commands through your real matcher in the shapes agents actually emit: env-variable prefixes, bare assignments, compound and nested commands, and symlinked paths. Don't call the policy enforced until each shape is blocked, or explicitly allowed by a named owner, and rerun the tests whenever the agent or policy changes.

The rule said no. The command said TZ.

Here's a line you might find in an agent's permission settings: deny rm -rf. Clear enough. Now here's a command an agent might actually emit:

TZ="$HOME" rm -rf build

It's the same delete with a harmless-looking environment variable in front. Until this week, in one widely used coding agent, that shape could slip past Bash deny and ask rules when sandbox auto-allow was on. Anthropic's Claude Code 2.1.289, released October 3, closed that gap. The same release closed a few more: commands that started with a bare variable assignment, deny and ask rules on nested parts of compound commands, and Read denies that didn't follow symlinked file references.

We're not reviewing a product here. Anthropic found the gaps and fixed them, and that's what you want a vendor to do. What matters is the lesson, and it applies to every agent stack that can run a shell, read files, or call a destructive tool. A deny rule written down is a promise. The matcher is what actually keeps it. And the matcher only sees the command shapes it was built to recognize.

So before you tell anyone your agent policy is enforced, run the odd shapes against it. Ask whether the rule actually blocks the commands your agent sends, not whether the rule exists.

A policy on paper and a policy in the matcher

Most teams review agent permissions by reading the config. Someone opens the settings file, sees deny: rm, deny: Read(~/.ssh/**), ask: git push, and nods. The review is about intent, and the intent is right.

But intent doesn't run. A matcher runs, and a matcher is just code. It parses a string, decides what the "command" is, compares that against patterns, and returns allow, ask, or deny. Every step has a choice in it. Does it look at the first word or every word? Does it split on &&, ;, and |, and does it go inside $( ) and subshells? Does it compare the path you typed or the path the file system resolves?

Each of those choices is a place where what the policy says and what the matcher does can drift apart. Nobody sees the drift in review, because the reviewer reads the rule and the agent never reads the rule. The agent just emits commands, and sometimes they come out in shapes the matcher's author didn't picture.

Agents aren't trying to sneak around you. They write shell the way people write shell: they set a variable inline, chain three steps with &&, wrap something in a subshell, follow a symlink because that's where the file lives. Those are normal shapes, and normal shapes are exactly the ones a narrow matcher misses.

A deny rule on paper is a promise. The matcher is what keeps it.

Five shapes to throw at it

Pick the commands and paths your policy denies. Then send each one through your real matcher in every shape below, using the version and settings you actually deploy.

Env-variable prefix. TZ="$HOME" rm -rf build, FOO=1 BAR=2 rm -rf build. Does the matcher strip the assignments and find rm, or does it see a command called TZ=... and wave it through? Try it with expanded values too, not just literals.

Bare assignment. A line that starts as an assignment and does the work inside it or right after it: out=$(rm -rf build), DIR=build; rm -rf "$DIR". If the matcher only classifies the first token, this is where it goes blind.

Compound and nested parts. make clean && rm -rf build, (cd app; rm -rf build), echo ok | xargs rm -rf build, bash -c "rm -rf build". The denied step is in the middle, or inside a subshell or a second interpreter. A rule that matches only the head of the line will approve the whole chain.

Symlinked paths. You deny reads of ~/.ssh/** or .env. Then a symlink called notes.txt points at the private key, or the IDE passes a file reference that resolves into the blocked folder. Does the deny follow the link to the real target, or does it only check the name it was handed?

The cousins. /bin/rm, command rm, \rm, find build -delete, a quoted path, a relative path that climbs with ../. You don't need every variant on earth. You do need the ones your agent is likely to emit, and the ones your team would emit at 2 a.m.

Do this safely. Use the tool's decision or dry-run mode if it has one, and otherwise a throwaway environment with a canary directory you don't mind losing. You're testing the decision, so nothing real should be in the blast path.

Record each result as blocked, asked, or allowed. If anything comes back allowed, it gets one of two outcomes. Either it's fixed and re-tested, or someone with a name writes down that the shape is allowed and why. Some teams keep this on a single page, a Deny Shape Test Card per agent. What matters is the list of shapes and the results, not what you call the page.

How the work moves

Prove a deny rule against real shapes

  1. List the rules

    Every deny and ask rule for agents with shell, file, or destructive tool access.

  2. Write the shapes

    Env prefix, bare assignment, compound or nested, symlinked path, common cousins.

  3. Run the real matcher

    Shipped version and settings, dry-run or disposable environment, canary target.

  4. Close or own each gap

    Fix allowed shapes, or have a named owner accept them with a reason.

  5. Rerun on change

    New agent version, sandbox mode, or policy edit means the shape file runs again.

Illustrative workflow, not a client screenshot or measured result.

How the false confidence plays out

These are composites, not client incidents or measured rates. They're the patterns that show up when the gap between the rule and the matcher goes unnoticed.

The clean audit. Security reviews the config, signs off on "destructive commands denied," and moves on. Nobody ran a single prefixed or chained command. The sign-off certifies the paper.

The upgrade that moved the line. A matcher fix like this week's tightens behavior, which is good. Matchers also change in the other direction: a new parser, a new auto-allow mode, a new way of passing file references. If your shape tests ran once at setup, you don't know which side of the line you're on today.

The sandbox as excuse. "It's sandboxed, so the deny list is just belt and braces." Then auto-allow inside the sandbox turns out to be exactly the path where deny rules were being skipped. Your sandbox and your deny list fail in different ways. Test both.

The symlink in the repo. A config file in the working tree is a link to a credentials file outside it. The Read deny covers the credentials path by name. The agent reads the link, because it was asked to summarize the config.

Each time, the policy was reasonable. Nobody had checked it against the shapes.

This isn't the permission matrix, or containment, or supervision

There are a few layers of agent control, and this one is easy to blur into the others. They answer different questions.

The permission matrix asks who may act, and on what: which agent job may call which tool against which system. It's the intent.

Containment asks what should be out of reach even if every rule fails: networks, mounts, production credentials that simply aren't there.

Supervision asks who watches when the agent acts alone, what gets logged, and how you stop it.

Deny-shape testing asks whether the matcher between the agent and the shell actually enforces the matrix for the commands the agent really sends.

A perfect matrix with a narrow matcher is still a hole. A strong sandbox doesn't tell you whether your ask rules ever fire. And a supervisor reading logs after the fact is the person who finds out a deny didn't hold. They can't stop it in advance. You want all four, and you want to test each one separately.

Decision guide

Paper deny versus tested deny

ConsiderationTested denyPaper deny
EvidenceShape tests run against the shipped matcher.Config file read in review.
Command shapesPrefixes, assignments, chains, subshells covered.Only the plain command checked.
File pathsDeny follows symlinks to the resolved target.Deny matches the name it was handed.
GapsFixed, or allowed by a named owner with a reason.Unknown until something slips.
UpgradesShape file reruns on every agent or policy change.Tested once at setup, if at all.
Decision framework: assess your own agent policy against these conditions.

Put this into practice

  1. List every deny and ask rule for each agent that can run a shell, read files, or call destructive tools. Include the defaults you never wrote yourself.
  2. For each rule, write the five shape families against it: env-variable prefix, bare assignment, compound or nested, symlinked path, and the cousins your team actually uses.
  3. Run them through the real matcher, on the version and settings you ship, in dry-run mode or a disposable environment with a canary target.
  4. Mark each one blocked, asked, or allowed. Fix the allowed shapes, or get a named owner to sign off on keeping them, with a reason.
  5. Keep the shapes as a test file and rerun it whenever the agent, its CLI, the sandbox mode, or the policy changes. A fix in the changelog is a reason to rerun the file, not to skip it.
  6. Don't call the policy "enforced" in a review, a deck, or a customer questionnaire until that file passes.

If you're building agents that work inside real repos, file systems, or operational tools and want permissions that hold up when someone tests them, that's the work we do in AI applications & agents, and in web applications when the agent sits inside a product your team runs.

The rule says no. Make sure the matcher agrees.

Related work: Spin Market.

twimco.

We build alongside the people doing the work.
Bring us the problem you’re thinking about.

Related work

Spin:Market ↗︎