Skip to content

The Command You Approve Is Not the Command That Runs

Why approved commands run as something else: shell expansion rewrites what your agent's safety check read. A case for guarding at execution.

In July an agent on a Windows machine was asked to clear an app’s tmp cache. The command it built contained a shell variable inside double quotes, so the outer shell expanded the variable to nothing before rm ever saw it. What one process approved as a scoped cache clear, another process executed as a wipe of /bin, /boot, /etc, and the user’s home directory. It ran for four minutes. When the agent itself noticed and tried to kill the runaway rm, its own safety layer refused: killing the cleanup counted as interfering with the workload. The human finally stopped it by shutting down WSL entirely.

That’s issue 82165 against Claude Code, still open, reported by the person who lost the files. It’s not alone. There’s a developer who watched a quoting mistake turn an approved file copy into rm -rf /* on a production server, taking two websites down with it. There’s a home directory wiped by an rm -rf * that ran after the agent created a folder literally named ~. There are days of uncommitted work gone because a case-flipped path landed on macOS’s case-insensitive filesystem as if it were the real project folder.

I’ve been thinking about what these share, because I write code with these agents every day and I’m not going to stop. And I think it’s this: the command a safety mechanism reads and the command the shell executes are different documents. Every layer between the model and your files can rewrite the text in between. Quoting, variables, tilde, case-folding, all of it happens after whatever checkpoint came before.

The industry’s first answer was to ask the human before anything risky. Anthropic published how that goes: users accept 93% of permission prompts. At that rate the prompt isn’t oversight, it’s ceremony. The people who study warnings for a living have measured click-through rates from under 10% to over 70% depending on context, and Cormac Herley’s argument cuts deeper: ignoring noisy warnings is rational when they almost never matter.

So vendors moved to classifiers, models that judge whether a command looks dangerous. Anthropic measured their own honestly: about 17% of overeager actions get through. And the misses aren’t random, they cluster where judgment is hardest, like whether a session’s earlier approval really covers this particular action. A model reading the same text as the human inherits the same blind spot: both see the command as written. Neither sees what expansion will do to it.

Which suggests a question worth asking: where should the guard sit, and what should it do when unsure? At the point of execution, as close to the shell as possible, failing closed. That’s the bet rv makes.

rv is a small Mac tool that hooks into your coding agent’s shell pipeline. When Pi is about to run a bash command, rv evaluates it first. Recursive deletes get denied, force-pushes get denied, and each denial names the rule that fired. The rules normalize the command first, stripping sudo and env wrappers so sudo rm -rf can’t dress itself up as something else.

You might say: just use a sandbox. Sandboxes are great. E2B’s microVMs, devcontainers, Docker isolation, best blast-radius reduction available and I recommend them. But notice which question they answer: how to protect the host from the workspace. Most of us have the other problem, wanting the agent working on this Mac, in this repo, with these credentials, because that’s where the work is. Mount your project into a container as a bind mount and a delete inside the container deletes your real files just as dead. If the workspace is the thing you’re protecting, the guard needs to run where the command runs.

Designing rv meant deciding what it does when it’s unsure. That turned out to be the whole design.

If rv can’t parse the payload a hook sent it, it blocks. An allow-list fails by refusing, loudly, and someone fixes it; a deny-list fails by allowing, silently, forever. Saltzer and Schroeder wrote that principle down in 1975 and it hasn’t aged a day. Unknown commands are allowed, though, and that’s deliberate: rv ships with few rules precisely because a guard that blocks everything unrecognized gets disabled within the hour, and a disabled guard is worse than none.

There’s an escape hatch, on Grok only for now. If rv stops something you meant to run, you can mint a one-time unlock scoped to that exact command in that exact folder, and next time it asks again. Not on Pi or OpenCode yet. Their hook payloads don’t carry the working directory today, though rv could forward it, and I won’t scope an unlock to less than command-plus-folder, because trust-this-command-anywhere is how you end up back where you started.

Install is one line:

curl -fsSL https://rykanv.com/install | sh

The code is public at github.com/christopherkarani/rv. v1 is macOS 26 on Apple Silicon, wired into Pi today, with other hosts close behind.

I’m not claiming this replaces sandboxes, and I’m not asking anyone to trust another model’s opinion about danger. It’s one narrow thing: when your agent decides to run a command, something beside the shell reads the same text the shell will execute and refuses the handful of patterns that have already deleted people’s home directories. The approval gap doesn’t close by hoping prompts get better. It closes where the command runs.