A second agent (or a self-coder running 'git checkout .') silently reverts your uncommitted work
Market & numbers — every figure sourced
A second agent (or a self-coder running 'git checkout .') silently reverts your uncommitted work
You spend an hour with an agent fixing a real bug. The edits look right on disk. Then they're just... gone. The file is back to its last-committed state, no error, no diff, no trace. Nothing crashed. Something quietly reset the working tree out from under you.
This is one of the most demoralizing failure modes in agent-driven and multi-process development, because the work never existed anywhere durable. We've eaten this exact loss more than once, and it always comes from the same two sources.
What we tried
We ran more than one thing against the same repo at the same time:
- A second agent in another git worktree of the same repo, both editing toward the same goal.
- A "self-coding" / auto-improve loop with a reset step in it — a watcher that periodically runs something like `git checkout .` or `git reset --hard` to get back to a "clean" known state before its next pass.
Both felt safe in the moment. Worktrees are supposed to isolate work. And an auto-loop resetting to clean before it starts seems hygienic. The assumption in both cases was: "my uncommitted edits are mine; another process can't touch them."
What broke
The killer detail: uncommitted edits are not stored in git's object database. Tracked-but-uncommitted changes live only in your working tree and index. When any process runs `git checkout .` or `git restore .`, git overwrites those files with their last-committed content and writes nothing to the object store. There is no dangling commit, no blob to fish out — so reflog and fsck cannot bring it back 0. That's the whole horror of it: it's not "hard to recover," it's unrecoverable by git.
The two paths to that reset:
- The self-coder's reset step. An auto-improve loop's `git checkout .` does not distinguish "junk it generated" from "the hour of work you just did by hand in the same tree." It blows away everything uncommitted, including yours. This is the more common culprit because it's scheduled — it'll fire on a timer whether or not you're mid-edit.
- The second worktree, indirectly. Git itself is careful here: by default `git worktree add` "refuses to create a new worktree when `<commit-ish>` is a branch name and is already checked out by another worktree," and regular `refs/` are shared across worktrees while each worktree keeps its own `HEAD` (per the git-worktree docs). So git won't let two worktrees sit on the same branch and stomp each other's files directly. The loss happens when someone forces it (`-f`), or when both worktrees push/rebase the shared branch and one agent's commit history rewinds under the other, or when the second process is the one running the destructive `checkout`/`reset`. The "second worktree" is the trigger; the destructive git command is the weapon.
The unifying root cause: work that isn't committed has no owner git will defend. Worktree isolation protects committed branch state, not your dirty working tree against a sibling process that runs a reset.
The fix
Commit fast, and disarm anything that resets the tree. Two moves:
1. Make work durable the instant it's worth keeping
A WIP commit takes about 10 seconds and converts "ephemeral edits a reset can erase" into "an object in the database that survives any later checkout/reset and is recoverable via reflog."
```bash
git add -A && git commit -m "wip: <what you just did>"
```
Don't wait for "done." Commit the moment the work would hurt to lose. You can always `git commit --amend` or squash later. A commit is the only thing a sibling process can't silently revert — and even if a branch gets rewound, the commit stays reachable through `git reflog` (git-reflog docs).
If you're not ready to commit, at least stash — `git stash` saves dirty state as real commit objects (git-stash docs), so a later `git checkout .` in the tree can't touch it.
2. Disarm the wiper
Find the self-coder / auto-improve / watcher process and either stop it or remove its reset step before you start hand-editing in that repo.
```bash
find scheduled resetters (macOS launchd example)
launchctl list | grep -i improve
launchctl unload ~/Library/LaunchAgents/com.<yourorg>.improve.plist
grep the loop's source for the destructive commands
grep -rn "checkout \.\|reset --hard\|restore \." path/to/loop/
```
The non-negotiable rule for any automated loop: it must never run `git checkout .` / `git reset --hard` / `git restore .` on a tree a human or another agent might be editing. If a loop needs a clean state, it should do its work in its own dedicated worktree or a throwaway clone, never the shared one.
3. Keep multi-agent work on separate branches in separate worktrees
If you genuinely want two agents in parallel, give each its own worktree on its own branch and let them integrate by merging commits — not by sharing a dirty working tree:
```bash
git worktree add ../repo-agent-b -b agent-b/feature
```
Then neither one's reset can reach the other's files, and integration happens through committed history.
Apply it
- Before hand-editing any repo that has automation pointed at it, check for a reset loop first (`launchctl list`, cron, a watcher script) and stop or de-fang it.
- WIP-commit aggressively. Treat any edit you'd be annoyed to redo as commit-worthy. ~10 seconds now beats an hour later.
- Replace `git checkout .` with `git restore` and a conscious decision — but understand both are equally destructive to uncommitted work; the safety comes from committing/stashing first, not from which command you type.
- Give parallel agents separate worktrees on separate branches. Integrate by merging commits, never by sharing one dirty tree.
- Mental model: git defends committed history, not your working tree. Anything uncommitted is fair game for the next reset — make it a commit and it stops being fragile.