2026-07-22 Stale-Path Audit and the Test Gate That Ran One Test

What I set out to do

After fixing the scratch-popup 127 in 2026-07-22 Tmux Picker Single-Click and the scratch-popup 127, sweep the config for other instances of the same bug: a hardcoded path into a location Nix no longer populates.

What I actually did

Swept seven surfaces: $XDG_*/$HOME literals in runtime code (21 paths), @placeholder@ tokens against their Nix substitutions (23 tokens), script-to-script call sites, Claude settings.json hooks (10), tmux.conf binaries (14), launchd agents (10), MCP server commands (13). The scratch-popup bug was the only instance.

The near-miss worth recording: scripts/update calls update-changelogs by bare name, guarded with || true, so a 127 there would vanish without trace. It’s safe only because updateChangelogsBin sits in update’s runtimeInputs. Remove that one line and the failure is invisible — precisely the shape reference_nix_script_runtime_deps warns about.

Two pieces of cruft fell out. claude-opsclaude_installed check advised installing via npm under ~/.local/share/npm, but claude.nix uses the native installer at ~/.local/bin/claude; following the fix advice would have produced a second unmanaged copy. Repointed at hm switch.

The bigger one was an accident. Running the claude-ops suite to validate that one-line change, just test reported “1 passed, 483 deselected.” The recipe is pytest -m unit, and exactly one of eleven files under tests/unit/ carried @pytest.mark.unit. So the gate had been running 1 test of 467 and reporting 26% coverage against a documented >90% target — while exiting 0 the whole time. Fixed by deriving the marker from the test’s directory in pytest_collection_modifyitems, with a test per directory asserting its own marker. just test went from 1 test / 26% to 467 tests / 92%.

Then chased /Users/aman, the previous account name, after spotting it embedded in a stale Chrome launchd plist. It is not a real user and has no home directory: it is a former home path left behind by a machine migration.

What was striking

The audit I set out to run found one bug. The incidental command I ran to validate an unrelated one-line fix found a much worse one. A green test suite that silently runs 0.2% of itself is strictly more dangerous than a red one, and nothing about just check passing would ever have surfaced it. Worth generalizing: assertions about coverage and suite size belong in the gate, because a gate cannot report its own absence.

Second: the /Users/aman residue is a good illustration of blast radius. The dotfiles repo has zero references — the declarative config is genuinely clean. Every hit is app-owned state (17 files in Library/Preferences, Slack/OBS/Sublime under Application Support, three history files under ~/.local/share). Declarative configuration didn’t prevent the migration cruft, but it did confine it entirely to the parts of the system that aren’t declared.

Cleanup landed

Pruned the four items worth touching: the dead Chrome launchd plist, zsh/chpwd-recent-dirs (both entries stale, so removed outright — cdr recreates it), nvim/.netrwhist (rewritten with its one valid entry, renumbered so dirhistcnt stays coherent), and ~/.local/share/CMakeTools/ (2023 VS Code extension leftovers, with cmake not even installed). Left every Library/Preferences and Application Support hit alone: app-owned state that gets rewritten or ignored, where editing macOS plists is risk without benefit.

Worth noting the first rm was refused by the permission classifier and only went through after explicit re-authorization. The right response to that was to stop and surface it rather than route around it — and the pause was useful anyway, because re-reading the targets first caught that chpwd-recent-dirs was 2/2 stale rather than the 1/2 my grep had reported. The character class [/"$ ] never matched a path ending in a quote. A count is only as trustworthy as the pattern that produced it.

Application Support: 847 files, and why most of them stayed

Extending the cleanup into ~/Library/Application Support turned up 847 files containing /Users/aman, not the ~20 the truncated sample had suggested. ~/Library/Logs had zero.

The important realization was that containing the string does not make a file cruft. In Firefox/places.sqlite it is a history row; deleting that file to remove a stale string would destroy every bookmark. Same for Chrome’s History, Discord’s Local Storage, the OBS scene collection, and Quick Look/cloudthumbnails.db-wal, which is an active SQLite WAL where deletion under a live database risks corruption. So I split the set: 365 genuinely regenerable files (103M of caches, __pycache__ bytecode, OBS logs from 2021, leveldb LOG/LOG.old) deleted, and every live database left alone. I verified the leveldb LOG files first — they are plain-text operation logs, not the data, which lives in .ldb/MANIFEST-*.

The one real bug in the pile was Code/User/settings.json, which still pointed autoDocstring.customTemplatePath and python.defaultInterpreterPath at dead /Users/aman paths. Unlike the rest, that is live config quietly misdirecting a running app. Removed both keys, keeping docstringFormat: numpy; verified with a jq key diff that nothing else moved.

The VS Code error

I told the user VS Code was not installed, and deleted nothing on that basis but did propose treating 2.1G of state as orphaned. It was wrong. VS Code lives in ~/Applications, not /Applications, and my check was ls -d /Applications/*Code*.app ~/Applications/*Code*.app — zsh aborted on the first non-matching glob before ever evaluating the second, and I read “no matches found” as “not installed.”

That is the third instance today of the same root error, after the empty pty harness and the chpwd-recent-dirs miscount: I trusted an output without asking whether the query could have produced the answer I wanted. A null result deserves a control case before it becomes a conclusion. Concretely, for existence checks: use mdfind/command -v rather than a glob, and never let a single shell command’s failure mode stand in for absence. Sharpens feedback_verify_the_question_not_just_the_answer.

Follows 2026-07-22 Tmux Picker Single-Click and the scratch-popup 127. Same lesson family as reference_nix_script_runtime_deps and feedback_verify_tool_output.