recovering-a-clobbered-file
SkillFiles & storageUse when a file may have been overwritten or truncated by a whole-file write, a shell redirect or an editor write: finding which revision holds the pre-write content, reading the tells that it happened, restoring it, and proving the restore reinstated only what was lost. Also when judging whether a green build says anything about a changed config file.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the recovering-a-clobbered-file skill
What this skill tells your AI
The instructions your AI receives, as published by nextlyhq/nextly in .claude/skills/recovering-a-clobbered-file/SKILL.md and read by ahel’s review.
First, name the BASELINE — everything below compares against it
Every check in this rule asks one question: what was at this path before the write. So the baseline is chosen once, and each command follows from it. Getting this wrong is not a smaller version of the same answer — it silently swaps which content is being judged.
The question is not "which of these two is it". Any of the last commit, the index, a retained stash, or nothing in git at all can hold that content — an untracked or ignored file was never in either, a stash holds edits that were reapplied and then clobbered, and edits that were only ever in the working tree are in none of them. Enumerating the cases cannot be complete, because which revision holds the content depends on how the work reached the tree; what settles it is naming the revision and CHECKING it holds what you expect:
-
git show <rev>:<path>prints a candidate's copy —HEAD, a stash, any commit — andgit show :<path>prints the index copy. Read the one you intend to restore from BEFORE restoring, and confirm it contains the pre-write content rather than assuming it does. -
A command that prints nothing has not shown you the file is absent. It is equally the shape of asking the wrong question, and two ways of doing that are easy to hit here.
<rev>:<path>names a path inside the revision's TREE, so the absolute path a symlink resolver just produced is not a valid argument and every candidate reads as missing; convert to repository-relative first. And a path that was untracked when it was stashed is not in the stash commit at all —git stash -uputs it in the stash's THIRD parent, reachable asstash@{n}^3:<path>— so the obvious query says absent while git holds an exact copy. -
The reading commands follow from that choice. Against a commit,
git diff <rev> -- <path>; against the index, the baregit diff -- <path>, which is working tree versus index. Note thatgit statusshows onlyMMand reveals neither. -
Those forms only compare TRACKED content. If the recovered path is untracked — the
stash@{n}^3case above is the common one —git diff <rev>reports the baseline as deleted and says nothing about what the file now holds, whatever that is. Measured: with the original in the stash and replacement text on disk, it prints-ORIGINALand no+line at all, so a clobber reads as a plain deletion. Materialise the candidate into a file created for the purpose, and compare withgit diff --no-index, which reads both sides from the filesystem and shows-ORIGINAL +CLOBBERED:base="$(mktemp)" && git show <rev>:<path> > "$base" && git diff --no-index "$base" <path>mktemprather than a fixed name, and one&&chain rather than separate steps, for the reason this whole rule exists:> /tmp/basetruncates whatever is already at that path — through a symlink included — and two recoveries at once overwrite each other's baseline. Chaining also stops a failedgit showleaving an empty file that the comparison then reads as recovered content.
Before concluding git holds nothing, look at the objects no ref points at.
Content staged and then reset out of the index is unreachable rather than gone:
git fsck --unreachable lists the blob and git cat-file -p <sha> prints it.
Measured — a file staged with deliberate content, unstaged, then clobbered is
absent from git show :<path> and recovered verbatim from its unreachable blob.
That is the last place to look, and no <rev>:<path> form above reaches it.
If no revision and no unreachable object holds it — and you have established that rather than inferred it from an empty result — stop. Say so and look outside git — editor local history, a backup, an open buffer. A recovery that cannot recover is worth naming as one; running the steps anyway converts a known loss into an unnoticed one, because the resulting diff looks entirely clean.
Both failure directions are live, which is why this is not a detail. Comparing
against HEAD when the index is the baseline reports the staged additions as
though they were your edit — and, worse, a clobber that preserved everything in
HEAD while destroying the staged lines shows additions ONLY, clearing the
overwrite that just happened. Comparing against the index when HEAD is the
baseline misses anything already staged.
Three tells that this has happened, in the order they appear
They are worth knowing individually, because each looks like good news:
-
The diffstat shows deletions on a file you believe you are creating, read against the baseline named above. A created file has no deleted lines, so one
-in its++---bar settles it — but only when the comparison starts from before the file existed.Read against the wrong baseline it answers a different question, and a new path that was STAGED is the case that makes this concrete. The index holds its pre-write content and
HEADdoes not, so the index IS the baseline — the baregit diff --statis the right command, and it correctly shows deletions when that staged content was clobbered. Reaching forHEADthere compares against a revision where the path does not exist at all, which reports additions only and clears the overwrite. The baseline section decides this; the tell just uses what it decided. This is the cheapest tell and the one most easily read past, because by then the write has already succeeded and attention has moved on. -
A metric improves far more than the change should explain. "1264 inputs became 119" was recorded as evidence that the new scoping was tight. It was evidence that inputs had been REMOVED. A number that moves an order of magnitude in the direction you were hoping for is the moment to ask which change produced it, not to write it down as a result.
-
Content you did not write, and did not mean to remove, is gone. Conclusive, and it has to be stated as the disappearance rather than as the suspicion that led there.
Two of these three settle it and one does not, so the split is worth stating plainly rather than by rank: a deletion against the correctly chosen baseline (1) and vanished content (3) are each conclusive on their own; the metric (2) is only ever a prompt to go and look, because a number can move for reasons that have nothing to do with a write.
What led there in the real case was noticing that the replaced file already used
$TURBO_EXTENDS$— the mechanism the new comment introduced as if it were absent. That is a good prompt to go and look, and it is NOT evidence: adding$TURBO_EXTENDS$totest:coveragewhentestalready uses it means the file was also "already doing the thing", with nothing overwritten at all. A tell that fires there sends a correct additive edit into a destructive recovery procedure, which is worse than the miss it was guarding.So confirm it against the baseline named above — the
--stat, then the hunks — and require content that predates your edit to have vanished.
Configuration coverage is UNEVEN, so find out before trusting green
The clobber survived lint, check-types and the full unit suite. The reason
is narrower than "configuration is untested", and the narrow version is the
useful one, because the broad version is false: nine suites in this repo read
build configuration and assert on it. packages/blocks-engine/src/typecheck-config.test.ts
parses tsconfig.json, tsconfig.tests.json and package.json and pins exact
settings; packages/builder/src/layering.test.ts parses vitest.config.ts and
asserts its include. For a property one of those covers, a green run IS
corroboration.
turbo.json has no such suite, which is why this one went through. So the
question to answer before reading green as evidence is not "is this a config
file" but "does anything read the property I just changed" — and the answer
varies by file, by package, and by which field inside it.
The consequence of getting that wrong is delayed and looks unrelated to the
diff. A dropped inputs entry means the hash no longer moves when that input
changes, so turbo replays a cached pass over code nothing ran against. A dropped
dependsOn edge means two tasks that were ordered may now be scheduled
together. Neither fails at the moment of the edit, and by the time either
surfaces the diff that caused it mentions none of the symptoms.
$TURBO_EXTENDS$ means the package config ADDS
Two package configs use it — packages/ui/turbo.json and
packages/blocks-react/turbo.json — and both rely on the property:
$TURBO_EXTENDS$ inside dependsOn or inputs interpolates the ROOT task's
list at that position. ["$TURBO_EXTENDS$", "build"] is "everything the root
task depends on, plus this package's own build".
So a package task list is an APPENDIX, never a replacement, and writing one from scratch is a silent subtraction: the file still parses, turbo still runs, and the inherited entries are simply not there. To add an input, append to the existing array. If you find yourself composing the whole array, you are about to drop whatever the root supplies.
Restoring, and proving the restore
Recovery is deliberately not a procedure here. Which revision holds a given pre-write state depends on how the work reached the tree — committed, staged, stashed, or never recorded — and a manual that covers most of those and is silent about the rest reads as complete, which is worse than being brief. Three things are worth stating; the rest is the situation in front of you.
1. Identify which revision actually holds the pre-write content, and read it, before running anything. That is the baseline question above, including its stop condition. No command is right by default.
2. The restore does not carry your edit. Re-apply it on top as the delta it actually was — an append only where the edit was additive; for a removal or a replacement, appending duplicates a setting or reinstates content meant to go.
3. Prove it BEFORE submitting, against the same baseline. Expect only the
edit you meant. This step is not optional, because the clobber and the restore
both live inside one PR: the branch diffstat nets out and reads as though
nothing happened, and that summary is exactly what hides the loss. The
merge-commit check in the verifying-merged-work skill is a separate, post-merge
confirmation — by the time it can run, a broken config has already merged.
Two commands are worth knowing because their names mislead: git checkout -- <path> restores from the INDEX rather than a commit, and
git checkout origin/main -- <path> answers "what does main have", which is a
different question from "what was here before my write" and quietly imports
upstream changes when main has moved.
Signals
- GitHub stars
- 57
- Forks
- 6
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
recovering-a-clobbered-file- Source
- github.com/nextlyhq/nextly