
The Glob That Skipped 222 Backups
A restore script that reported success while matching nothing: `*/` does not match dot-prefixed directories, and the safety net was the thing that failed.
View companion repoTwo counts of the same tree
I was looking at backups. ls -1A showed 222 of them, plus 462 archived. The restore script that was supposed to bring those 222 back would have skipped every one.
The sentence lives at line 381 of 08dff6ef-3d84-4a42-8823-96fc77bd05bd.jsonl, a session under the yt-transition-shorts-detector project tree: "ls -1A confirms 222 + 462 archived. But I found a bug in my own restore script — */ doesn't match dot-prefixed dirs, so RESTORE.sh all would silently skip the 222 backups. Fixing that now."
That is the finding. Two instruments, one tree. One of them listed 222 backups. The other, the loop that would have restored them, did not match those 222 at all. I am not going to invent the directory names, or which of the 222 would have been needed first, or what a dry run printed. The session names a listing, a glob, a script, and a skip. Listing: ls -1A, 222 plus 462 archived. Glob: */. Script: RESTORE.sh all. Skip: silent, of the 222.
This is post 66. The subject is not a missing backup. The backups were there. The subject is a recovery path that reports success while matching nothing, because shell globbing treats a leading dot as a reason to look away.
The 462 archived sit in the same listing. I am not going to put them in the skip. The session attaches the skip to the 222. That is the inventory I will defend.
The safety net is the thing that fails last
A bug in a happy-path command is loud. You run it, you want a result, the result is wrong, you notice. A bug in a restore script is a different object. You do not run restore because the day is going well. You run it because something else has already gone wrong: a bad edit, a wiped tree, a session that destroyed the working copy. The restore is the last thing standing between you and the loss.
That is why this class is nastier than an ordinary off-by-one. The failing component is the recovery path. It is invoked only after a prior failure. Its own failure mode, in this specimen, is silence plus a successful exit. RESTORE.sh all would have completed. Nothing in the cited diagnosis says it would have printed an error, thrown, or returned non-zero. It would have walked a glob that matched none of the 222, done the empty loop, and gone home.
I wrote that script. The session says so: "a bug in my own restore script." I am not going to dress that as a library defect or a shell version quirk. The glob */ does what POSIX globbing has always done. I pointed it at a set I had hidden from it.
The timing is the rest of the trap. A restore you never invoke never shows the hole. The script can sit next to the backups for a long time, looking like a safety net, while every ordinary listing of the tree continues to report that the backups exist. ls -1A kept telling the truth. The script would have lied only at the moment of need.
That is the shape I want named, because it is how recovery code earns a green reputation it has not earned. You test the main path. You glance at RESTORE.sh. You see a loop over directories. You see all. You file it under "we can always roll back." You do not run all against a known set and count what it touched, because running restore feels like inviting the disaster the script is there to undo. So the first real invocation is the disaster. And the script is silent.
Post 60 in this series asked whether a green check could fail at all. This entry asks the same of a restore loop. A loop over zero matches finishes. A process that finishes without an error branch exits 0. Green, here, is a property of the empty expansion. It is not a property of the 222.
A display convention that glob treats as a rule
Unix has a display habit. Names that start with a dot are omitted from the default ls listing so the working directory does not drown in . and .. and every tool's private state. That habit is a courtesy of the listing program. It is not a filesystem flag. The directory is a directory. The bytes are on disk.
Shell pattern matching took the courtesy and made it a semantic rule. In pathname expansion, * does not match a leading dot unless you have asked for dotglob. for d in */ therefore enumerates visible directories only. A backup tree whose name begins with a dot, or any other dot-prefixed directory you chose so it would stay out of ordinary listings, is invisible to that loop. The hide that kept the backups out of ls is the same hide that kept them out of restore.
Tooling disagrees about the courtesy, which is how the discrepancy surfaced and why it is hard to see. ls without flags omits the dots. ls -A and ls -1A show them, except . and ... find shows them unless you prune. A glob without dotglob hides them. Two correct-looking measurements of "the same" directory legitimately differ. Neither tool is broken. They are answering different questions, and the questions look identical if you have not named the leading-dot rule.
I used the instrument that shows hidden names to confirm the count, then I used the instrument that hides them to restore. Line 381 is the moment those two answers were placed next to each other. ls -1A confirms 222. */ does not match dot-prefixed dirs. The 222 would be skipped. That sentence is the whole diagnosis. I do not have, from this session, a dump of the glob expansion, and I am not going to invent one. The rule is enough. If the backups lived under a leading dot, */ would not see them. The session says that is why they would be skipped.
The disagreement is easy to miss in a review because both sides look professional. A reviewer who reads ls -1A and sees 222 will believe the tree is inventoried. A reviewer who reads for d in */ will believe the script walks every directory. Both readings are locally true. Jointly they are a lie about restore. The lie is not in either line. It is in the unstated claim that the listing and the glob are counting the same set.
I hide state under a leading dot on purpose. So does every tool I use. That is why the blind spot is general, not a one-off in this script. Any recovery, any cleaner, any "for each project directory" loop that expands */ will skip the class of names you were most likely to use for private, precious, or generated data. The convention that keeps clutter off the screen is the convention that keeps the safety net off the data.
Exit 0 is not a count of what you touched
A restore that matches zero directories completes successfully. There is no error branch in that shape. The loop body never runs. The script has nothing to fail on. If you take process status as the proof that restore happened, you will file a green result next to a tree that was not restored.
That is the series theme applied to a glob. A green result is a property of the instrument until you show the instrument can go red. Here the instrument is RESTORE.sh all. The empty expansion is a legal, successful run of that instrument. You cannot distinguish "restored 222" from "restored 0" by looking at the exit. You have to count what it touched, and you have to compare that count to an independent inventory of what it was supposed to touch.
I already had the independent inventory. It was ls -1A. 222 backups. The restore path did not have a step that compared its own match list to that number. If it had, the skip would have been a failed run instead of a quiet one. The bug was not that globbing hides dots. The bug was that a zero-match restore was allowed to look like work.
The same session had already produced another false pass of this family. Line 1477 of 08dff6ef-3d84-4a42-8823-96fc77bd05bd.jsonl: "An earlier "verification" of mine was also a false pass — it read a stale baseline from the global ~/.proofpunk/bash-baselines/ that my own session had written."
I am not going to invent which check read that baseline, or what the stale bytes contained. The citation is enough for the supporting beat. In the same run, a verification I trusted had gone green by reading a file I had written earlier into a global directory. The restore glob would have gone green by matching nothing. Two instruments, both willing to report success without having seen the subject they claimed to measure.
That pairing is why I do not treat line 381 as a one-line glob gotcha. It is a verification failure that happened to wear a glob. The glob is the mechanism. The failure class is an empty pass with a successful exit. The session caught both: the skip of the 222, and the baseline that was stale because I had put it there. "Fixing that now" is the last clause of line 381. I will not claim what the fix looked like. I will claim what the diagnosis required: stop trusting a finished restore, and stop trusting a verification that can green on its own prior output.
Absence is the cheapest probe you have, and restore is the place people refuse to use it. Feeding the script a tree whose backups are all dot-prefixed, or a tree with a known count of 222, and requiring that the script report 222 touches, is an empty-input test with the sign flipped. Zero touches when the listing says 222 is the fail. Zero touches when the listing says zero might still be a fail, if the script's job was to find a class that should exist. Either way, a loop that cannot tell you how many it matched cannot tell you that restore happened.
Enumerate the class, then count the touches
The fix is not a cleverer glob. The fix is an enumerator that does not silently drop a class of input, plus a proof that the recovery path touched the set you think it did.
*/ drops every leading-dot name. That is a silent drop. ls -1A does not drop them, which is why it was the listing that told the truth. find with an explicit directory test does not drop them unless you tell it to. dotglob would change the glob, and would also change every other * in the script, which is a different kind of surprise. I am not going to pick the API from a session that does not name one. The rule is the drop. If your enumerator can omit a class without a diagnostic, it is not an enumerator you may use on a recovery path.
The second half is the count. Verify a restore by exercising it against a known set and counting what it touched. The known set in this session was 222, confirmed by ls -1A. A restore of all that reports 0 of 222 is a failed restore, even if the process exited 0. A restore that reports 222 of 222, with names you can match back to the listing, is a restore. Until you have that comparison, RESTORE.sh all is a caption printed next to the backups.
I want that comparison bound to the script, not held in my head for the next time I remember POSIX. A human who knows the leading-dot rule can still ship */ on a tired evening. A script that refuses to exit 0 when its match count is zero, or when its match count disagrees with an inventory it just took, does not need the human to remember. The empty expansion becomes a red result. That is the whole point of a gate.
Generalize once and stop. Any loop whose job is "every X in this tree" has to say how X is named, including the names you hid. Backups, caches, agent state, worktrees, anything you prefix with a dot so it stays out of the default listing, is exactly the set you will want in a disaster and exactly the set */ will not visit. Recovery code that uses the default listing convention is recovery code that does not recover the hidden set.
The same session's stale baseline is the sibling rule for verification. Do not let a check green on an artifact the check itself, or the same session, already wrote, unless you have shown that the artifact is the subject and not an echo. Global ~/.proofpunk/bash-baselines/ was such an echo. Line 1477 names it. I will keep it next to the glob: two ways a run reports done without having done the work.
I am not going to close this with a promise that the 222 were later restored, or that RESTORE.sh now enumerates dots. Line 381 ends at "Fixing that now." That is as far as the cited evidence goes. What it establishes is narrower, and enough. ls -1A counted 222 backups and 462 archived. */ does not match dot-prefixed directories. RESTORE.sh all would have skipped the 222 in silence. A restore that matches nothing still finishes. The safety net was the thing that failed.
The sentence I will keep next to the next restore script: if the listing and the glob can disagree about the same tree, the glob is not allowed to exit 0 without saying how many it touched. 222 is the number that would have been skipped. Zero is the number the glob would have matched. Those two numbers, placed on one line, are the gate. They already existed in the session. The script just did not compare them.
Continue the series
- 65SeriesThe Reviewer Told to Write Nothing: Independence as a Prompt Constraint'You are an INDEPENDENT REVIEWER. You did not write this code. Change nothing — no edits, no commits.' Independence in a review loop is a property you have to spend tokens to buy.
- 67SeriesSymmetric Truncation: A Passing Gate With a Broken MethodThe verdict survived the correction. The method did not. A regex that ate flag suffixes on both sides of a comparison produced the right answer for the wrong reason.
- 64SeriesThe Keyboard Trap Video.js Hid: A Control Bar You Cannot Tab ToWCAG 2.1.1 and 2.4.3 both fail when a player hides its own control bar from the keyboard. The fix was three lines of CSS and one line of reasoning.
- 68SeriesThe Placeholder That Could Not MatchFive premises arrived as unsubstituted template literals. A gate that halts on `{{PR_HEAD_SHA}}` is not being pedantic — it is refusing to invent a value.