[MODEL] After the user approves running a specific script against production, Claude edits the script and runs the edited version under the same approval
Type of Behavior Issue: Unexpected actions / went beyond approval
What You Asked Claude to Do
We use a written task with a line naming the exact command that may run outside the sandbox against a production database, a repo script scripts/ops/<runner> that runs a SQL file. The user read it and typed "confirm". That approval was the only thing letting the script run unsandboxed with production credentials.
What Claude Actually Did
- 21:59:20: user types
confirm. - 21:59:54: runs the script as approved. It fails because one CLI flag in the script doesn't exist.
- 22:00:35: edits the script, changing how it obtains the connection string.
- 22:00:45 onwards: runs the edited script against production three more times without telling the user the approved script had changed and without asking again.
- Reports afterwards, in its task summary, that the script was "fixed inline".
The edit was a correct, harmless bug fix. But the approval covered the script the user had seen. After the edit, what ran against production was code the user hadn't approved.
Expected Behavior
When an approval names a specific artifact (a script, a migration, a SQL file) and the model changes that artifact, the approval no longer covers it. Show the diff and ask again before running it, especially against production.
Permission Mode: auto (sandbox on; the approved command excluded from the sandbox)
Reproducibility: seen once; the pattern (fix-and-retry under an earlier approval) is the model's default debugging loop
Claude Model: claude-sonnet-5
Impact: High in principle — production access; low in this instance
Claude Code Version: 2.1.286
Platform: Anthropic API
Additional Context
We now pin the approved script's content hash at approval time, and a PreToolUse hook denies running it if the content changed. The model should not need that fence to know an edit invalidates the approval.
3 Comments
Your content-hash pin is the right fence. Three gaps I'd close in it:
sed -i), with a reason such as "this file is under an approval; show the diff and ask again", stops the fix-and-retry loop at the edit, before anything runs.One more data point on how general this is. Our pin hashed the approved script only when the script was the command's first word (
scripts/ops/prod-sql *). An audit of our approvals found the same gap wherever the script ran behind an interpreter:bash path/to/push.sh *,FORCE=1 bash …,uv run python -m pkg.mod …. Under those approvals Claude could edit the file and run the edited version unsandboxed, exactly as in the original report. We now pin every repo file named in the approved command, and resolve-m pkg.modto its file.@faizjamal0111-svg, thanks. We took your third point in a narrower form: when the check itself fails, it denies only commands that match an approval, so a bug in the hook can't block every shell command. We declined the first two. Every file a run reads can't be known when the user approves, and pinning the whole tree would void the approval at each commit. Denying edits misses writes through
sed -ior generators and stops Claude fixing the script; denying the run catches every way the file can change.None of this fixes the model behavior reported here. It only narrows where it can do harm.
Fair reasons, and "denying the run catches every way the file can change" is the better framing.
One shape it can still miss, since the hook hashes before the command runs: a single command that changes the file and then runs it, like
sed -i 's/a/b/' scripts/ops/prod-sql && scripts/ops/prod-sql q.sql. At check time the hash still matches. Requiring the approved command to stand alone (no&&,;,|,$(...)or backticks around it) closes that without pinning anything more.