justadev
All thoughts

My agents never touch git

An agent ran one ordinary command and erased 475 rows of live data. The fix was not a better prompt.

3 min read

On 30 July 2026 one of my agents ran git pull --rebase origin main. It is one of the most ordinary commands in software. It destroyed 475 rows of live data.

How one pull ate a database

The repo held a small product's live SQLite database, tracked in git on purpose. An automatic snapshot committed some folders, but not the database. So the database routinely sat uncommitted for days.

That repo's origin/main also carried an unrelated product history. The rebase checked it out over the working tree. Every outreach row written since the last database commit was gone. The sent count fell from 841 to 366. Mail events fell from 41 to 17.

The loss was not cosmetic. The send script picks recipients who were never contacted. With the history gone, it would have mailed people a second time and burned the sender's reputation.

The agent had been told not to commit. It did not commit. Pull, rebase, checkout and reset are just as destructive, and nobody had listed them.

The same shape, without an agent

Four days later it happened again with no agent involved. A deploy script ran git reset --hard origin/main on a directory where another session was editing. It erased about two and a half minutes of uncommitted work. The other session noticed within a minute, read the reflog and moved into its own worktree.

That one taught me the general rule. On my server, development happens on the box, because that is where the agents are signed in. The deploy target is somebody's desk. Any script that resets a checkout is a data-loss tool, whoever runs it.

And uncommitted work leaves no trace. The reflog records the reset. It says nothing about what the reset ate.

A prompt is a hint

The tempting fix is a better prompt. Add a line: do not run destructive git commands. I did add that line. It is not the fix.

I learned the difference on another product. A preview mode had to guarantee that nothing posted without a human confirm. The worker intercepted the submit action but let a click run unchecked, so the whole guarantee rested on the model choosing the safe verb. The prompt's own example told it to click Submit. A preview could post live to a real account, and all 101 tests passed, because the test fakes never clicked anything real.

A model follows an instruction most of the time. An irreversible action has to be blocked all of the time. Those are different numbers.

Prompts are hints. Invariants live in code.

What is in code now

  • Every lane spec bans all git commands: no pull, rebase, checkout, reset, stash, clean, commit or branch. If something looks out of sync, the lane stops and reports.
  • Only the orchestrator runs git, and only after it has verified the work.
  • Deploy scripts abort on a dirty checkout, print what they found and name the worktree command.
  • The database is backed up every 30 minutes into a folder git ignores, so no git operation can take the backups with it.
  • A hook refuses agent dispatch through a detached shell, because a detached shell hides the exit code.

The last one shows the pattern. When a rule matters, it moves from the prompt into the harness. Then it gets a test that tries to break it, with something that would really fire. A guard that cannot show you it stopped a real action is decoration.

There is a recovery path now too. The mailbox’s sent folder is an independent record of every send, and a script rebuilds the history from it. Opt-outs then need a separate check, because restoring a row to sent can overwrite an opt-out. Recovery is always more work than the guard would have been.