
๐ค Ghostwritten by GPT 5.4 ยท Fact-checked & edited by Claude Opus 4.6
A late-May rename of an internal agent turned into a textbook git reset --hard disaster. The damage was self-inflicted: a second terminal, pointed at the same repository, ran git reset --hard origin/main while a large multi-file rename was still staged but not committed. In one instant, 27 of 49 staged files disappeared from the working state, and the rename was left half-applied.
That is the dangerous part. A destructive git mistake does not always produce an obvious failure. Sometimes it creates a repository that still builds in places, still resolves in others, and only fails where old and new names cross paths. Some layers pointed at the new slug while others had silently reverted to the old one. That kind of split-brain state is worse than a clean break because it looks recoverable right up until auth, routing, or credential resolution takes the wrong branch.
This build-log entry covers that failure mode: what broke, how the recovery worked, and which rules came out of it. The short version is simple. git reset --hard is one of the few truly destructive git commands, parallel terminals multiply the blast radius, and long-running staged work without frequent commits is an avoidable risk.
TL;DR: git reset --hard does not just "clean things up" โ it can instantly destroy uncommitted work, and in a parallel-terminal workflow it can leave a feature in a misleading half-state.
Most git mistakes are recoverable because they move references around rather than deleting content from the working tree and index. git reset --hard is different. It forcibly resets three things at once: HEAD, the index, and the working tree. If staged or unstaged changes have not been committed, they vanish immediately.
That matters even more in rename-heavy work. A rename is rarely one edit in one file. It tends to sprawl across:
When a rename is interrupted halfway through, the repository can still look superficially healthy. Imports may resolve. Some tests may pass. The app may start. But the semantic contract of the rename is broken because old and new identifiers coexist.
In this incident, the command was run in a second terminal against the same active repository. Parallel terminals are normal and useful, but they make it easy to forget that both shells are mutating the same index and working tree. One terminal can be carefully staging a multi-file refactor while the other terminal โ perhaps used for a quick sync or cleanup โ destroys the state the first terminal depends on.
Git's own documentation describes --hard as resetting the index and working tree to match the target commit, discarding tracked changes in the process. That behavior is not surprising in theory. It is devastating in practice when the command lands in the middle of a staged-but-uncommitted session.
A good rule of thumb: if a command can be described as "discard local changes," it deserves the same caution as a production data deletion command.
TL;DR: The reset did not just remove work โ it created an inconsistent repository where some layers referenced the new slug and others silently reverted to the old one.
The rename had been staged across 49 files. After the reset, 27 of those staged changes were gone. The rename was no longer a rename. It was a partial translation.
That kind of breakage is subtle because rename work crosses boundaries that do not fail uniformly. One layer may key off a route path, another off an internal slug, and another off a credential lookup name. If only some of those references change, the system can enter a state where requests still flow but resolve incorrectly.
The practical risks looked like this:
| Layer | New slug applied? | Old slug still present? | Failure mode |
|---|---|---|---|
| Routing | In some paths | In others | Requests resolve inconsistently or hit dead routes |
| Auth/credential lookup | Partially | Partially | Valid secrets requested under the wrong identifier |
| Background jobs | Mixed | Mixed | Scheduled tasks target stale names or fail silently |
| Tests | Incomplete | Incomplete | Coverage gives false confidence โ assertions match only one side |
| Documentation/runbooks | Lagging | Present | Operators follow the wrong recovery path during incidents |
This is where reliability and security intersect. A half-applied rename across auth and routing layers is not just a nuisance โ it is a hazard.
Reliability suffers because components disagree about the canonical identifier. A route may point to a new slug while a downstream lookup still expects the old one. Security suffers because credential or access resolution can drift toward dead or unintended references. Even if nothing is exposed, the system is now making identity decisions against inconsistent naming.
The recovery standard could not be "the app looks fine" or "the main path works again." The only acceptable definition of done was end-to-end slug consistency across every layer touched by the rename.
TL;DR: Recovery was not a single command โ it was a controlled reconstruction of the missing rename work followed by end-to-end verification that the slug resolved consistently everywhere.
The first step was to stop making it worse. No more cleanup commands. No more rebases. No more "quick fixes" in another shell. Once a destructive git event has created a mixed state, preserving evidence matters more than speed.
Before changing anything, the missing surface area had to be mapped โ identifying where the rename was supposed to land:
After a destructive git event, the temptation is to patch the visible failures first. That is how half-states survive. The better move is to define the full blast radius up front.
A file-by-file diff helps, but rename recovery also needs semantic checks. The key question is not "which files changed?" but "where does the old slug still exist where it should not?"
Useful checks included:
A simple pattern like this is illustrative:
git status
git diff
git grep 'old-slug'
git grep 'new-slug'Those commands are not magic. They just force the repository to reveal mixed identity.
The lost staged changes were reconstructed and re-applied until the rename was consistent again. This was slower than the original pass because every file now had to be treated as suspect. But that slowness was useful โ it forced verification at each layer instead of assuming staging had captured everything the first time.
A rename touching auth and routing is only finished when the full path resolves correctly. That meant checking:
The repository was not considered healthy until the old slug had been eliminated from active paths and the new slug resolved cleanly from entry point to downstream dependency.
TL;DR: Three rules came out of the failure: commit frequently during multi-file operations, isolate risky work in a git worktree, and never run --hard resets in parallel terminals against an active session.
The most useful postmortems produce operating rules, not just stories. This one yielded three.
Staging is not durability. It feels structured, but it is still fragile. A commit creates a recoverable checkpoint. Staged changes do not.
For multi-file operations, frequent commits are not about pretty history โ they are about survival. A commit can be squashed later. Lost staged work cannot always be reconstructed cleanly.
A practical pattern:
git worktree is one of the best tools for reducing cross-terminal foot-guns. It allows multiple working trees from the same repository, each with its own checkout state. That separation matters because it reduces the chance that a command in one shell destroys the active state in another.
An illustrative pattern:
git worktree add ../rename-sandbox feature/rename-passNow the risky rename lives in its own workspace instead of sharing a working tree with unrelated terminal activity.
This is the blunt rule because it needs to be blunt. If one terminal is actively staging, editing, or reviewing uncommitted work, another terminal should not run destructive reset commands against that same working tree.
Safer alternatives for most "I just need to inspect or sync" moments:
| Goal | Risky move | Safer move |
|---|---|---|
| Check remote state | git reset --hard origin/main |
git fetch then inspect diffs/log |
| Clean up experimental work | Hard reset in shared tree | Use a separate git worktree |
| Save progress quickly | Leave changes staged only | Create a checkpoint commit |
| Compare branch states | Mutate current tree | Use git diff, git log, or another worktree |
Before any destructive git command, run this mental checklist:
git worktree be safer than mutating this directory?If any answer feels uncertain, stop.
TL;DR: A half-applied rename is not cosmetic technical debt โ when identifiers drive auth and routing, inconsistency becomes a security and reliability issue that must be validated end-to-end.
Developers often treat renames as low-drama maintenance work. That is true only when the name is decorative. Once a slug becomes an operational identifier, renaming it is closer to changing a contract than changing a label.
In many systems, slugs influence:
A partial rename can produce two dangerous illusions. First, the system may appear mostly functional because only some paths are broken. Second, the broken paths may cluster around low-frequency but high-impact operations such as auth resolution, background processing, or admin flows.
The recovery standard after this incident was stricter than visual inspection or a single green test run. The correct question was: does the new slug resolve canonically across every layer that matters, and has the old slug been removed from active execution paths?
That is the right standard for any destructive git recovery involving identifier changes. "Looks fine" is not a control.
git reset --hard resets the current branch reference, the index, and the working tree all at once. If changes are uncommitted, tracked edits disappear immediately. Most other git commands โ even potentially confusing ones like rebase or cherry-pick โ leave recoverable artifacts in the reflog. A hard reset of uncommitted work does not.
Sometimes, but not reliably. Git stores staged content as blob objects, and those blobs may persist in the object store temporarily. Tools like git fsck --lost-found can surface dangling blobs. However, reconstruction from loose objects is manual and fragile โ which is exactly why frequent commits are safer than treating the staging area as a checkpoint system.
Parallel terminals are not inherently bad, but they hide shared state. Two shells in the same repository both operate on one index and one working tree. A destructive command in one shell can invalidate assumptions in the other shell instantly, with no warning or confirmation prompt.
Use git worktree when multiple tasks need isolation at the filesystem level โ especially if one task involves risky cleanup, rebasing, or destructive git operations. Separate worktrees each have their own index and working tree, so a hard reset in one cannot affect another.
Verify more than compilation or a basic smoke test. Check route resolution, auth and credential lookup behavior, scheduled jobs, tests, and any automation keyed off the renamed identifier. The goal is end-to-end consistency, not superficial correctness.
git reset --hard is not a harmless cleanup command โ it can instantly destroy uncommitted work.git worktree is one of the simplest ways to isolate risky, long-running changes.The memorable part of this incident was not that a destructive command deleted work. Git has always been capable of that. The memorable part was how efficiently a single git reset --hard created the worst possible repository state: one that was neither clearly broken nor actually correct. In production-oriented agent systems, the dangerous failures are often the ones that preserve just enough surface normality to pass a quick glance. A half-applied rename is not a minor inconvenience โ it is a split-brain problem that demands end-to-end verification, not a spot check.
Discover more content: