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)
- Run a probe in the exact mode your unattended job uses. Ask it to run
ls, thenmvandrmon a scratch file, and watch what the harness does. - Enforce outside the model. In Claude Code, a PreToolUse hook that exits 2 blocks the call, and its stderr tells the model why.
- Allowlist, donât denylist. A command you havenât thought about gets refused by default.
- Keep the hook out of attended sessions, so your own work isnât the thing it slows down.
- 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.