Agent boundaries ¡ Happened

"Never rm" was a sentence, and rm worked

My nightly agent's prompt said never mv or rm. I probed it in the same permission mode it runs in: ls, mv and rm all ran. The prohibition was prose.

Fixed Accurate as of
Two panels. Left, the probe with the prompt only: ls, mv and rm each listed as ran, no denial, under the prompt text never mv or rm and the wrapper comment the model never runs a shell. Right, the first night with the hook: 18 of 18 shell calls read-only, 0 refusals, with a note that a PreToolUse hook allowlists read-only commands, checks writes against a pre-run snapshot and refuses to start on uncommitted instruction edits.

The short version: The prompt for my nightly vault-ingest agent said “never mv or rm”. The wrapper script’s comment said “the model never runs a shell”. On September 12 I ran a probe in the exact mode the nightly runs in. ls, mv and rm all executed. No denial. Now a hook gives that run a read-only shell, mechanically.

Get the week's seven in one email, every Monday.

What went wrong

The nightly runs headless, in a permission mode I picked so it could write its report. That mode also let it delete. The two safeguards I believed in were a sentence in a prompt and a comment in a script. Neither is enforced by anything, and nobody is awake at 3 a.m. to say no.

What we learned

  • A prohibition in a prompt is a request. Test what the permission mode allows before you trust what the prompt forbids.
  • Comments describe intent, not behavior. “The model never runs a shell” was true of the design and false of the harness.
  • Taking the shell away wasn’t an option. In that harness build, the shell was also the model’s only way to list and search files. The fix had to narrow the shell, not remove it.

What went right

A hook now sits in front of every shell call the nightly makes. It splits the command where the shell would, requires every segment’s first word to be on an allowlist of read-only commands, refuses redirections and command substitution, and refuses the flags that write, like find -delete and sed -i. Writes are checked against a pre-run snapshot, and the run refuses to start if its instruction files have uncommitted edits. Only the nightly loads it, and attended sessions run without it, so the allowlist never gets between me and my own work.

First night after the hook: 18 shell calls, all read-only, zero refusals.

How to do it (as of October 2026)

  1. Run a probe in the exact mode your unattended job uses. Ask it to run ls, then mv and rm on a scratch file, and watch what the harness does.
  2. Enforce outside the model. In Claude Code, a PreToolUse hook that exits 2 blocks the call, and its stderr tells the model why.
  3. Allowlist, don’t denylist. A command you haven’t thought about gets refused by default.
  4. Keep the hook out of attended sessions, so your own work isn’t the thing it slows down.
  5. Expect gaps. A month later a review found a lone & and two sed commands slipping through. The quote-aware rewrite of the parser is still in progress.

Status

Fixed, with follow-ups. The allowlist closed its first gaps on October 15, and a stricter parser is being built.

The week's seven, by email

Entries post here daily. Every Monday, one email with the past week's seven. Unsubscribe anytime.

Let's Connect