audit-core.sh §5d — a gate for the pipefail + SIGPIPE trap this repo keeps hitting. Under set -o pipefail, piping into a reader that exits early turns a success into a failure: grep -q stops on its first match, awk on its exit, head after N lines, the writer takes EPIPE and dies with 141, and pipefail reports the pipeline as failed even though the reader matched. Three occurrences so far. Two were found and fixed by hand — a 4000-line git show into grep -q reporting "no heading" on a file that had one, and ldd --version | grep -qi musl reading false on every musl box, whose assertion is still named "the pipefail trap this repo has hit before". The third broke main: nvim-reachability.sh invented two orphans because a visited module's lookup returned 141. Each fix included a sweep of the tree, correct at the time and unable to cover code written afterwards. The gate is scoped to a shell-string producer (printf/echo) feeding an early-exiting reader, in files that actually set -o pipefail. That shape converts to a herestring with no behavioural difference and no reason to prefer the pipe, so a finding is never a judgement call. sed <file> | head -n1 — a file producer, ~15 instances — is deliberately out of scope: converting those is not free, and a gate that fires fifteen times on working code is a gate someone turns off. Four existing instances converted (check-modern.sh, parity-check.sh, test-core.sh ×2). All fed small values and none was a live bug — which is precisely why a hand sweep leaves them, and why the next author copies the shape somewhere the producer is large. The scanner lives in scripts/lib/common.sh as _core_pipefail_hits, beside _core_fail_digest and for the same reason: so test-core.sh can drive it on fixtures. A gate for a bug that has recurred three times is only worth having if it demonstrably fires, and probe-testing caught a defect in this one before it shipped — it used to scan any file that merely mentioned pipefail in a comment, which is the false-positive class that gets a gate switched off. Eight assertions pin both halves: the three banned reader forms are caught, and the herestring fix, a comment describing the hazard, a file with no pipefail, a file producer, and the library's own definition are all left alone.
What’s new
One reverse-chronological feed across every repo, parsed straight from eachCHANGELOG.md at build time — not a hand-kept mirror. Each change is classified so you can filter to perf, config, orsecurity at a glance. ACorechange fans out to every OS repo on the next git subtree pull.
audit-core.sh §4b — the nvim orphan backstop core.manifest claimed already existed. core.manifest lists nvim/ as a directory rather than per-file, because a vendored lazy.nvim tree churns wholesale and per-file listing would be noise. The stated justification was that "verify-core.sh (byte-for-byte vs upstream) is the orphan backstop here instead" — but verify-core.sh has never existed in this repo. So from the day nvim/ went directory-granular, §1's manifest⇄fs check auto-listed every new path under it and nothing else looked: a lua module nothing loads could sit in the tree indefinitely and fan out to all eight OS repos, silently. luacheck does not help — it lints the files it is handed and does no reachability analysis. §4b walks the load graph instead — a real traversal, not an "is this name mentioned anywhere" scan. That distinction is the whole point: a mention-scan passes two dead modules that require each other (a disconnected cycle, non-zero indegree, reachable from nothing), and passes a module named only in another file's comment. Both are exactly the orphan this exists to catch. So it inventories every module, strips lua comments, reads each file's edges, then walks outward from the roots and flags every module never visited. Only real load expressions count as edges — the module name must be preceded by require (covering require("x"), require "x", and pcall(require, "x")) or by lazy's import =. Matching every quoted gerrrt.* string would still be a mention scan: health.lua deliberately peeks package.loaded["gerrrt.servers"] precisely so it does not load the registry, and counting that as an edge would let the whole servers/ arm look reachable from the health root even with every real require("gerrrt.servers") deleted. Comment stripping handles --[[ … ]] blocks across lines too, since a line-only stripper leaves the block interior searchable. Two roots, both genuine entry points rather than exemptions: nvim/init.lua, and gerrrt.health — which Neovim discovers by runtimepath for :checkhealth, so nothing requires it and nothing should. Two edges cannot be read literally from source and are resolved during the walk: a directory import (gerrrt.plugins names a directory, so a target with no file expands to its target.* children, as lazy does), and the dynamic require in servers/init.lua, which does pcall(require, "gerrrt.servers." .. name) over its servers list — so visiting gerrrt.servers expands to the listed names, that registry being the only static evidence those modules are wanted. The edge KIND is carried through the walk, because the two resolve differently at a missing target: import expands to children, but a require with no module behind it is a dangling require lua raises at runtime, and is reported. Treating every fileless target as a directory import meant require("gerrrt.utils") silently marked every gerrrt.utils.* child reachable. The inventory itself is validated, since the walk is only as sound as its name→file map: .init is stripped for a real */init.lua path only (doing it on the module string let a file named foo.init.lua masquerade as module foo), a dot inside a filename is reported as unaddressable (lua resolves gerrrt.a.b through a/b.lua, never a.b.lua), and two files claiming one module id fail — lua loads exactly one of them, so the other is dead config that would otherwise ride on its twin's reachability. The registry is also checked both ways, because a generic "unreachable" is a worse message than the truth: a module in no list entry is dead config; a list entry with no module file is a runtime load error servers/init.lua reports at startup. A missing or unparseable registry fails closed — silently skipping it would disable the entire servers/ arm, which is how this class of gap starts in the first place. Verified against planted fixtures for every class it claims to catch — orphaned utils/ module, stray top-level module, disconnected require cycle, comment-only mention, multiline block comment, package.loaded peek, require() of a directory, lazy import matching nothing, duplicate module id, unaddressable dotted filename, unlisted LSP module, registry entry with no file, unparseable registry, missing registry — plus the two exemptions (a false positive on health.lua or plugins/ would make the gate unusable). Each negative fixture asserts the finding text and exit status 1, so the documented CLI contract is covered too. The real tree is clean: all 97 lua modules under nvim/lua/gerrrt/ are reachable today. That module set is the gate's scope — nvim/init.lua is the entry point it walks from rather than a vertex, and lazy-lock.json and .luacheckrc are not lua modules at all. bash 3.2 safe, so the macos-latest CI leg runs it. Closes the nvim/ half of #454.
lib/bootstrap-lib.sh now owns privilege escalation, so a bootstrap stops hard-coding sudo. blib_resolve_su [--require] resolves the escalator once into BLIB_SU (an explicitly set value always wins, including an empty one; else root needs nothing, else sudo, else doas), and blib_priv is the public way to run a privileged command. Every OS bootstrap wrote sudo inline at roughly a dozen call sites, which is wrong on precisely the machines a bootstrap meets first: fedora:latest and alpine:3.20 ship no sudo, and neither does a WSL distro's first boot (root, before /etc/wsl.conf installs the default user) or a minimal Server image. Those runs died at the first package-manager line with sudo: command not found — exit 127 under set -e, before doing anything at all. It is also why bootstrap-test.yml can exercise only --links-only, and must pass BLIB_SU= to manage even that. --require makes "not root and no escalator" a hard error for a provisioning run, while a links-only run correctly continues (wiring symlinks needs no privileges).
blib_sudo_keepalive_start / blib_sudo_keepalive_stop — no more invisible password prompts. A bootstrap's privileged calls are interleaved with from-source cargo/go builds that take minutes, comfortably outliving sudo's 5-minute timestamp. sudo writes its prompt to stderr and reads from the TTY, so a later call whose stderr is redirected (>/dev/null 2>&1, ubiquitous in these scripts) stopped dead at a prompt nobody could see: no output, no progress, indistinguishable from a hang, and reproducible only on a box slow enough to cross the timeout. Prime once up front, refresh in the background, and return non-zero if that first authentication fails so the caller can abort before half-provisioning. A no-op under doas (no refreshable timestamp) and as root.
blib_user_bindirs_on_path — stop the presence guards lying. A bootstrap's command -v <tool> guards decide whether to spend minutes building from source, but they are answered by the PATH of whatever shell launched the bootstrap — on a fresh box, bash. cargo install writes ~/.cargo/bin and go install writes $GOBIN (~/.local/bin by convention here), while ~/.cargo/bin reaches PATH only via the OS zsh layer — i.e. only inside a Core shell that does not exist yet. So every guard reported "missing" and every re-run rebuilt the entire from-source tool set. Adds only directories that exist, and never twice.
blib_note_fail / blib_failed_count / blib_failures_report — a half-provisioned box now says so. A bootstrap is full of steps that must not abort the run (a COPR that is down, a rate-limited API, a crate that fails to build), so each is written || true — and the script then printed "bootstrap complete" and exited 0 regardless, making a box that got none of its extra tooling indistinguishable from a good one, to CI and operator alike. blib_failures_report returns non-zero when anything was recorded, which is the contract a caller maps onto its own --strict flag. All four are covered by a new hermetic section in scripts/test-core.sh (no package manager, no network, no privileges), including the bash 3.2 set -u empty-array rule that would otherwise crash the report on the happy path.
PORTABILITY.md — how to write Core that survives the fan-out. The rules were real and consistently followed, but recorded only in ~8 scattered code comments, so they were unteachable to a new contributor and unenforced for new files. That is the likely root cause of the Homebrew paths that sat in maint/ and tmux/scripts/. It documents the bash 3.2 floor (with the banned constructs and their portable forms), the BSD/busybox coreutils traps, the shim pattern with the full inventory of shipped shims, what to do when a capability genuinely cannot be probed, and why the have() probe is redefined per loading context on purpose.
VENDORING.md — the same contract from an OS repo's side. Previously scattered across ARCHITECTURE.md, RELEASE-RUNBOOK.md and a source comment, so a downstream maintainer had no single answer to: which core/ paths may I touch, what does core.lock mean, which number band may I claim, how do I upstream a fix. Includes the footgun that was documented only in zsh/loader.zsh — a fragment dropped in a gap in the Core band (say 22-foo.zsh) is gated as Core and silently vanishes under CORE_PROFILE=minimal.
CODE_OF_CONDUCT.md — the one standard community-health file that was missing while the README actively solicits contributions.
Core now performs a real bootstrap link run in its own suite. bootstrap-test.yml asserts the symlink graph, but it is workflow_call-only and dotfiles-core ships no bootstrap.sh — so it only ever runs from the eight OS repos. Core unit-tested the blib_* helpers and never linked anything, which meant a bootstrap-lib.sh regression was caught downstream, in eight repos, instead of here. Seven assertions now link the actual Core tree into a sandbox $HOME/$XDG_CONFIG_HOME and check the graph a consumer depends on: every numbered fragment lands flat in $ZSH_CFG (the load-order contract loader.zsh globs), loader.zsh itself is linked, nvim/ resolves as a directory symlink, tmux/starship/lazygit/jj/gitconfig/vimrc land at their promised destinations, clip and clip-paste are executable on ~/.local/bin, the seeded files (local.gitconfig, sesh.toml) are real copies rather than symlinks — a symlink there would track a user's git identity back into Core — and a second pass is a no-op that backs nothing up. Hermetic: the tpm directory is pre-seeded so the one network call in the function is skipped.
scripts/sync-core.sh has tests. The highest-blast-radius script in the repo — it gates on the audit, git subtree pulls into eight working trees, and stamps core.lock — had no coverage at all. Its only proof was sync-fanout.yml running it for real against the live fleet, i.e. the fleet was the test. Twelve assertions on hermetic fixtures (a miniature of the real topology: a vendored origin, a local checkout, and a throwaway fleet, with audit-core.sh stubbed so the gate can be driven red and green in-process). Every case is a refusal or an idempotency property, because that is how this script fails: a broken guard does not throw, it fans a bad tree out to eight repos and reports success. Covered: a red audit refuses the fan-out and refuses before mutating anything; local HEAD ≠ remote tip refuses (what you audited is not what would vendor); an uncloned repo and a core/-less repo are skipped, not failed; dotfiles-Windows appears in neither the fleet file nor the fallback array; --dry-run prints the plan and commits nothing; core.lock lands at the repo root with the full sha, version and branch; the tree is clean afterwards so the next run is not self-blocked; re-syncing an unchanged sha manufactures no commit; and a dirty target is refused, counted failed, and does not abandon the repos after it.
A release tag can no longer exist before its commit is on main. make tag used to commit and tag in one step, leaving a local vX.Y.Z on a commit that was not yet merged. That window is not closable by discipline: --no-follow-tags governs your push, while the tag lives in shared .git state any other process can push. It happened. During the v4.11.0 cut a concurrent session pushed its own branch with push.followTags set, carried the release tag to origin, and fired release.yml and sync-fanout.yml against an unmerged commit — publishing a Release and opening eight vendor PRs across the fleet against a commit that was never on main. Nothing merged, because sync-fanout opens PRs and never merges them, but the number had to be retired: release tags are immutable by ruleset, so v4.11.0 could not be re-pointed. The invariant is structural now, not procedural — a vX.Y.Z tag only ever exists on a commit already on origin/main. make tag commits and creates no tag at all, so a stray push has nothing to carry. make publish runs after the PR merges and refuses unless origin/main actually carries this core.version, then tags origin/main and pushes. --push is withdrawn — its whole semantic was the hazard — and fails with a pointer to --publish. Phase 2 tags the release commit, not origin/main's tip. core.version does not change again until the next release, so "the tip carries this version" stays true for every commit that lands afterwards — tagging the tip would sweep work still under [Unreleased] into the release, and release.yml builds the Release body from the [vX.Y.Z] section, so that work would ship undescribed. It resolves the commit that set core.version to this value and tags that, reporting when the tip has moved on. It also validates that commit's [vX.Y.Z] section before creating any tag — that it exists and is non-empty, using release.yml's own awk so the two cannot disagree about what empty means. release.yml builds the Release body from that section and rejects an empty one, and release.sh will promote an empty [Unreleased] without complaint; publishing first and discovering either afterwards leaves an immutable tag on a release that cannot be published, burning the version for a reason knowable up front. make release's printed recipe is updated to match. It still ended with git tag -a + git push --tags, so an operator following the output it generates would have recreated exactly the pre-merge tag this change exists to eliminate. Both refs go up in a single --atomic push, with a --force-with-lease on the vN alias. Pushed separately they can half-land: vX.Y.Z published while vN is stale fires the workflows against a stale alias, and a re-run then refuses because the immutable tag already exists. The lease rejects the push if another publisher moved vN after this run read it, and an ancestry check covers the gap before that read: whatever vN points at must be an ancestor of the commit being tagged, so the alias can only ever move forward. Both are needed — a publisher finishing before the read is seen as this run's own expected value, so the lease alone would be satisfied while vN rolled backward. This also makes the merge method irrelevant to the tag. Eleven behavioural assertions cover it — including that the tag does not follow a tip that advanced after the release merged; the script previously had none, which is how the ordering survived.
The audit now names the behavioural assertion that failed, in the failure line itself. It said behavioral tests failed — run: ./scripts/test-core.sh, which sends the operator away to reproduce a result the run already had. For an intermittent failure that is advice that cannot be taken: the re-run passes and the evidence is gone. That is not hypothetical — it cost two occurrences of an unattributed flake, both lost because the ✗ scrolled past far above the summary and only the summary survived being piped through tail. The suite's output is already buffered for the background run, so the names cost one grep and travel wherever the fail line travels: the summary block, --json, the CI job log, a truncated paste in an issue. Not a CI annotation — fail() writes to stderr and ci.yml runs audit-core.sh directly with nothing emitting ::error::, and claiming a destination this does not reach would be the same overclaim the digest exists to prevent. The names are joined without rewriting the records, so a message containing a literal | (nine assertions do, 'exec … || exec …' cannot fall back among them) is not spaced out into false boundaries — two failures reading as four is worse than terse in the one line someone has when they cannot reproduce the failure. Up to three are named, then a count (+N more) rather than a silent truncation, because "one flaky assertion" and "the whole section is down" need telling apart before deciding to re-run or investigate. A run that exits non-zero having printed no ✗ at all — a crash, a kill, a timeout — now says that, instead of an empty list beside a red line. Matched after stripping SGR escapes rather than anchoring on a bare ✗: fail() prefixes the mark with $c_red, so an anchored match finds nothing whenever colour is on — a detector that would go quiet in exactly the runs someone is watching. The serial path (CORE_AUDIT_SERIAL=1) keeps the old line; its output is not captured, and piping it to capture would cost the live colour output that mode exists to give. The rendering lives in scripts/lib/common.sh as _core_fail_digest so the suite can test it, which matters more here than usual: every branch of it fails quietly, producing a plausible line that has silently lost the name — indistinguishable from the flake merely not being nameable. Proving those by making a real gate fail would mean recursively invoking the audit or hand-injecting a fault, and CI repeats neither, so five assertions drive it on fixtures instead: a coloured ✗ is still extracted, five failures render as three names plus a true total, exactly three grow no (+0 more) tail, a message carrying its own literal || survives verbatim rather than gaining false boundaries, and both a marker-less log and an unreadable file yield empty so a crash is never misreported as assertions. Confirmed as real regression tests by mutation — dropping the escape-strip makes the coloured case yield nothing, dropping the overflow notice reddens that case alone, and the pipe fixture fails against the join this entry replaces.
ARCHITECTURE.md now names Core's two deliberate exceptions instead of leaving them to be rediscovered as drift. zsh/55-maint.zsh was already excepted in writing at the gate; zsh/60-update.zsh — ~480 lines of seven-package-manager logic, including a Tumbleweed check to choose zypper dup over zypper up — was justified only in a code comment. The reasoning is sound (one verb, N backends, exactly like bin/clip) and now says so where the layering rule is stated.
The maint runner no longer names an OS prefix; the scheduler unit supplies the PATH. A scheduler starts the runner with a stripped environment, which is why the Homebrew prefixes were hardcoded. maint-install now captures the live PATH of the shell installing it and bakes it into the unit — Environment="PATH=…" (systemd), an EnvironmentVariables dict (launchd, XML-escaped), and an env-prefixed command (cron, POSIX single-quoted and then %-escaped, in that order — cron hands its command field to /bin/sh, so an unquoted or double-quoted value containing $(…) or a backtick would be evaluated on every scheduled run). Whatever prefix this OS uses is already correct in that PATH, so the OS supplies the truth and Core hardcodes nothing. The brew step is now gated on have brew alone. Action required on an existing schedule: a unit written before this change carries no PATH, so the runner falls back to the POSIX floor and the brew/mise steps skip silently — the job still succeeds while doing less. Re-run maint-install once. maint-status detects this and says so rather than leaving it to be noticed.
tmux-cheat.sh discovers a brew prefix instead of naming one — $HOMEBREW_PREFIX (exported by brew shellenv, so the tmux server usually carries it), falling back to brew --prefix. When neither resolves it adds nothing and takes the existing pager fallback: a missing tool degrades visibly, where a wrong absolute path was a silent lie on every non-brew machine.
A failing linter gate named itself and nothing else. Five sections — luacheck, shellcheck, markdownlint, actionlint, gitleaks — ran their tool with >/dev/null 2>&1 and reported a one-line verdict, so a red run said ✗ markdownlint reported issues with no rule, no file and no line. Each ended with a "run it yourself" hint, which is fine locally and useless in CI — the one place the tool is installed, the finding is already computed, and re-running it costs a push and a full CI cycle per guess. Diagnosing a single MD049 violation this way took three round-trips. Output is now captured and printed beneath the ✗, via a shared fail_detail in scripts/lib/common.sh: stderr (so --json keeps stdout parseable), indented (so it reads as detail, not as further findings), and capped at CORE_FAIL_DETAIL_LINES (40) so a pathological run cannot bury the summary it is meant to explain. gitleaks also gains -v --no-color, without which it prints only leaks found: N and the file/line/rule stay hidden — the same non-answer. Printing its report is safe precisely because --redact is already in use: the value is replaced with REDACTED, so the report names the file, line, rule and fingerprint without reproducing the secret.
core-doctor reported ✗ bat on Debian/Ubuntu/Kali for a tool that was installed and fully wired (#418). Those distros ship the binary as batcat; 00-tools.zsh resolves it into $BAT_BIN, and cat, catp, MANPAGER, the fzf file preview and fif's preview all ran on it. The report still called it absent — two lines above its own resolved section printing bat → batcat — and listed bat under "install missing", advising an install of something already present. The cause was an asymmetry between the only two renamed tools. 20-aliases.zsh gave fd an alias under its canonical name and bat none, so bat was untypeable by the name its README, man page and every upstream recipe use. bat now carries the matching alias bat="$BAT_BIN" (a no-op alias bat=bat where the name is already canonical; zsh does not re-expand an alias to its own name). That alias is not what makes the report honest, though it looks like it would: zsh's command -v resolves aliases, so fd's ✓ had been coming from the alias rather than from PATH all along. The doctor now resolves each row through a new _core_doctor_bin — one definition shared by the human render and --json, so they cannot drift — which maps fd/bat to $FD_BIN/$BAT_BIN and everything else to itself. Presence, the install-missing list and the JSON tools object all follow the real binary; the JSON keys stay canonical (.tools.bat, never .tools.batcat) for existing consumers. Resolving there also fixed a second defect the alias could never have reached. core-doctor -v forks "$tool" --version, and a parameter expansion is never alias-expanded — so on Debian the probe ran fd, hit command not found, and had the error swallowed by the pipeline: the row rendered as a bare, versionless ✓ fd. Both rows now fork the resolved binary and print their version. Five cases in scripts/test-core.sh pin it against a stubbed PATH (Debian names, canonical names, neither), with the doctor assertions deliberately run without 20-aliases.zsh loaded so a ✓ can only come from the resolver.
PORTING-MATRIX.md's carapace = go³ cells named an install path that cannot be followed on any platform. Footnote ³ promises go install where a tool is unpackaged, and the carapace row pointed openSUSE and Kali straight at it. That install cannot succeed for any published version, for two independent reasons: carapace-bin's go.mod carries replace directives (spf13/pflag, kevinburke/ssh_config), and go install pkg@version refuses any module that does; and the generated sources (pkg/{actions,conditions}/*_generated.go) are not committed, so even a plain go build on a clone fails until cmd/carapace/main.go's go:generate lines have run. Checked exhaustively rather than inferred from the current release: across all 184 tags from v0.0.3 (2020-08-31) to v1.7.3 (2026-06-30), 184 carry a replace directive and 0 commit the generated sources. That scope is the operative part — go install takes any @version, and pinning an older one fails identically. Nor is it a transient break to wait out: upstream's own .goreleaser.yml runs go generate ./cmd/... as a pre-build hook, and the AUR's from-source PKGBUILD does the same, so this is the intended build shape. The three cells now point at a new footnote ²⁷ carrying a route per target — the upstream .rpm for openSUSE (the block dotfiles-Fedora's bootstrap.sh already ships and has proven), the .deb for Kali/Debian, and the AUR carapace-bin for Arch (the prebuilt one; the AUR's bare carapace is a from-source, x86_64-only build). Alpine and Gentoo were already correct and are now documented as verified rather than merely unmarked. ²⁷ also records what the release-URL route costs — no repo is added, so nothing upgrades carapace afterwards — the unsigned-artifact wrinkle that makes zypper -n stricter than dnf here, and the source build as the escape hatch with its real binary size (81.6 MB released, ~114 MB unstripped). Footnote ³ gained a pointer so the general go install promise is not read back onto this row. dotfiles-Arch, dotfiles-Kali and dotfiles-openSUSE still make the impossible call in their bootstrap.sh, failing invisibly because _dotfiles_go_install discards the explanation; each is tracked in its own repo against this footnote. (PORTING-MATRIX.md)
The nvim reachability gate invented orphans on main. The membership lookups piped into an early-exiting reader — printf '%s\n' "$visited" | grep -qxF "$m" — while the script runs under set -o pipefail. grep -q exits on its first match, the writer takes EPIPE and dies with 141, and pipefail makes the pipeline non-zero even though the reader matched: a module that is visited reads as unvisited and is reported as an orphan, with printf: write error: Broken pipe captured as a finding alongside it. Timing-dependent — the writer must still be writing when the reader exits — so it passed every PR run and failed on the push to main. Measured on a large input, the piped form gave 20/20 false negatives and the herestring 0/20. Every lookup now feeds its input by herestring, awk … <<<"$mods" included, since awk's exit closes the pipe the same way.
blib_set_login_shell could throw away a complete, correct wiring over its last, purely cosmetic step. It runs at the very end of wire_links, after every symlink is already in place — but neither the /etc/shells append nor chsh tolerated failure, so under the caller's set -e a host with a read-only /etc (a container), a restricted chsh, or an LDAP/SSSD-backed account aborted the whole bootstrap. Worse, the operator saw a bare tee: /etc/shells: Permission denied and no indication of what had or had not been done. Both steps now warn and continue, naming the manual command to finish the job.
grep -q on a large piped producer read a match as a failure under pipefail. The new origin/main CHANGELOG guard piped a 4000-line file into grep -q, which exits the moment it matches — leaving git show to die of SIGPIPE, and set -o pipefail then surfaced git's 141 rather than grep's 0. The check reported "no heading" on a file that had one. Captured to a variable instead. Swept the rest of the tree for the same shape: the other instances pipe small printf/find output that fits the pipe buffer, so they never trip it, and the one borderline case in test-core.sh was made immune anyway.
The atuin autostart apparatus gate now tells a slow box apart from a broken detector. The gate proves the box can bind and connect an AF_UNIX socket with python3 alone, then runs a known-good stub and treats any verdict other than holds as a regression in verify-atuin-guard.sh — deliberately, because the obvious "skip unless it holds" form uses the code under test as its own apparatus check and would let a real regression skip every assertion below while leaving the audit green.
core-doctor no longer reports a false ○ (idle) for starship and carapace. _core_wired probed only starship_precmd and _carapace, but both tools renamed the functions their init emits — starship 1.24.2 emits prompt_starship_precmd and carapace-bin 1.5.7 emits _carapace_completer, and neither emits the old name at all. Since Core sources each tool's own init (_cache_eval starship starship init zsh), the probe silently went stale as the tools moved, so every box on a current starship or carapace saw ○ (idle) for an integration that was demonstrably driving the prompt and completion (measured: 1760 carapace-bridged commands, PROMPT set by starship). That is the exact failure the probe exists to prevent, inverted — a misleading ○ instead of a misleading ✓. Both arms now accept the old and current names, so boxes pinned to older releases keep reporting wired.
The atuin autostart apparatus gate no longer reds when the box is merely slow. The gate proves the box can bind and connect an AF_UNIX socket with python3 alone, then runs a known-good stub and treats any verdict other than holds as a regression in verify-atuin-guard.sh — deliberately, because the obvious "skip unless it holds" form uses the code under test as its own apparatus check, and would let a real regression skip every assertion below while leaving the audit green. The strictness was right; the deadline was not. §J4 runs at CORE_ATVERIFY_POLL=3, chosen so the many negative cases do not idle away a long bound — but for the one stub that is supposed to succeed at everything, that bound is not idle waiting, it is a deadline: 300ms for a spawned daemon to bind and answer. A loaded runner misses it, the verifier declines with "a daemon started by hand … never answered" exactly as designed, and the gate rendered that property of the box as a defect in the detector. It reddened an audit leg for a change that had nothing to do with atuin. The tempting repair — skip on unmeasurable — is wrong, and the reason is recorded in the code because it is easy to re-derive incorrectly: that verdict is the verifier's fail-closed answer for a family of causes, and most are deterministic and are the detector (a renamed or duplicated anchor, control-arm row accounting that no longer matches, and internal: no verdict was reached (this is a bug in verify-atuin-guard.sh)). Skipping on it would silence sixteen assertions while the subject announces its own bug, and no amount of retrying separates those from slowness, since every one of them repeats. So nothing skips. The known-good run simply gets a deadline with real headroom — 30 ticks instead of 3, plus one retry — while every negative case keeps the tight bound. A transient stall now has to land twice inside a 10x-wider window to be seen at all. Measured on the repo's own fixture with all four arms holding: 10.9s at 3 ticks, 14.0s at 30, 23.4s at 100 — so this costs about three seconds of wall clock, once per suite, and 30 rather than 100 because --premise autostart also spends the bound proving unreachability, which no amount of promptness shortens. The gate keeps the property that matters: it cannot go quiet, because every verdict other than holds still reddens it. The three failures are now told apart — moved (miscategorising correct behaviour), unmeasurable (declining where it should measure, carrying the verifier's own reason), and no parseable verdict at all (the apparatus failing to report, carrying stderr — the Alpine shape where a stray line merged into the JSON).
The atuin autostart suite no longer reports an unmeasurable run as an upstream finding. verify-atuin-guard.sh has three verdicts on purpose — holds, moved, and unmeasurable for "the apparatus could not be trusted, never a finding about upstream" — but the socket-only-stop assertion in scripts/test-core.sh compared against holds and swept everything else into a single else, so a declined run printed the exact claim the third verdict exists to prevent: that a zombie daemon had kept committing into later arms. It is the only arm in that section expecting the POSITIVE verdict from an otherwise well-behaved stub, so it alone inherits every environmental way a run can honestly decline. The section runs at CORE_ATVERIFY_POLL=3 — 300ms for the manual-spawn control's daemon to bind and answer — which a loaded box misses, yielding "a daemon started by hand never answered … An apparatus limit, not a finding" with nothing having survived. That reddened audit-alpine on an unrelated docs-only PR; a rerun of the identical commit went green. The three states are now distinguished: holds with no survivor passes, unmeasurable skips with the verifier's own reason surfaced, and moved fails as the real finding. A run that produces no parseable verdict at all is reported as its own outcome carrying stderr, rather than being read as moved — that shape has a history here, being how §J4 first went red on Alpine when a stray musl-side line merged into the JSON. The assertion does not go quiet in exchange — the survivor half is now checked unconditionally and stays a failure under any verdict, because a live daemon is a leak whether or not the run could measure. A contaminated control cannot hide behind the skip either: the opening control runs before any daemon exists, the spawn control while the socket is still present, and the closing drain control only after the owner pid is confirmed dead — so the zombie's rows have no route to unmeasurable, only to moved or a survivor.
A concurrent test run no longer fails the audit with a sandbox leak that never happened. verify-atuin-guard.sh --premise autostart built its sandbox at /tmp/atverify.XXXXXX, and the test-core.sh assertion that a completed run leaves no sandbox behind enumerated that prefix globally — snapshot before, snapshot after, anything new is a leak. /tmp has other writers, so a second suite running on the same box during the window was counted as the first run's leak. Two worktrees, two agents, or simply make audit in one terminal while make tag audits in another was enough. It cost a real make tag — leaked 1 new sandbox dir(s) on the repo's most consequential command, where the operator's natural next move is to re-run or reach for TAG_SKIP_AUDIT=1. A release gate that teaches the operator to skip it is worse than no gate. Sandboxes now carry a per-run tag — /tmp/atverify.<tag>.XXXXXX, from the new CORE_ATVERIFY_TAG — and the assertion globs only its own. The tag defaults to the script's pid, so make verify-atuin-guard and atuin-guard-verify.yml pass nothing and still get a prefix no concurrent run can collide with; two live processes cannot share a pid. It is validated as 1-16 characters of [A-Za-z0-9_-] and rejected rather than sanitized, because it becomes a path component and a caller that globs its own tag needs the tag it passed. The cap is an AF_UNIX budget, not style: sun_path ends near 108 bytes and the daemon socket sits inside the sandbox, which is the same reason /tmp is hardcoded there instead of $TMPDIR. An empty tag is rejected rather than defaulted, which is why the knob reads ${…-$$} and not the ${…:-…} its two neighbours use. An empty value is not a caller asking for the default — it is a caller whose tag expression came out empty — and accepting it would sandbox under the pid while the caller globbed /tmp/atverify..*, matching nothing and greening the leak assertion forever. That is the same vacuous pass the self-check exists to catch, arriving by a different door. The validation runs in the C locale, and this is a defect that was shipping rather than a precaution: POSIX defines a range like [A-Z] by collation rather than codepoint, and on glibc under en_US.utf8 the unpinned pattern accepts tág — measured on the Ubuntu CI leg, not reasoned about, so the ASCII-only contract was not being enforced there at all. It is invisible from macOS, where all 84 installed UTF-8 locales reject the same sample, which is exactly why Core cannot take one userland's answer for the fleet's. The byte cap is the same fault one step downstream: {1,16} counts characters, so sixteen multibyte ones are up to 64 bytes and the limit stops being the AF_UNIX budget it exists to be. Downstream, not separate — every character in [A-Za-z0-9_-] is single-byte ASCII, so the count can only diverge from the byte length once collation has already leaked a non-ASCII character in. The suite reports how much of this it actually exercised, rather than implying more. It asks the box for its installed UTF-8 locales (locale -a, falling back to named candidates on musl, which ships no such command) and looks for one under which the unpinned pattern really accepts a non-ASCII sample. Finding one, it names it and the case genuinely fails if the pin is removed; finding none, the result states the count and says the pin is unexercised there, asserted by contract only. Across the fleet that reads: Ubuntu exercised under en_US.utf8, while macOS (84 installed), Arch (1 installed) and Alpine (7 candidates, since musl ships no locale) report contract-only — so one leg proves the fix and the other three say honestly that they cannot. The no-match case then runs under LC_ALL=C rather than an empty LC_ALL, which is not "no locale" at all but a fall-through to the caller's LANG: an unprobed locale that could be the very one that accepts the sample, making the run exercise the pin while the line claimed it had not. Two earlier drafts of this check were vacuous — one probed for multibyte decoding, which a locale can do while still collating á outside [A-Za-z], so it passed identically with the pin removed. That is the shape this file already exists to refuse, and the coverage line is now part of the assertion rather than a comment about it. Two assertions, because narrowing a glob and blinding it look identical from a green run. The leak check now plants a foreign-tagged sandbox inside its own window and still requires a clean delta; a companion case plants one foreign and one of its own and requires the delta to name its own and only its own. The existing self-check — which fails loudly when the glob cannot enumerate at all, after an unfollowed /tmp symlink once made this pass vacuously on macOS — is unchanged, and matters more now: a tag that never reached the script would empty both snapshots the same way.
maint-status now reports a scheduler unit whose runner path no longer exists. _maint_unit_needs_refresh only ever asked whether the unit carried a PATH capture, so the other way a scheduled job dies silently went unreported: move the consuming repo and the scheduler keeps firing at the absolute runner path frozen into the unit at install time. Found on a real machine, where a launchd agent had been pointing at a path that had not existed for months. Nothing surfaced it from any angle. maint-status printed the timer happily, launchctl list showed exit status 0 because the job had not fired since the move, and maint-run kept working — it resolves the runner relative to the live config rather than reading the unit, which is exactly why the breakage stayed invisible. The detector now also reads the runner back out of the unit — ProgramArguments[1] from the launchd plist, the path after ExecStart=/usr/bin/env bash in the systemd service, the command past cron's single-quoted PATH= prefix — and flags it when it does not resolve. Both causes are fixed by re-running maint-install, so the hint now says which happened: a stale unit predating the PATH capture is a snapshot to refresh, a dead runner path usually means the repo moved. Each arm matches the exact shape maint-install renders and stays quiet on anything else, because a "close enough" parse turns a live job into a false death notice: the systemd and cron arms read a command, not a path field, so a hand-edited … bash /runner --quiet or … bash /runner >>/log must not be read as one long, nonexistent path; and the launchd array must be ProgramArguments' own value rather than the next array in the plist. A recorded path must also be absolute, which maint-install always writes: a relative one would be resolved by [[ -f ]] against whatever directory maint-status was invoked from, making the verdict a property of the caller rather than of the unit. A box with no schedule installed stays quiet too. Every token is located by position rather than by appearance: launchd's argv[0] must be the interpreter, and cron's PATH= value is consumed as a real single-quoted token, so a command that merely contains text resembling the interpreter — inside the assignment, or inside a later quoted argument — can never have its argument read back as our runner. On the launchd side the encoded forms a plist may legally use (", ') are decoded so the hint names the real filename, and anything undecodable (a numeric character reference, an unknown entity) is refused for the same reason the rest is: a filename that cannot be reconstructed is not evidence of anything. The two causes can coexist, and a unit predating the PATH capture is if anything the likeliest to have been orphaned by a move as well — so the runner is inspected first and path is the fallback. Reporting the milder cause there would tell the operator that some steps will skip on a job that does not run at all. A % in the recorded runner disqualifies it in both command-reading arms, because in neither is the literal text what runs: systemd expands % specifiers in ExecStart — the expansion _maint_systemd_escape already doubles against in Environment= — and cron reads % as its newline metacharacter. Thirty-one behavioral assertions — twelve scheduler states, four that a recorded path is extracted correctly (two read back verbatim, plus escaped-quote scanning and ' decoding), twelve that an extended command, a displaced array, a relative path, a spliced-in program, a quoted look-alike, an undecodable reference, or a %-bearing value is refused rather than mis-parsed, and three that a dead runner outranks a stale PATH. The healthy fixtures point at a runner that really exists, or the whole section would pass vacuously.
The release cut no longer tells the operator to do something the repo forbids. RELEASE-RUNBOOK.md §1.1 step 4 said "merge commit, not squash", and tag-release.sh printed the same hint twice. Merge commits are disabled — mergeCommitAllowed and rebaseMergeAllowed are false, and main's ruleset pins allowed_merge_methods: ["squash"] — so the instruction was impossible to follow, and it was printed at the worst possible moment: mid-cut, by the repo's highest-stakes command, where the natural reaction is to assume the ruleset is misconfigured and go change it. v4.10.0 had already shipped as a squash (cd4278e, one parent) in silent contradiction. The "not squash" clause was never load-bearing. It was descriptive — added in #106 (first shipped in v2.1.1) to record how releases merged then, since #95, the v2.0.0 release, had landed as a real merge commit back when merge commits were still enabled — and left behind when they were turned off. What makes the recipe correct is step 5 tagging origin/main, the post-merge tip, so release.yml's core.version-at-the-tagged-commit guard, git describe, and the vN alias are all satisfied by a squashed tip. RELEASE-RUNBOOK.md now records that reasoning under §"Why squash is fine", including the instruction to trust the repo over the docs if they ever disagree again. tag-release.sh now names no merge method rather than swapping one hardcoded claim for another. Deriving the wording from the live setting would need gh and a network call, which its offline-safe next-steps output cannot take, and there is no settings-as-code file to read instead. Naming a method was never actionable anyway — GitHub only offers the methods a repo enables, so the operator cannot pick a disallowed one. The hints now state the property that actually matters (step 2 tags origin/main, so the merge method cannot affect the tag), which has no way to go stale.
The boundary scan no longer strips comments at all. Stripping was a false-negative machine: # is a comment in shell and TOML but the length operator in Lua, so local p = t[#t] .. "<prefix>/bin" was truncated and passed; a delimiter inside a string is code, so export P="#<prefix>/bin" was truncated too; and a line inside a heredoc or a Lua long-bracket string is runtime data however it starts. Each fix uncovered the next, because getting it right needs a parser for all five grammars the gate now scans. The rule is flat instead: a manifested Core file must not contain an OS-absolute path anywhere, prose included — name the prefix rather than spelling it. Two comments in maint/ and tmux/scripts/ were reworded to comply. That costs a wording choice and buys a gate with no hiding places. The one sanctioned exemption is now redacted rather than dropped. Removing the whole LaunchAgents line exempted everything else on it, so a second literal riding along on a legitimate assignment evaded the gate; only the sanctioned segment is replaced now, and the rest of the line is scanned normally. Verified against the old filter: with _x="<prefix>/bin" appended to a LaunchAgents line, the line-drop passed it and the redaction catches it.
V4-PROPOSAL.md no longer claims v4 is unreleased. Its status block said "IMPLEMENTED … pending the v4.0.0 release cut" and described the work as sitting on a branch — ten minor releases after v4.0.0 shipped. It is now marked as the historical design record it is, pointing at ARCHITECTURE.md / PORTABILITY.md / VENDORING.md for how the shipped system actually behaves.
The Core⇄OS boundary gate was green while two Core files carried Homebrew paths. audit-core.sh §5c rejects OS-absolute paths in portable Core, but its file list stopped at zsh/*.zsh plus the symlinked configs — so bin/, maint/, and tmux/scripts/, all manifested Core that ships to eight repos, were never scanned. They were not clean: maint/dotfiles-maint.sh hardcoded /opt/homebrew/{bin,sbin} and /home/linuxbrew/.linuxbrew/bin in its PATH and probed both by absolute path to run brew shellenv, and tmux/scripts/tmux-cheat.sh did the same in its pop-up PATH. The rule was documented, believed enforced, and was not — on seven of the eight target machines those paths do not exist. The gate's scope is now derived from core.manifest rather than hand-kept. That list had fallen behind three separate times — first the symlinked configs, then the bin//maint//tmux/scripts/ executables, and even then it still omitted zsh/completions/*, lib/ux.sh, lib/bootstrap-lib.sh and .bin/sync-upstream.sh. Every omission was the same bug, so the fix is structural: the manifest already is the definition of "what is Core", and a file added to it is scanned automatically. The blind spot cannot silently reopen, because reopening it would mean the file is not Core at all — which the manifest gate already fails on. Coverage went from 19 files to 167 (including the vendored nvim/ tree). The gate is also unconditional now: it used to be SCOPE_SHELL-gated, but it is pure sed+grep and cross-cutting, so a narrowed --scope run must not be able to skip a fan-out-correctness check. The one exemption — zsh/55-maint.zsh, whose launchd arm legitimately writes ~/Library/LaunchAgents — is now per-line rather than per-file. Skipping the whole module would have re-opened the blind spot inside it: an accidental /opt/homebrew added to maint-install, or to any other function there, would have sailed through. Only the LaunchAgents lines are dropped; everything else in the file is scanned. Verified the way a gate change has to be: the previous tree is red under the new scope and green under the old one, which is the only evidence that the widening bites.
maint-install now escapes the runner path it writes into every scheduler unit. The write-side half of the % problem the entry above only closed on the read side: the captured PATH was already escaped three different ways, one per scheduler grammar — but the runner alongside it went in raw, and it is no more of a constant: it is wherever the consuming repo happens to have been cloned. A single metacharacter in that path produced a broken schedule, and all three failures were silent or nearly so: - systemd expands % specifiers in ExecStart= (%h = home directory, %i = instance, …), so a repo under …/a%h/… ran a different path entirely — or the unit refused to load outright on an unknown one. It substitutes variables there too, so a component literally named ${HOME} was equally not the path that ran. The Environment= line one row above was already protected against the specifiers — and performs no variable substitution at all, which is why $ needs its own pass rather than a wider shared helper. The argument was also unquoted, so systemd split the runner on whitespace, and a " or \ in the name carried unit-file syntax rather than being part of the filename. - cron treats % as its newline metacharacter: the command was truncated there and the remainder handed to it as stdin, so the job simply stopped running. maint-install already escaped % in the PATH portion and not in the runner. The runner was unquoted besides, so a space split the command and a $(…) or a backtick in the path was code, evaluated on every scheduled run. - launchd got &, < or > straight into ProgramArguments, yielding a malformed plist that launchctl load rejects. The PATH value two lines below was already escaped. Each field now goes through the escape its own grammar needs: the systemd runner is written quoted, through the Environment= helper plus a command-line-only $ → $$ pass (quoting is what reduces whitespace, " and \ to the same substitutions % and $ already needed), the cron runner through the same single-quote-then-escape-% pair as the cron PATH, and the launchd runner — along with the two log paths, which had the same hole — through the plist's XML escape. The crontab entry is also emitted with print -r rather than echo. maint-install runs under emulate -L zsh, where the echo builtin interprets backslash escapes — so two characters in a directory name were enough to corrupt the table that the careful quoting above had just produced: \n split the entry across two lines and \c truncated it outright, leaving a schedule that silently was not the one anyone asked for. _maint_unit_runner decodes each new encoding symmetrically, so maint-status keeps naming the real filename. It stays as strict as it was, and the strictness is the same rule in three places: a value the reader cannot reconstruct is not evidence of anything, so it is refused rather than guessed at. The systemd arm therefore refuses a closing quote with argv after it, a surviving % specifier, and a surviving $VAR reference — the text in the file is then not the path systemd runs, and resolving either would mean reimplementing systemd's specifier table and reading the unit's environment block. The cron arm refuses a bare % — one that is not our own \% — because sh quoting is no defence there: cron translates the field before sh ever sees it, so the command is truncated at that % whatever the quotes say. That test has to run before the \% decode, which would otherwise destroy the evidence of which kind of % it was. The launchd arm already applied the same rule to an undecodable entity reference. The older unquoted shapes still parse, because a unit on disk is only rewritten when the operator re-runs maint-install — and a % in one of those is still refused outright by _maint_lone_arg, which remains the right answer there: nothing escaped it, so the recorded text genuinely is not the path that runs. Twelve further assertions: one round-trip per scheduler through a runner path holding % $ ${} & < > " \ ', a space, and the two-character sequences \n and \c — installed, read back verbatim, and reported as current rather than as a dead runner — one per scheduler confirming the same artifact through a party that is not this codebase (/bin/sh parses the cron command back after applying cron's own \% pass, plistlib parses the plist, and the systemd ExecStart is pinned against a literal expectation), one that the crontab entry is a single marker-terminated line, and five refusals for the quoted shapes. That one is stated as a pair deliberately: with this fixture the two echo corruptions cancel in the line count — \n adds a newline and \c removes the final one — so a bare "is it one line" check reads green on a table that is one wrapped fragment plus one unterminated one. Reaching the marker is what truncation cannot fake. A round-trip through our own reader alone would pass a matched pair of wrong escapes, and a fixture whose backslash pair is not a recognized escape passes the echo hazard without ever exercising it — the first revision of this one used \g and did exactly that. The whole block skips, rather than passing vacuously, on a filesystem that will not take " or \ in a name. The pre-existing cron render assertion now anchors on the runner's closing quote, so dropping the quoting fails there rather than on the one box whose path has a space.
Caller-supplied workflow inputs no longer reach a run: body as code. auto-tag-call.yml spliced ${{ inputs.bump }} straight into its script in a job holding contents: write and persist-credentials: true, so a caller passing bump: 'patch"; …; #' could run arbitrary code with the tag-push token. It was the one place the fleet broke the rule notify-web-call.yml states outright — "a caller-supplied string must not be able to write shell". Both bump and release now arrive through env:, and bump is checked against a patch|minor|major allowlist at runtime — workflow_call inputs cannot be type: choice (that is workflow_dispatch-only), so the type system will not do it. A typo now fails with the valid set instead of reaching auto-tag.sh's arg parser. claude-routines-call.yml had the same shape with ${{ inputs.distro }} in a job holding CLAUDE_CODE_OAUTH_TOKEN; it now goes through env: too, and is likewise allowlisted to the six distro names its own input contract already documents — env: makes the value shell-safe, but the Claude prompt is an instruction channel, so an arbitrary string there remains a prompt-injection vector. No run: body in any workflow interpolates an expression any more. Neither rejection path echoes the raw value back. The runner parses stdout line by line, so a multiline input can open a new ::…:: command on the following line and forge or suppress annotations no matter how well it is shell-quoted; both paths strip CR/LF/%/: and truncate first, mirroring how atuin-guard-verify.yml already handles upstream-derived text.
Engagement-data write guard. note, logshell, bhce and nmapsweep used to fall back to $PWD when $ENGAGEMENT was unset, so running them inside a checkout wrote client data into that repo. They now resolve their root through _eng_writeroot, which refuses any $PWD inside a git work tree.
The field references open read-only. htp/xdev/evade/ipp are symlinks to tracked files, and hacktheplanet's "target fill" recipe told you to substitute the real client IP/hostname/domain into the buffer — one :w from publishing engagement data. They now open with -R; htp -w edits deliberately, and the fill recipe writes a copy under $ENGAGEMENT.
.gitignore backstop repaired. *.xml carried a trailing comment, which gitignore does not support — the pattern was the whole line and matched nothing, leaving nmap -oX output unguarded. The ignore list also described the *template's* directory names rather than the ones mkengagement creates, so scope/, recon/, scans/, web/, screenshots/, exploit/ and notes.md were all unblocked.
Pinned + verified tool installs. The five curl | sh installers are gone. install/tool-versions.env pins each tool's version and the SHA-256 of its release asset; bootstrap.sh verifies before installing and fails closed. starship moved to apt, which packages it.
Secret scanning in CI — gitleaks over the working tree and full history.
hethttp refuses to serve a git work tree on 0.0.0.0.
bhce can take credentials off argv — op://… resolves through 1Password, - prompts with echo off.
doggo, carapace and sesh never installed on a fresh box. mise lands in ~/.local/bin, which is not on PATH during bootstrap, so the go install fallback's command -v mise always missed. A PATH prelude fixes this and the related re-install-every-run behaviour of atuin.
A symlink cycle in the .zshrc wiring. bootstrap.sh re-did a link the library already makes, bypassing the ELOOP guard in _blib_seed_zdotdir_rc.
bootstrap.sh no longer silently installs nothing when install/packages.txt is missing.
apt_install's per-package retry keeps --no-install-recommends.
The bootstrap workflow's path filter omitted install/ and wsl/, so package-list edits never re-ran the bootstrap test. Filters removed.
dotsync hardcoded ~/dotfiles-Kali; it now resolves this checkout.
The offensive tmux binding shipped even when its script was not linked, and hardcoded ~/.config against an XDG-aware bootstrap.
@batt_enable was unconditionally off "because WSL has no battery" — now detected, so bare-metal laptops keep the widget.
ssh/config pinned modern-only crypto on Host *, which refuses to negotiate with the legacy targets an offensive box exists to reach. Scoped to your own infrastructure.
pseudo-shell.py proxied through Burp by default, so every request failed opaquely when Burp was not running; now opt-in. Its requests dependency documents a PEP 668-compatible install path.
redup printed "go not installed" for an intentionally empty tool list, and ran searchsploit -u without the privilege its root-owned checkout needs.
Makefile — the entry point (make lint, test, core-sync, packages-check, …). Makes core.lock's make core-lock instruction true for the first time.
scripts/sync-core.sh, test/check-core-freshness.sh and a freshness workflow — the consumer-side core-sync line, which three files already referenced and none provided.
test/check-companion-integrity.sh — tamper detection for the second vendored subtree, mirroring core-integrity.
test/check-packages.sh + a packages workflow resolving every manifest name against kali-rolling.
markdownlint in CI, against the .markdownlint.jsonc that had been sitting unused.
SECURITY.md, CODEOWNERS, issue and PR templates, CONTRIBUTING.md, .shellcheckrc, .editorconfig, .gitattributes.
bootstrap.sh --dry-run and --no-upgrade.
companion_version / companion_tag in companion.lock, for symmetry with core.lock.
The gating workflows (lint, bootstrap, companion, routine-filter) no longer use trigger-level path filters: a paths:-skipped workflow produces no check run, so requiring one would hang every non-matching PR.
os/kali.gitconfig no longer duplicates Core's init.defaultBranch, and os/kali.zsh no longer duplicates Core's ~/.local/bin PATH prepend.
offensive/templates/engagement.md documents the layout mkengagement actually creates.
bootstrap.sh no longer fails on a machine without sudo. The escalator is now resolved once (BLIB_SU: empty as root, else sudo, else doas) and used everywhere, instead of a hard-coded sudo at a dozen call sites. A container, a WSL first boot, or a minimal Server image previously died at the first dnf line with sudo: command not found (exit 127) before doing anything.
bootstrap.sh can no longer stall on an invisible password prompt. The sudo timestamp is primed up front and refreshed in the background for the life of the run, and privileged calls no longer discard stderr. Previously, calls placed after the multi-minute cargo/go builds outlived the 5-minute timestamp and blocked on a prompt written to /dev/null — indistinguishable from a hang.
Re-running bootstrap.sh no longer rebuilds the Rust/Go tools from source. The presence guards probed PATH, but ~/.cargo/bin and ~/.local/bin are only added by os/fedora.zsh — i.e. only inside a Core *zsh* — so a run from bash rebuilt all six crates plus yazi every time. provision() now puts both bindirs on PATH first.
A failed step is now reported. Best-effort failures are collected and printed as a closing summary instead of being swallowed, so a box missing carapace, op, lazygit and every cargo tool no longer reports bootstrap complete. --strict exits non-zero.
/etc/wsl.conf is backed up before it is overwritten (.pre-dotfiles.<epoch>, matching every other managed file). It was the one destructive write with no backup.
**OS detection no longer matches Fedora-*like* distros by accident.** ID=/ID_LIKE= are parsed as keys; the old grep -qi fedora /etc/os-release also matched Rocky, Alma, CentOS Stream, Nobara, and any incidental substring such as a URL. Fedora-like distros are now an explicit --force-os opt-in.
--help no longer drifts. It was sed -n '2,17p' "$0", coupled to the header's line numbers — the exact trap core/scripts/sync-core.sh documents. It is a heredoc now.
.gitignore no longer ignores the tracked core/.claude/ files. The .claude/ pattern was unanchored, so it matched at any depth — a hazard for a vendored tree whose git tree SHA must match core.lock.
bootstrap.sh --dry-run — previews the whole plan (packages *and* the symlink graph) and changes nothing, via the shared lib's BLIB_DRY; prints the wiring tally.
bootstrap.sh --strict and --force-os; a preflight that checks for the commands the script assumes and fails once with the full list.
bootstrap.sh now installs the core/ pre-commit guard on a fresh clone (blib_install_core_guard), which the shared lib always intended but was never called.
1Password's signing key is fingerprint-verified before rpm --import; a mismatch fails closed. The three upstream install scripts are downloaded, sanity-checked, then run — never curl | sh — and starship installs to ~/.local/bin, needing no root.
Root repo scaffolding that GitHub can actually see (it previously existed only under core/, where GitHub ignores it): CONTRIBUTING.md, SECURITY.md, CODEOWNERS, PR and issue templates, .editorconfig, .shellcheckrc, .gitattributes, .pre-commit-config.yaml, this changelog, and a thin Makefile (make lint / check / dry-run / integrity / hooks).
packages workflow — resolves every name in install/packages.txt against a matrix of supported Fedora releases, replacing hand-maintained availability prose with a check.
A stale 4.5 MB orphaned worktree copy under .claude/worktrees/, and the obsolete zsh/local.zsh ignore entry (host overrides have lived at ~/.config/zsh/99-local.zsh since v4).
bootstrap.sh --dry-run — previews the entire run (package plan + symlink plan + /etc/wsl.conf handling) and changes nothing. The shared library has supported BLIB_DRY end-to-end all along; this layer simply never exposed it.
Root Makefile — lint, bootstrap-dry, packages-check, secrets, core-lock, core-verify. lint reproduces the CI gate exactly, so a failure is visible before pushing. This also makes core.lock's own header instruction (“Regenerate … with: make core-lock”) true for the first time.
packages workflow — resolves every install/packages.txt name against the Arch repos on PR and weekly, without installing. Nothing previously checked the package list, on a rolling release where renames are routine.
Root .gitattributes, .editorconfig, .shellcheckrc — Core ships all three, but EditorConfig/shellcheck/gitattributes resolution is directory-scoped, so they governed core/** only and this repo's own files had no policy.
CODEOWNERS, pull_request_template.md, SECURITY.md, and this file.
bootstrap.sh could exit 0 having installed nothing. blib_read_pkgs' exit status is lost inside the < <(…) process substitution, so a missing or empty install/packages.txt produced an empty array, a failed pacman -S, a zero-iteration fallback loop, and a success message. It now refuses to continue.
bootstrap.sh silently swallowed per-package install failures. The fallback loop discarded every error, so a handful of renamed packages yielded a green run and a half-provisioned machine. Failures are now collected, reported at the end, and produce a non-zero exit — after wiring completes, so the box is still usable.
bootstrap.sh clobbered an existing /etc/wsl.conf. Every other mutation in this system backs up first (blib_link → .pre-dotfiles.<epoch>); this one overwrote, losing any local [automount] / [boot] / [network] settings on a re-run of a script documented as idempotent. It now no-ops when already correct and backs up otherwise.
pacorphans passed all orphans to pacman as a single argument. zsh does not word-split unquoted parameters, so pacman -Rns $orphans handed over one newline-joined string; now ${(f)orphans}. zsh -n cannot catch this — the syntax is valid.
--help was coupled to the file's header line numbers (sed -n '2,17p' "$0"), so editing the banner silently drifted the help text. Replaced with a usage() heredoc, matching the fix core/scripts/sync-core.sh already documents.
Stale .gitignore entry zsh/local.zsh — a pre-v4 path that does not exist in this repo. Added credential, .envrc/.direnv and key-material patterns (direnv is installed by packages.txt and hooked into every shell).
Privilege escalation goes through the library's _blib_priv, honouring BLIB_SU, instead of a hardcoded sudo. This makes bootstrap.sh work as root and on doas-only boxes — and makes provision() runnable in a container, which Arch base images (no sudo) previously prevented.
Arch derivatives are accepted with a warning rather than refused: the guard now falls back to ID_LIKE=…arch…, so EndeavourOS/Manjaro/CachyOS work.
go install for sesh logs its errors to a file instead of /dev/null, and the module version is overridable via SESH_VERSION (still defaulting to latest — see the note in provision()). carapace is a printed paru hint and takes no version override, per the analysis in #89: go install cannot work for any version of it. That error logging is what makes such a failure visible in the first place — the old /dev/null form is precisely why the carapace call could fail on every bootstrap without anyone noticing.
A failed run now says where it failed (ERR trap), and a successful one prints the wiring tally and points at core-doctor.
bootstrap.sh installs the local core/ pre-commit guard on a fresh clone (blib_install_core_guard), catching a hand-edit at commit time rather than waiting for core-integrity.yml at PR time.
The autostart stand-down is measured now, not assumed (dotgibson/dotfiles-core#402). _core_atuin_daemon_guard stands down entirely under ATUIN_DAEMON__AUTOSTART — it unhooks itself from precmd_functions and never probes — because atuin is supposed to supervise its own daemon there. That covers Alpine and macOS, two of the eight machines, and on those two it was the only mitigation. Nothing measured it: the weekly detector never set the variable, so a green run said nothing about those rows, and an upstream regression would have cost them their history with no symptom — the same failure mode as #366. scripts/verify-atuin-guard.sh gains --premise discard|autostart (default discard) plus make verify-atuin-guard-autostart, and the weekly workflow gains measure-autostart and report-autostart jobs with their own issue titles. One premise, one verdict, one title: an autostart finding needs a different remedy from a discard one, and would otherwise arrive under a heading that misdescribes it. The default target still starts no background process — asserted by construction, since the stub logs every invocation it receives. The measurement's own discipline is the interesting part. Because this premise is caused rather than observed, "autostart did not spawn a daemon" and "this box cannot host a daemon" are the same observation — so a manual-spawn control runs first and its failure is unmeasurable, never a finding about upstream. A second control tries a manual bind over a stale socket and records the answer, which is what separates "the client will not re-spawn after a crash" from "the daemon cannot bind over a leftover inode". Teardown goes through atuin daemon stop and is proven by a connect rather than believed from an exit status, escalating to the socket's owning PID and then a signal; the run refuses to spawn at all on a build lacking either subcommand, because a daemon it cannot reap would be left writing into a tree the exit trap is about to delete. Two upstream facts this established on 18.19.0, both recorded in PORTING-MATRIX.md: the stale-socket shape is the load-bearing one — every atuin history start is a fresh process, so "fire-and-forget" can only mean a crashed daemon's leftover inode defeats the spawn — and the healing lives in the client, since atuin daemon start alone refuses over a stale inode with Address already in use while the autostart path unlinks it first. Also measured, and load-bearing for the arms: with a daemon serving, history start writes nothing and the row lands on history end. /tool-scout loses a standing upstream question it could only ever have answered from release notes, and the report names the trap in its own remedy: "make the guard stop standing down" is the fix that breaks those two machines, because the degrade path sets ATUIN_DAEMON__ENABLED=false and under autostart that removes the spawn itself.
git-absorb — auto-route staged hunks into the earlier commit each belongs to (dotgibson/dotfiles-core#394). 00-tools.zsh now detects git-absorb and sets HAVE_GIT_ABSORB. It works out which earlier commit each staged hunk belongs to and writes the fixup! commits for you; git/gitconfig already sets rebase.autosquash = true, so git rebase -i folds them in without further ceremony. It is the automatic counterpart to the git fix alias Core has always shipped — git fix <sha> when you know the target commit, git absorb when you don't. That [alias] line now carries a comment saying so. This is the house-style ideal: a tool that needs no alias at all. It installs as git-absorb on PATH, which git dispatches as the git absorb subcommand, so it shadows nothing and 20-aliases.zsh gains only a note explaining why there's nothing to add. Detection exists purely so core-doctor can report it, where it joins the dev / repo group. One documented caveat: the probe is command -v git-absorb, so a distro that installed the binary into git's libexec/git-core instead of a PATH directory would give a working git absorb and an unset flag — no mainstream package does, and probing git absorb --version would add a git fork to every interactive shell, which 00-tools.zsh exists to avoid. It is also the first core-doctor --json key that is not a bare identifier, so the function's docstring now says which parsers care: the key is emitted quoted and the JSON is valid, but jq's dot shorthand reads .tools.git-absorb as a subtraction — consumers write .tools["git-absorb"]. Packaged essentially everywhere and installed nowhere on Linux, so it takes the ²¹ "available, not installed" shape and joins that footnote's macOS-only bullet alongside lnav. Verified against each distro's own package pages: Arch extra, Alpine community, Gentoo dev-vcs/git-absorb stable on amd64 in the main tree (no GURU), Homebrew and Debian/Kali all on 0.9.0 — note repology reports the Debian source package as rust-git-absorb while the binary you install is git-absorb. openSUSE Tumbleweed is the one laggard at 0.6.17, the gap #394 flagged, now confirmed rather than snapshotted. New PORTING-MATRIX.md row and footnote ²⁶. (zsh/00-tools.zsh, zsh/20-aliases.zsh, zsh/30-functions.zsh, git/gitconfig, PORTING-MATRIX.md)
watchexec — event-driven repetition, the third corner of a triangle Core had two of (dotgibson/dotfiles-core#393). 00-tools.zsh now detects watchexec and sets HAVE_WATCHEXEC. viddy re-runs a command on a timer and hyperfine re-runs it a fixed count while measuring; nothing re-ran it when files changed — the "re-run the tests when I save" verb (watchexec -e py -- pytest). Own command, inert without the binary, and deliberately not aliased to watch: 20-aliases.zsh already points watch at viddy, and collapsing "re-run on a timer" into "re-run on a change" would silently hand you the wrong one. It also opens a new dev / repo group in both of core-doctor's inventories. It is the one tool in the matrix that nothing in the fleet installs, macOS included — unlike lnav, the MacBook Brewfile doesn't carry it either, so every machine is opt-in. Arch extra, openSUSE Tumbleweed, Alpine community (native musl) and Homebrew are all on 2.5.1; Gentoo has it in GURU only at 2.5.0 — and it is deliberately kept out of footnote ¹²'s GURU list, which enumerates what dotfiles-Gentoo actually installs, not what exists (the gping¹⁹ precedent). Fedora and Debian/Kali don't package it at all — confirmed against Fedora's own package search, not a repology snapshot — so those two take cargo install --locked watchexec-cli; note the crate is watchexec-cli, because plain watchexec on crates.io is the library and installs no binary. New PORTING-MATRIX.md row and footnote ²⁵. (zsh/00-tools.zsh, zsh/20-aliases.zsh, zsh/30-functions.zsh, PORTING-MATRIX.md)
lnav — the missing "read a log as a log" verb (dotgibson/dotfiles-core#392). 00-tools.zsh now detects lnav and sets HAVE_LNAV. Core had no tool for this category at all: bat/rg read a log as lines, jq/gron/jnv read it as JSON, glow as markdown — none of them knows that a log is a sequence of timestamped records. lnav autodetects the common formats, merges several files into one timeline ordered by timestamp, follows like tail -f, and exposes the parsed records to SQL. It's its own command with no alias (like jq/gron/jnv) and is inert without the binary, so nothing changes on a box that doesn't have it. Unlike the Rust/Go tools already in the matrix it is a C++ CLI, so there is no cargo install/go install escape hatch — but it doesn't need one: upstream ships static musl binaries each release (lnav-0.14.0-linux-musl-x86_64.zip plus an arm64 twin), so the fallback on an unpackaged or lagging box is "unzip the official build", not "compile it". It is detect-only on Linux: no Linux repo's install/packages.txt carries it and no bootstrap.sh installs it, so the flag lights up only once you install it yourself; macOS is the exception, where the Brewfile has carried it since 2026-07-15. That is hyperfine/shellcheck/shfmt/ouch's situation, so lnav joins that bullet in footnote ²¹ and its row carries ²¹ alongside its own ²⁴. Every version was read off the distro's own package page rather than a repology snapshot, and Fedora is reported per release because it is a versioned distro and one unqualified "current" hides the answer: F45/Rawhide 0.14.0, F44 0.13.2, F43 0.12.4. Arch extra, openSUSE Tumbleweed and Homebrew are on 0.14.0, and Alpine has a native musl build in community. Two targets lag enough to name: Gentoo at 0.11.2 — the only version in the tree, and the package needs a new maintainer — and Kali/Debian at 0.13.2. On any of them the upstream static musl zip gets you 0.14.0 without waiting for the package. New PORTING-MATRIX.md row and footnote ²⁴, and lnav joins the data / net group in both of core-doctor's inventories. (zsh/00-tools.zsh, zsh/20-aliases.zsh, zsh/30-functions.zsh, PORTING-MATRIX.md)
OSC 133 semantic prompt marks — [ / ] jump between prompts in tmux copy mode (dotgibson/dotfiles-core#391). Core emitted no OSC sequences at all, while tmux has parsed OSC 133 since 3.4 and exposed previous-prompt / next-prompt in copy mode the whole time — the capability was already paid for on every machine (the fleet floor is Gentoo's 3.6a) and simply unused. zsh/00-tools.zsh now marks prompts and tmux/tmux.reset.conf binds [ / ] in copy-mode-vi to jump between them, turning "scroll up hunting for where that command started" into a keypress. No new file, no binary, no core.manifest change; { / } are deliberately left alone (vi previous/next-paragraph), and no version gate is needed. The A mark lives in $PROMPT, and that is measured rather than preferred. The obvious implementation — emit \e]133;A\e\\ from precmd, next to the command-block rule that already runs there — does not work, and fails silently: zsh's prompt preamble ends in ED (\e[J, "erase to end of screen") over the very line the mark was just written to, and tmux drops a line's prompt flag when that line is cleared. Measured on tmux 3.7b, previous-prompt then does not move at all — the feature looks implemented and does nothing. Embedding it in PROMPT as a zero-width %{…%} escape means it is re-emitted on every prompt draw, after that ED, which is also why every other shell integration marks prompts this way. The hook that applies it is APPENDED to precmd_functions, so it runs after starship_precmd re-sets PROMPT wholesale, and is idempotent for the box where PROMPT is static and would otherwise grow one mark per prompt. 45-plugins.zsh carries the same mark on the transient prompt: collapsing a finished prompt to ❖ redraws that line too, and scrollback is exactly what previous-prompt jumps through. Only A and C are marks tmux documents a dependence on, so that is the subset; D;<exit> is emitted anyway because it is free from the exit code already captured and is what non-tmux OSC 133 consumers read for per-command status. C/D stay hook output — being cleared costs them nothing, since nothing reads them back off the grid. The marks stand down in two places: under Ghostty's own shell integration, but only OUTSIDE tmux — GHOSTTY_SHELL_FEATURES is exported and reaches the tmux server, while Ghostty injects into the initial shell only, so guarding on the variable alone would have silenced the marks in exactly the place they are spent — and on TERM=dumb, which would render them as literal ]133;A garbage. Fourteen behavioural cases in scripts/test-core.sh pin all of it. One premise of #391 did not survive measurement and is recorded here so it is not re-derived: that _cmd_block_precmd returning its last print's status rather than the command's left starship_precmd reading the wrong $?. zsh saves and restores $? around each hook in precmd_functions — measured on 5.9, every hook sees the command's code regardless of what the one before it returned, a non-zero return does not stop the rest of the chain, and it never reaches the prompt's own %(?..). The hook now returns $ec anyway, as the contract the D;<exit> mark is written against, but nothing was broken and nothing user-visible changed — including starship's error indicator, which was always correct. The same correction applies to the "run our precmd FIRST so $? is the command's" comment that line has carried since P12: the ordering is worth keeping for OUTPUT order, not for $?.
The atuin daemon's systemd path is measured, and the tail claim holds on it (dotgibson/dotfiles-core#352). Adoption's whole justification was that the daemon owning the SQLite writes removes the DB-lock contention every shell and tmux pane pays, and it had never been measured here for want of a systemd box. --systemd has now run — seven times, on Fedora 44 under WSL2 with a real user manager, a real transient unit, glibc 2.43 and atuin 18.19.0 — and every run passed the three checks a first real run had to: the unit stayed active for the whole arm, its MainPID owned the listening socket, and the row deltas were exact. On prompt latency — history start alone, the call _atuin_preexec blocks the shell on, and the only figure quotable as latency — with the history DB on a local ext4 home, three runs: p50 1.55× / 1.55× / 1.60×, p95 2.50× / 2.33× / 2.17×, p99 2.91× / 3.25× / 1.35× faster. The median win was already known; the tail win is the part that was borrowed from upstream and is now measured — with the caveat the third run makes plain, that the p99 sign is stable while its magnitude is not, so the tail belongs in the record as "faster in every run, 1.4–3.3×" rather than as a number. It agrees in direction with the one earlier blocking-call run (p99 improving 49–69%) — but that was the same machine days earlier, not a second host, so it is reproducibility over time and not independent corroboration. Total write work (start + end, which the hook backgrounds) improves at p50/p95 too, but its p99 remains unresolved — expected, since end is exactly where the two metrics diverge. Storage backing turned out to be the confounder, which is the part worth carrying forward. Same harness, same host, same day, prompt-latency p99: tmpfs flips sign run to run (2.23× slower, then 1.48× and 2.04× faster), ext4 wins in every run (1.35–3.25×), and a high-latency non-local filesystem wins 26–43× — daemon-off p99 there is 0.8–1.1 seconds while daemon-on stays flat at ~27 ms. Without a real fsync there is barely a lock to contend over, so the mechanism only becomes visible where storage is slow enough for lock hold time to matter. That is the likeliest explanation for this repo's contradictory older figures, whose storage was never recorded — likeliest, not established, since nobody re-ran them. The harness now prints the sandbox filesystem on every run, not only under CORE_ATBENCH_BASE, and adds "durable storage" to the not-covered list when the DB lands on tmpfs, because the default sandbox lives in /tmp — which is precisely where the tail is least readable. atuin/config.toml records all of it, relabels the older pair-timed and unknown-storage tables as the weaker evidence they are, and names the cheap re-run that would settle the musl question (the same Alpine container with the DB on real disk). The UNVALIDATED-SYSTEMD marker is retired across the harness, Makefile and the suite; what replaces it on the user-visible surface is the caveat that outlives the runs — not bare metal, not a real multi-pane session, not musl on hardware, and a 9p mount is a proxy for a network home rather than one. The autostart spawn cost reproduced on real disk at +42.16 ms (p50), in line with the ~+41/+45 ms seen in containers.
The modernization floor now bans allow-unsafe-pr-checkout, the one input that can re-open a "pwn request" in this fleet. actions/checkout v7 (2026-06-18) started refusing to check out fork-PR code under pull_request_target / workflow_run — a repository:, ref:, or head SHA resolving to the fork — and backported that enforcement on 2026-07-20 to v3.7.0/v4.4.0/v5.1.0/v6.1.0/v7.0.1. The single escape hatch is an input GitHub deliberately named to be easy to spot in code review and static analysis, so scripts/modern-baseline.yml now greps for it as a rule-1 banned pattern. Nothing had to be fixed first: the string appears nowhere, there is no pull_request_target in the fleet, and the lone workflow_run trigger (sync-fanout.yml) checks out a released tag rather than a fork ref — so this is purely preventative, and it fans out N-way to the OS and role repos that consume lint-call.yml@v4 and friends. dotfiles-Kali / dotfiles-Defense are exactly the repos where someone might one day reach for pull_request_target. Rule 1's existing grep -HnF sweep already covers both .github/workflows/ and .github/actions/, so no new enforcement branch was needed in check-modern.sh. Also refreshes the banned_runners rationale with ubuntu-22.04's now-fully-published schedule — deprecation opens 2026-09-17, brownouts 2027-03-23/03-30/04-06/04-13, fully unsupported 2027-04-17 (actions/runner-images#14254); comment-only, the ban itself has been in place since it was added pre-emptively.
scripts/bench-atuin-daemon.sh — the atuin daemon's latency claim is no longer purely borrowed. Adoption's whole justification is that the daemon owns the SQLite writes so shells stop contending for the DB lock, and that was cited from upstream, never measured here. The script measures the per-command pair a shell hook actually runs (history start + history end) under N concurrent writers sharing one seeded history DB, daemon off vs on, reported as p50/p95/p99 — plus the daemon-spawn cost the first command pays on the autostart path, which is unique to machines with no service manager. Report-only and deliberately not part of make audit (it needs a real atuin binary and starts a background daemon); make bench-atuin runs it, and it SKIPs cleanly on a box without atuin. It also asserts behaviourally something the hermetic suite can only take on faith from upstream's settings.rs: that atuin, with XDG_RUNTIME_DIR unset, binds exactly the socket path _core_atuin_daemon_guard probes. The harness now also covers the two paths it structurally could not, and enforces a rule that stops it lying. --systemd measures the systemd-unit path: env -i guaranteed XDG_RUNTIME_DIR was unset, so atuin's default $XDG_RUNTIME_DIR/atuin.sock — the Fedora shape, and the other branch of _core_atuin_daemon_guard's expression — was unreachable by construction. It runs the daemon from a sandbox-scoped transient unit (systemd-run --user), never your atuin-daemon.service, points XDG_RUNTIME_DIR at the sandbox so it cannot collide with a real daemon, asserts the unit's MainPID actually holds the listening socket, and skips rather than degrades without a user bus — reporting no-systemd numbers under a systemd label is the one thing the flag exists to prevent. It shipped unvalidated — written where systemd-run --user could not reach a bus, so only its fail-closed skip path had ever executed — and has since been validated against a real user manager; see the entry below. CORE_ATBENCH_BASE puts the sandbox HOME and history DB on a network home, with the socket deliberately decoupled onto a short local path — AF_UNIX does not work on NFS/SMB and sun_path caps near 108 bytes — and discloses the cost of that, which is that such a run no longer exercises atuin's default socket resolution, so the socket-agreement claim is withdrawn rather than weakened. And every arm must now prove its writes landed: the DB's row delta has to equal the samples the arm claims, or the arm is not reported. That is not hypothetical — with the daemon enabled and unreachable, atuin 18.19.0 exits 0, prints a well-formed history id, writes nothing to stderr and discards the entry (atuinsh/atuin#3561), which is the fastest table this script can produce for work that never happened. The check covers both halves, since history end updates the row rather than inserting one and a pure row count would sail past a silently-discarded end.
The atuin daemon bench's fail-closed surface is now pinned by the suite (scripts/test-core.sh Section J2). make audit can never run the bench itself — it needs a real atuin, a real zsh and a background daemon — which is precisely why the parts that are hermetic are worth asserting: that --help documents every knob including the scope caveat a figure must be quoted with, that an unknown argument still exits 2, that a malformed CORE_ATBENCH_WRITERS or CORE_ATBENCH_BASE exits 2 rather than skipping (WRITERS=0 otherwise makes every arm vacuously complete and vacuously row-correct), and that --systemd against a stubbed busless systemctl skips with no results table — the no-degradation requirement expressed executably rather than asserted in prose. The row-count SQL is extracted from the script and executed against a synthetic table, the same "run it, don't pattern-match it" idiom Section J uses on the example unit's ExecStart.
The guard's upstream premise is now measured weekly in CI — and the check it replaces could report "all clear" from an apparatus that had never written a row (dotgibson/dotfiles-core#383). _core_atuin_daemon_guard is a workaround for one measured fact: on atuin 18.19.0, with the daemon enabled and its socket unreachable, atuin history start exits 0, prints an id, stays silent on stderr and discards the entry (atuinsh/atuin#3561). A persistent precmd hook, a throttled connect(2) on the prompt path, and a one-way degrade in every interactive shell across eight repos are justified by that fact alone. The copy-paste recipe that carried the standing re-verification failed open. It seeded its database through the unreachable-daemon path, so on a build that discards, the database was never created; its row count masked every failure as 0; and before and after were therefore both 0, which is the premise-holds signature. It was right by luck, not by measurement — and any apparatus failure at all, a missing python3 or an unreadable DB, read the same way. Measuring "the row count did not go up" without first proving the apparatus can write measures nothing. scripts/verify-atuin-guard.sh replaces it and reports three verdicts rather than two, because the third is the one that matters: holds, moved, and unmeasurable — the last meaning the apparatus could not be trusted, which is emphatically not good news and never collapses into holds. A daemon-off control arm runs first and must write exactly one row before any verdict is allowed. Both unreachable shapes the guard claims to catch are measured — an absent socket and a stale socket file left by a crashed daemon — and each is proven unreachable first, by a bounded connect(2) and the /proc/net/unix LISTEN scan, so a delta of zero can never rest on a socket that was quietly healthy. Exit codes are the verdict (0/1/3), which deliberately breaks this repo's skip-and-exit-0 idiom in one place: exit 0 is a positive assertion about upstream, so a bare box must not be able to produce it. .github/workflows/atuin-guard-verify.yml runs it every Tuesday at 13:00 UTC against whatever atuin upstream ships that week — the one thing in this repo deliberately not pinned, because a pinned atuin would re-measure a version whose behaviour is already recorded and miss the next release, which is the one that actually costs history. That inversion is bounded structurally rather than by trust, in three jobs: resolve holds a token and verifies the download's checksum and GitHub build-provenance attestation but never executes a byte of it (gh attestation verify has no anonymous mode, which is what forces the split); measure holds no token at all and is the only job that runs upstream code, refusing to proceed unless the asset still hashes to the digest resolve attested; report holds issues: write and never sees the binary, consuming only an opaque base64 blob. holds files nothing — a bot that opens an issue weekly to say nothing changed gets muted. Review hardening, because the first cut of this got three of them wrong in ways that matter. The detector now measures four arms, not two — {absent, stale} x {--hook, plain} — because atuin's own init zsh emits atuin history start --hook -- "$1", so the plain form is a path no shell in the fleet actually runs and a change scoped to hook mode could have broken every prompt while the detector reported holds. An unreadable database mid-run is now unmeasurable, not moved: atuin_db_rows returns -1 on a failed read, after - before then goes negative, and the verdict block read that as "the row count changed" — the same apparatus-versus-upstream conflation the control arm exists to prevent, pointing the other way. And a history id is checked for shape, not merely non-emptiness: the premise is that the shell gets an id it can hand to history end, so a deprecation notice on stdout must not read as a pass. In the workflow, a value derived from an upstream file can no longer forge job outputs (a multi-line .sha256 could append ok=true after ok=false and get an unattested asset measured), and a verifier that exits outside the documented 0/1/3 — or leaves an unparseable verdict.json — now fails the job instead of passing for a quiet week. A second control arm runs last, and it closes two holes at once. The opening control proves the apparatus at t=0 only — but a database that stops being writable mid-run still reads fine, so the -1 sentinel never fires, all four arms report an honest-looking delta of 0, and the run reports holds from an apparatus that had quietly died. It also probes the premise the four arms structurally cannot see: the one-way degrade is correct only while atuin discards during the outage, and an atuin that spooled those entries would leave exactly the same absence behind — then flush them on the next successful write, landing five rows on the closing arm instead of one. That would invert the reasoning zsh/00-tools.zsh degrades on, so > 1 is a moved finding and < 1 is unmeasurable; the verdict vocabulary is unchanged, and the only new outcomes are ways to not reach holds. What it still cannot see is stated rather than implied: a spool only a live daemon would drain needs a daemon spawned to observe. Finally, the report no longer disclaims coverage it has. Its scope paragraph went on saying "--hook is not exercised" after the matrix was widened to four arms — in the same output as a reason that said "all four arms (absent/stale x hook/plain)" — and the assertion that should have caught it grepped for two nouns the false sentence also contained. The coverage claim is now derived from the arms that actually ran, in both renderers, because a hand-written one is a second copy of the matrix and the second copy is the one that rots; the test checks the report and the JSON from a single run for agreement rather than for keywords. The row-count SQL and its fail-closed -1 now live in scripts/lib/atuin-db.sh, shared with scripts/bench-atuin-daemon.sh: both rest on the same claim about atuin's schema, so a forked copy would let one gate keep believing a model the other had already found stale. zsh/00-tools.zsh gains a machine-readable # CORE_ATUIN_GUARD_VERIFIED_AGAINST= anchor, because grepping the surrounding prose — which also names 18.16.1 — is how a detector silently starts comparing against the wrong version.
/tool-scout now re-checks the workarounds whose justification can expire (dotgibson/dotfiles-core#383). _core_atuin_daemon_guard is not a preference — it is a workaround for one measured upstream fact (atuin 18.19.0 discards a history entry when the daemon is enabled and its socket is unreachable, atuinsh/atuin#3561), and every millisecond it spends on the prompt path is justified by that fact alone. Nothing was watching whether it stayed true: atuin is not pinned in mise/config.toml and has no renovate.json entry, so a version bump arrives silently on whichever machine updates first, and the behaviour has already changed once in the direction that makes it harder to notice (18.16.1 failed loudly; 18.19.0 fails silently). The routine now carries a standing re-verification list and the version each workaround was verified against. The measurement is deliberately not there and must not be copied back: that is what scripts/verify-atuin-guard.sh and its weekly job are for (entry above), and the recipe that used to live in the routine is the one that failed open. What is left is the half a script cannot do — compare the anchor in zsh/00-tools.zsh to atuin's newest release, lead with any open verdict issue, and weigh the remedy as an eight-repo change — plus the upstream questions no measurement here can reach: whether atuin still health-checks its own daemon under autostart (the guard stands down entirely there, and that is the only mitigation on Alpine and macOS), and whether it has gained a client-side buffer that would invert the one-way degrade. A changelog that does not mention the bug is still not evidence the bug is gone, so a release past the anchor is a finding in its own right. Re-verifications lead the report rather than competing inside the ranked shortlist, and "nothing is due" must be said out loud, since silence reads the same as forgetting.
Three latent faults in scripts/verify-atuin-guard.sh, each harmless while nothing spawned and each load-bearing once something does (dotgibson/dotfiles-core#402). run_one captured stdout with $( ), which blocks on pipe EOF rather than child exit — a client that daemonized without reopening stdio would have hung the arm forever, and timeout could not have cut it, since it signals only its direct child. A hit bound rendered as a finding: rc 124 reached the verdict block as "exit code is 124, was 0" and reported an apparatus limit as moved; it is now unmeasurable with a named reason. And AT_ENV was assigned inside measure(), so under set -u any trap reading it after an early return died exactly when cleanup mattered most.
core-doctor was silently blind to twelve tools Core already detects. 00-tools.zsh probes 38 binaries into HAVE_* flags, but the health report only ever knew about 29 — ast-grep, difft, gping, hyperfine, jj, jnv, ouch, shellcheck, shfmt, tldr, uv, viddy were detected and then reported by neither the human report nor --json. Every one of them had been adopted without touching the doctor, over several releases, and nothing caught it. All twelve are now reported. The two inventories are one inventory. groups (human) and alltools (--json) were hand-synced literals; both now derive from a single _CORE_DOCTOR_GROUPS definition, so they cannot disagree by construction. The parity test is kept as the guard against a second literal reappearing. A new test closes the gap parity structurally cannot see — it reads the _have lines out of zsh/00-tools.zsh and requires the inventory to cover them, so a future adoption that skips the doctor fails the suite by name. (Direction is one-way on purpose: op has no HAVE_OP, and fd/bat are set from FD_BIN/BAT_BIN after resolving fdfind/batcat.) The group definition now carries the membership rule, so additions land somewhere defensible instead of at the end. The legend was scoped to what is true. It read ✗ falls back to classic, which held when the report was mostly command replacements. Most of the inventory is now opt-in tooling that shadows nothing — there is no classic ast-grep or jj — so it now reads ✗ absent; the replacements below fall back to the classic command. The terminal browser stays deliberately absent from the report: BROWSER_BIN picks from w3m/lynx/links2/links/ elinks, so a fixed w3m row would read ✗ on a box running lynx perfectly well. (zsh/30-functions.zsh, scripts/test-core.sh)
core-doctor's install hint advertised a paste-ready command that could not run. It printed <manager> <every missing tool> as one line — sudo dnf install rg lnav …. But apt/dnf/zypper/pacman all abort the whole transaction on a single unresolvable name, and unresolvable names are the common case here, not the edge: these are command names, while the package is frequently called something else (rg=ripgrep, delta=git-delta, fd=fd-find on Debian, dust=du-dust, yq=go-yq, op=1password-cli), and several tools are not packaged at all on some targets (sesh anywhere, watchexec on Fedora/Kali, carapace and yazi on Kali). So one bad entry silently blocked the good ones — sudo dnf install rg alone already failed. At least 12 of the inventory names break the line on some box, which is why this is not fixed with an exclusion list; and the alternative, a per-distro command→package map, is a rot-prone duplicate of PORTING-MATRIX.md. The hint now prints the missing tools as names, states that they are command names and that packages differ, gives the manager verb as a per-tool template (sudo dnf install <pkg>), and points at the matrix for the authoritative name. A new test asserts the template form is present and that the verb is never followed by a real tool name, so the batch form cannot come back. (zsh/30-functions.zsh, scripts/test-core.sh)
core-doctor -v printed _v=0.26.1 garbage instead of version annotations — the flag never worked. local _v sat inside the per-tool loop in _core_doctor_render, and zsh prints name=value when local re-declares a parameter that already holds one (TYPESET_SILENT is off, including under emulate -L zsh). So the first tool annotated its ✓ correctly and every tool after it emitted a bare _v=… line into the report, which is why the leak needs two present tools to show up at all. Declaring _v once alongside gi/tool/line fixes it; the assignment inside the loop is unchanged. This shipped broken and survived because nothing drove the -v path — the suite only asserted that the default render emits a group heading and that --json carries its top-level keys. There is now a hermetic case that stubs _core_have plus two shadowing tool functions and asserts both that versions render and that no _v= line appears. It uses two tools deliberately: a single-tool version of the same test passes against the unfixed code and would have guarded nothing. (zsh/30-functions.zsh, scripts/test-core.sh)
A timed-out package probe was logged as "0 upgradable" — an up-to-date box — instead of "unknown" (dotgibson/dotfiles-core#380). Every arm of the maint runner's upgradable-count chain was count=$(_to "$MAINT_PKGCOUNT_TIMEOUT" <mgr> | grep -c …), and that shape cannot tell the two apart: when timeout SIGTERMs a stalled manager there is no output, grep -c prints 0, and grep's non-zero status — the pipeline's, since it is the last stage — is discarded by the assignment. So the count=-1 "we don't know" sentinel was bypassed on precisely the failure the timeout had been added to survive (a mirror that accepts the connection and then stalls), and the daily log asserted the box was current when nothing had been measured. Counting now goes through a _pkgcount helper that captures first and gates on how the probe died — 124, the GNU/gtimeout expiry status, or >=128, killed by a signal — rather than on the manager's status. That second arm is not belt-and-braces: BusyBox timeout reports its SIGTERM as 143, not as 124, so a 124-only gate was green on every leg of the CI matrix except Alpine, where it still logged a stalled manager as 0. Gating on the manager's status instead would have been wrong in the other direction, because these managers use exit status to mean things: dnf check-update exits 100 when updates exist, pacman -Qu and checkupdates exit non-zero when there are none, so a general non-zero gate would have reported "unknown" on the healthy path. The pacman -Qu arm stays unwrapped and counted directly: it reads the local DB, cannot stall, and its 0 is real. The log line now says count UNAVAILABLE with the bound that was exceeded, instead of printing the sentinel as -1 upgradable. The nudge was never affected either way — it needs a positive count — so this was a log-and-cache honesty defect, which is the whole reason the sentinel exists. Two new tests cover it: one drives the real timeout against a manager that stalls (the pre-existing case stubs _to away by design) and reports the observed status, and one pins the BusyBox spelling on every host rather than only on the Alpine leg.
The up nudge's cache could still be written malformed by the shell that claims the throttle slot (dotgibson/dotfiles-core#380). The writer-side normalisation added with the reader-side quoting set count to empty on a fresh box, and the claim-slot write then persisted that empty value — producing exactly the "\n<epoch>" file the fix was supposed to prevent, whose unquoted (f) split slides the epoch into the count slot and prints "1786128391 updates available". Only the quoted "${(@f)…}" read was actually holding the line. It now normalises to the -1 sentinel, so the cache is well-formed at rest: it reads back cleanly, _pkgup_notice's <1-> gate rejects it, and the nudge stays silent until the backgrounded refresh lands a real number. The comments claiming the race was closed from both ends now describe what the code does.
ux_spin could take down a set -euo pipefail caller after its animation loop, and went silent when its busy-spin guard fired (dotgibson/dotfiles-core#380). The loop body was normalised (|| :) on the ground that this library is sourced — bootstrap.sh runs under set -euo pipefail — but every statement after done was still bare. A failed cursor-restore printf therefore aborted the caller with the cursor still hidden and the wrapped child still running: the identical end state documented for the unnormalised sleep. A failure in either result branch aborted between wait and rm -f, leaking the mktemp file. All of them are normalised now; rc is captured and returned explicitly, so nothing the caller sees changed. Separately, both spinners now leave one static (still running…) frame when the busy-spin guard trips. ux_spin cleared the line before the blocking wait, so a long command on a box with a broken pacing primitive showed nothing at all for the rest of the run — indistinguishable from the hang the elapsed-time readout exists to rule out — while _core_spin left a frozen glyph, which reads as "wedged". The wording, not the glyph, is what distinguishes "the animation gave up" from "the command did"; the glyph itself comes from the existing frame set, so the non-UTF-8 fallback is honoured rather than a hardcoded braille cell. The two mirrors agree again.
bench-atuin-daemon.sh started the daemon with a spelling the shipped unit deliberately probes for (dotgibson/dotfiles-core#380). Both starters ran atuin daemon start unconditionally, while examples/atuin-daemon.service goes to real trouble to ask the binary which spelling it has, because the subcommand does not exist on older builds. The bench failed closed there — socket never appears, ON arm dropped, no wrong number printed — but the diagnostic blamed the daemon rather than the spelling, on exactly the machines most likely to hit it. The bench now runs the same probe once at startup and reuses the answer in both the plain and --systemd paths. daemon stop in the teardown is left alone on purpose: it is already best-effort and the kill below it is what actually stops the process.
The weekly fleet-drift sweep reported the fleet's ordinary state as a failure. The sweep anchors to the latest released Core tag — deliberately, to avoid a false "BEHIND by N" on every unreleased commit — but make sync has never fanned out from a tag: sync-core.sh resolves git ls-remote <remote> main and vendors the branch tip, which is why the core_tag it stamps looks like v4.9.3-56-g44a44fc. Those two facts contradict each other on every day between releases. _classify treated anything not byte-identical to the reference as drift, so a single sync in an unreleased week put all eight Unix repos at "AHEAD by N", exit 1, a red run and a filed issue — while printing "run make sync", the one action guaranteed to push them further ahead. Green was only ever the accident of the fleet happening to sit exactly on a tag commit. A recorded commit ahead of the reference and an ancestor of origin/main (falling back to main) now reads as current: it carries newer Core off the released lineage, the opposite of the staleness this dashboard exists to find. The tolerance is deliberately narrow — ahead but not on main means the repo was synced from something that is not Core's release lineage, and still fails, as do behind, diverged, and a marker this clone cannot measure. With no mainline ref resolvable at all it fails closed and says so rather than green-lighting a lineage it could not check. This is the same false positive dd4f529 fixed for dotfiles-Windows in _classify_subtree; the Unix repos had carried it since. Two things that made the red run hard to read are fixed with it. The header printed the reference's sha beside the current branch — Fleet drift vs Core f95fc2b88218 (main) — so a sweep anchored to v4.9.3 looked like a comparison against main's tip; it now names what was actually resolved. And make sync is advised only for repos that genuinely lag, since it would overwrite an off-lineage marker rather than reconcile it. scripts/test-core.sh now drives the classifier hermetically against a throwaway Core — a tag, commits past it, and an off-main commit — pinning every verdict, including that real staleness still fails. Green is not the same as finished, so ahead-on-main rows get their own verdict rather than a plain ✓: a yellow •, plus a closing N repo(s) carrying UNRELEASED Core tally that prints even under --quiet. That tally — not the exit code — is now where "the fleet is running Core newer than any release" lives. It is a state that is fine to run and wrong to leave indefinitely, and flattening it into the same green as a properly pinned fleet would have traded one bad signal for a missing one. The fix it names is a release, not a sync.
--strict printed a red row and still exited 0. It is documented — in the header and in Exit: — to turn a not-checked-out repo from a skip into a failure, and it did bump the counter and print red, but it never set the drift flag, so the script returned success and every caller read the run as clean. A repo that was never cloned is now drift; it is deliberately not counted as stale, because make sync cannot repair a repo that isn't there, and the closing advice no longer offers a recipe that would not work.
An unresolvable --ref was silently answered with a different question. --ref nosuchref fell through the resolution ladder to origin/main and reported against it, while the banner still named nosuchref — the same class of mislabel as the header bug above, and worse, because the caller had been explicit. The fallback ladder exists for the default path (no tags yet, or a clone too shallow to reach one); an explicit ref that does not resolve is now a usage error (exit 2).
Six documents still described a bot deleted a month earlier — including the routine whose job is to watch it. Core retired .github/dependabot.yml when the fleet moved to the shared Renovate preset in v3.2.0, but the prose never followed. .claude/commands/freshness-triage.md told the triage routine to expect dependabot.yml PRs, while scripts/freshness-dashboard.sh was already counting author:app/renovate — so the routine was briefed to hunt for an author that can never appear, in the same repo whose dashboard knew better. CONTRIBUTING.md sent contributors to dependabot.yml for the commit-prefix convention, a file that has not existed since v3.2.0. The rest were comments in freshness.yml, claude-routines.yml, and update-plugins.sh explaining the freshness bot's reason for existing by contrast with the wrong bot. All six now name Renovate, and those that pointed at a config file point at renovate.json; the triage brief — the one document that has to act on the answer — additionally carries the ci(deps): prefix and the app/renovate author signature a run should look for, which the passing mentions elsewhere do not need. The routine brief additionally gains what #377's own caveat exposed: Renovate parks bumps on a Dependency Dashboard issue that opens no PR, so an empty PR queue is not an empty bump queue — and since reading it needs gh issue list, outside this command's allowed-tools, the brief now says to report the dashboard as unchecked rather than conclude "nothing to triage". Makefile's update-hooks help was corrected differently, by deletion: it justified the target with "dependabot has no pre-commit ecosystem", and Renovate does ship a pre-commit manager. The dependency dashboard (#186) settles it — Renovate detects only devcontainer, github-actions, mise, and renovate-config here, so the target is still load-bearing — but that is a fact about the org preset's current configuration, living in dotgibson/.github, and encoding it in a help string is how the previous claim rotted. The line now states what the target does and nothing about which bot doesn't do it. Historical CHANGELOG.md entries are left alone — they were true when written.
A shell that outlived atuin's daemon recorded nothing, silently, for the rest of its life. _core_atuin_daemon_guard was a startup probe: one zsocket connect at the first precmd, then it unhooked itself. That covers a shell started after the daemon went away and nothing else — so a long-lived tmux pane whose daemon stopped underneath it (the ordinary case under systemd Linger=no, where the user daemon dies with the last login session) kept handing every command to a socket nobody was listening on. atuin 18.19.0 neither falls back nor complains: atuin history start exits 0, prints a well-formed history id, writes nothing to stderr and discards the entry (atuinsh/atuin#3561 — and note the direction of travel, since 18.16.1 at least failed loudly). A day of history, gone, with no symptom until you go looking for a command you know you ran. The guard is now a watchdog. It stays on precmd and re-probes, throttled to at most one connect(2) every 60 seconds; when the window has not elapsed the per-prompt cost is a single arithmetic expression over three integers — no fork, no syscall — which is the honest version of this layer's startup-cost discipline. What that discipline forbids on the prompt path is an unconditional syscall, not the compare that decides whether to make one. The probe itself measures ~0.06–0.10 ms against a local unix socket, so the window could be far shorter; it is 60 s because precmd fires per prompt, not per second — which already bounds the probe rate by how fast you type — and because the throttle's real job is the socket path that is not local, where connect(2) can block with no timeout available. CORE_ATUIN_PROBE_INTERVAL is the escape hatch for such a box. Three properties are deliberate, and the suite now pins each. Degradation is one-way: the first failed connect disables the daemon for that shell and unhooks the guard for good. Direct writes always work, so a false positive — a probe landing in the shipped unit's RestartSec=3 gap, say — costs only the lock relief until the next shell, whereas the opposite error costs the history; and during that gap atuin is discarding, so degrading early is still right. The warning is mid-session only: a shell already degraded at its first prompt stays as silent as it has always been (nothing changed under it, and machines that simply never run the daemon must not learn a line of startup noise), while a shell that had a working daemon and lost it prints one _core_warn line — "once" being structural, since the degrade path unhooks before it warns. And the throttle fails safe: its deadline is honoured only while it is at most one window away, so a backwards NTP step, a resume from suspend, or the $EPOCHSECONDS/$SECONDS fallback changing source mid-shell all fall through to a probe rather than parking the watchdog for the length of the jump. core-doctor now says which of the two degradations happened, and core-doctor --json grows an atuin_daemon object so a statusline can see a silently degraded shell without the user going looking. The prose has been corrected along with the code, because it asserted the opposite trade: zsh/00-tools.zsh claimed "it is a startup probe, NOT a watchdog" and that "re-probing every precmd would put a connect(2) in the prompt path", and examples/atuin-daemon.service told you sessions already open "keep discarding". atuin/config.toml and PORTING-MATRIX.md footnote ²⁰ are updated to match.
The spinner could peg a CPU core for the entire length of the command it was decorating. _core_spin's animation loop is paced entirely by _core_nap, and _core_nap cannot report failure: it swallows both arms (zselect … 2>/dev/null, sleep 0.1 2>/dev/null) and unconditionally returns 0. On a box where neither can actually sleep — no zsh/zselect module and no usable sleep — the 100ms tick silently became an unthrottled spin. Measured on the real function under a pty: 100% CPU for the command's full duration, versus 0% with the guard, same wall time and same exit status either way. The animation is cosmetic and the wait is what matters, so the loop now detects a nap that is not pacing it (>200 iterations inside 5s, unreachable with a working tick) and stops animating rather than stopping the command, falling through to the blocking wait. lib/ux.sh's ux_spin carried the same shape around a bare sleep 0.1 and gets the same guard, keeping the bash and zsh spinners the deliberate mirrors they are documented to be. Two things made this hard to see and are worth recording: the loop is unreachable without a tty ([[ ! -t 2 ]] runs the command directly, so every captured or piped run takes the passthrough path), and where gum is installed _core_spin delegates real binaries to gum spin — leaving the hand-rolled loop live only for function arguments, which is exactly what up passes it (_pkgup_list_to). The new regression test therefore drives a real pty with a function argument, and asserts on the iteration count (~201 guarded, six figures unguarded) rather than on CPU%, which is not deterministic enough for CI.
/os-package-availability cited line numbers it never read. The macbook run on 2026-08-09 (dotfiles-MacBook#120) filed a correct green verdict — all 76 Brewfile entries re-verified as resolving — but pointed two of its citations at the wrong entries: dust at Brewfile:53 when :53 is duf, and gnu-sed at :64 when :64 is visidata. The names were right and the packages resolve, so nothing was mis-diagnosed; the reference was simply to a neighbouring line. Package lists make this the cheapest possible error — they are dense, every entry inserted above a name shifts it, and neighbours look alike — and the routine's reporting rules asked for file:line without ever saying to confirm the line. The prompt now requires reading the line before citing it, and forbids carrying a number over from a previous run's report, inferring it from a nearby entry, or quoting a grep hit it has not re-checked. A green run with wrong line numbers is the corrosive case: it invites the reader to distrust the citations that are correct, which is the whole value of an availability audit.
PORTING-MATRIX.md claimed two Gentoo atoms that do not exist. The Gentoo run of /os-package-availability on 2026-08-09 (dotfiles-Gentoo#80) returned a clean verdict for install/packages.txt — every atom in the Gentoo install list still resolves — but caught the matrix asserting main-tree packaging for two tools that are not in ::gentoo at all: dev-vcs/jujutsu (row + footnote 8) and dev-go/shfmt (row + footnote 7). Both 404 on packages.gentoo.org and return nothing on search. Re-verification went one step further than the report and closed off the obvious fallback: neither is in GURU either — the overlay carries no dev-vcs/jj, and the dev-go/shfmt atom exists in no repository anywhere (the third-party overlays that ship shfmt call it dev-util/shfmt). Both Gentoo cells now render as cargo³/go³, the footnote-3 convention already used for ouch, ast-grep, and sesh, and footnotes 7 and 8 say plainly that the tool is absent from the main tree and the overlay rather than naming an atom a reader would try to emerge. Footnote 7 had hedged ("verify the exact package on first stamp"), which is exactly the hedge that lets a wrong atom survive a review — a name in the Gentoo column reads as a promise that emerge <atom> works, and for these two it never did. No OS repo's packages.txt needed an edit: jj and shfmt are opt-in/dev tooling and were already carried in none of them, so nothing was ever installing the wrong name. The same pass caught a second, older error in footnote 8 that had nothing to do with Gentoo: the cargo fallback was written as cargo install jujutsu, and jujutsu is not the crate that installs jj. It is a stub pinned at 0.7.2 whose own description reads "You don't want this crate - you want the jj-cli crate"; the real one is jj-cli (0.44.0). That fallback is what the Debian/Kali cargo³ cell points at too, so the one wrong crate name had been the documented install route for every unpackaged distro, not just the two rows this change touches. It now reads cargo install --locked jj-cli, matching the --locked form the OS bootstraps already use for their own cargo builds.
Every atuin bench figure ever produced was labelled latency and was not. The harness timed history start and history end in one span, but a shell hook does not pay for the two calls the same way: atuin's _atuin_preexec takes start in a command substitution, so the prompt blocks on it, while _atuin_precmd fires end into a detached background subshell — (atuin history end ... &) — where it costs the box and nothing else. Timing the pair measures total write work; only start is time a human waits. The writer now takes a timestamp between the two calls (both metrics from one pass, so the tables are strictly comparable — same samples, same contention) and the results print as two clearly separated tables, latency first, each saying what it may be quoted for. This is not a presentational fix: it puts the far-tail conclusion back in question. The recorded finding that the daemon trades frequent small waits for rarer, larger stalls comes entirely from pair-timed runs, while the one measurement that timed only the blocking call (Fedora 44 under WSL2, systemd unit) found p99 improving 49–69%. end is precisely where the two would diverge — with the daemon off it is the slower call, and with it on they equalise. atuin/config.toml and zsh/00-tools.zsh now relabel their figures as total write work and mark the tail question open in both directions rather than settled against the daemon; the p50/p95 win is unaffected and still holds on every host tried. No new measurements are claimed here — this change makes the re-measurement possible. The parser is the risky half and is tested accordingly (test-core.sh Section J2): the previous one split each file on all whitespace, so two-column input would have flattened into one distribution of double the length — a table that looks completely normal and is completely wrong. The stats block is now extracted and executed against synthetic samples whose two columns differ, pinning that each table reports its own, and a malformed sample line refuses the arm instead of being coerced.
The atuin bench dropped an arm roughly one run in eight, and the reason looked like atuin misbehaving under contention. history start is not the only write a command makes: meta.db (and the key beside it) are created lazily by the first history end. The warmup in db_reset ran history start alone, so meta.db did not exist when the writers launched and all N of them raced to create and migrate it on their first history end — one losing on UNIQUE constraint failed: _sqlx_migrations.version, which aborted that writer at iteration 1 and cost the whole arm. It was always the first arm, because meta.db survived a db_reset that only ever removed history.db. The warmup now runs a complete start+end pair, and the snapshot/restore covers the whole data directory rather than one file — which also delivers what the old comment already claimed: records.db grew monotonically across arms before this, so each arm was measured against a bigger sync store than the one before it, exactly the variable being controlled for.
The bench could never detect musl, and mislabelled the one run where that mattered. ldd --version 2>&1 | grep -qi musl looks right but cannot work under the set -o pipefail in force at the top of the script: musl's ldd exits non-zero after printing its banner, so the pipeline fails even though grep matched. Every musl run therefore reported unknown libc and went on to list musl among the things it had not covered — on the one run where that was false, and on the cheapest of the remaining gaps. Now the output is captured and matched as a string.
The showcase was never told a release had happened, and had not been since the notification was written. notify-web.yml listens for release: published, but the Release is created by release.yml running gh release create under the built-in GITHUB_TOKEN — and an event raised by GITHUB_TOKEN never starts another workflow run (the same recursion guard that stops a GITHUB_TOKEN push from firing pull_request). So the Release published, the event was inert, and dotfiles-web's repository_dispatch: types: [core-release] received not one POST in its lifetime. Nothing about the dispatch itself was broken — right event type, right target, working token — which is why it read as healthy from both ends. User-visible downstream: the site's only remaining refresh was a Tuesday cron, so its committed generated.json sat two releases behind (v4.7.1 against Core's 4.9.3) and every published install command was pinned to a stale --branch. release.yml now dispatches core-release itself from a job after publish, where no guard applies; notify-web-call.yml grew an event_type input (default refresh, so the @v4 callers across the fleet are untouched), validated against an allowlist because a typo'd type POSTs 204 and triggers nothing. That job is best_effort, because sync-fanout gates on this workflow's overall conclusion and a failed notification must never be able to stop a published tag from reaching the OS repos. notify-web.yml keeps its release: trigger for a Release published by hand from the UI, and now documents the trap so the dead path isn't mistaken for the live one.
/os-package-availability could query a single release and still return "Clean" — the one verdict the routine exists to rule out. Step 1 said to confirm each name "still exists in this distro's repos" without ever saying which releases to look in, so a run against one release could not distinguish "present everywhere" from "already dropped in the next release" — and would report a version read from stable as evidence the name resolves, full stop. That is not hypothetical: the Fedora run filed a Clean verdict while tealdeer and procs had both gone orphan and neither had been rebuilt for rawhide/F45, quoting their F43/F44 versions as passes. Both still install today and break on the F45 upgrade, which is exactly the early warning this audit is for. The routine now picks targets by release model: versioned distros (Fedora, openSUSE Leap, Alpine stable) need every currently-supported stable release plus that distro's own development branch where one exists, while rolling targets (Arch, Gentoo, Homebrew, Kali, Tumbleweed) have a single current repo that is itself full coverage. It also requires every quoted version to name the release it came from; classifies "in stable, gone from that distro's development branch" as Drifted rather than a pass; and requires a Clean verdict to state its release coverage and reconcile N-checked against N-in-list, so a partial run has to call itself partial.
claude-routines-call.yml ran the routines from a frozen v3 checkout. The reusable workflow checks out dotfiles-core to get the routine prompt, PORTING-MATRIX.md and the pinned CLI, and pinned that checkout to ref: v3 — directly under a comment reading "Core@v4 at ROOT … (v4 = the current major, matching the @v4 callers)". v3 is frozen at v3.9.0 (2026-07-19) while the line has since reached v4.9.3, and the routine prompt differs between the two, so every scheduled run has been executing the v3.9.0 prompt no matter what shipped in v4 — including the fix above. Bumped to v4 so the callers and the content they run agree.
lint-call.yml and auto-tag-call.yml ran the fleet from the same frozen v3 checkout. The defect above was not confined to the routines workflow — these two reusable workflows carry it in the three remaining pins, and the lint one is the consequential half. Both check out dotfiles-core for the pinned scripts/tool-versions.env, the setup-core-tools composite and the release scripts, and pinned that checkout to ref: v3 while every comment beside them declared v4 (lint-call.yml:57 reads "v4 = the current major, matching the @v4 callers"; the auto-tag step said "pin to the SAME major line callers pin this workflow to (@v4)" and then pinned v3 in the same breath). So every OS repo's lint gate has been running v3.9.0's pinned tools — shellcheck 0.10.0, shfmt 3.8.0, actionlint 1.7.8 — while Core lints itself with 0.11.0 / 3.13.1 / 1.7.12: the fleet was held to a weaker gate than the repo defining it. Bumped all three pins to the moving v4 alias. Measured before bumping, against dotfiles-Fedora with the gate's exact SHELLCHECK_OPTS and file selection: shellcheck 0.10.0 → 0.11.0 is byte-identical (exit 0, no findings either way) and actionlint 1.7.8 → 1.7.12 likewise. shfmt is advisory by construction — the step wraps it in an if/else that swallows the drift exit rather than setting continue-on-error (lint-call.yml:156-170), so new formatting opinions in 3.13.1 can only warn; note that a genuine shfmt install failure still reds the step, which is the point of not using continue-on-error. So the bump is expected to be a no-op for the blocking legs rather than a new-findings event — verified on one repo, not all eight.
up and the maintenance runner could hang forever, invisibly, on a package manager that stopped to ask a question. dnf5 verifies repository metadata signatures against a per-repo, per-user keyring (<cachedir>/<repo>/pubring), not the rpm keyring. So a repo with repo_gpgcheck=1 whose signing key only ever reached root's keyring — the ordinary outcome of a bootstrap that runs sudo rpm --import and then sudo dnf install — re-prompts to import it on every non-root --refresh, and since a declined import is never persisted, it prompts again forever. Every probe that hits this runs with stdout captured by $(...) and stderr sent to /dev/null, so the question is invisible while it holds the terminal. _pkgup_count/_pkgup_list were documented as backgrounded, where zsh's nomonitor hands a job /dev/null stdin and the shape was accidentally safe — but up calls _pkgup_refresh in the foreground once the upgrade finishes, so it inherits the terminal and up prints Complete! and then never returns. The runner's upgradable-count block is not a step(), so it had no stdin discipline at all and the run stopped dead after the last ✓ with no error. Pin stdin so the probes cannot be prompted regardless of caller, give step() the same treatment — which closes the identical exposure on the git-credential and tpm paths — and bound the count probe with _to (MAINT_PKGCOUNT_TIMEOUT, default 180s) for the separate case of a mirror that accepts the connection and then stalls. The redirect goes on the case/fi, not the function definition: in zsh f() { … } </dev/null binds at definition time and does nothing at call time, so it reads as correct in review while fixing nothing. Regression tests assert the probes cannot consume the caller's stdin rather than asserting they don't hang — same property, but it fails instead of wedging a suite that has no timeout anywhere.
examples/atuin-daemon.service started the daemon by a deprecated name, and that failure mode is silent. ExecStart ran atuin daemon; 18.19.0 warns on every start and points at atuin daemon start. On its own that is cosmetic — but with the daemon enabled and unreachable, atuin exits 0, prints a well-formed history id, writes nothing to stderr and discards the entry. So the day the old spelling is removed, ExecStart fails and Restart=on-failure/RestartSec=3 retries it forever with nothing ever listening. Scoped honestly: a Core shell started after that is fine — _core_atuin_daemon_guard probes the socket at its first precmd, finds nothing, and forces the daemon off so atuin writes SQLite directly. The exposure is shells that had already completed that one-shot probe while the daemon was alive, and anyone consuming this unit without Core's guard — which, examples/ being a copy-paste target, is precisely who it is written for. The unit now asks the binary which spelling it has and execs that, because the subcommand does not exist on older atuin and this file is copy-pasted onto machines Core does not control. Two things that do not work and are pinned by tests: exec A || exec B (once exec succeeds the process is replaced, so a non-zero exit can never reach the ||), and probing with atuin daemon --help (exits 0 on both spellings, so it proves nothing — which is why dotfiles-Fedora's existing capability probe would have installed a unit the binary rejects). New scripts/test-core.sh Section J covers the file, including systemd-analyze verify; note it remains classified repo-meta by ci-classify, so an examples-only change still gates nothing.
exec zsh — the documented first step after a bootstrap — dropped you into zsh-newuser-install with no Core loaded. The managed ~/.zshrc exports ZDOTDIR=$XDG_CONFIG_HOME/zsh, but nothing ever created $ZDOTDIR/.zshrc. The first shell was fine (ZDOTDIR unset ⇒ zsh reads ~/.zshrc); every zsh started from inside it inherited the export, found none of .zshenv/.zprofile/.zshrc/.zlogin there, and was treated as a brand-new user. The wizard was the visible half — the real damage was a shell with no fragments, no plugins, no prompt. On a non-TTY there was no wizard at all, just a silently empty shell; and the wizard's own option (0) writes a comment-only $ZDOTDIR/.zshrc, permanently suppressing it while permanently keeping the shell empty. blib_write_zshrc_loader now seeds $ZDOTDIR/.zshrc as a link to ~/.zshrc (via blib_link, so it backs up, is dry-run aware, and is idempotent) — including on the already-managed early-return path, so boxes bootstrapped before this fix are reconciled on the next run rather than only on a fresh write. Note scripts/bench-core.sh and scripts/new-os-repo.sh already built the coherent $ZDOTDIR model, which is exactly why the suite never caught this; Section I of scripts/test-core.sh now asserts it.
The update nudge could report a Unix timestamp as the package count — e.g. 1786128391 updates available. _PKGUP_CACHE is positional ("<count>\n<epoch>") but both readers split it with an unquoted ${(f)…}, and zsh drops empty fields from an unquoted expansion. The empty count is not something _pkgup_refresh can write — it normalises an empty result to -1. It comes from the startup hook itself: on the first shell of a fresh box there is no cache, so the count it reads is empty, and claiming the throttle slot persists that empty field alongside a fresh timestamp while the background refresh is still in flight. Read back unquoted, the leading empty field vanishes and the epoch shifts into the count slot — where it passes the <1-> positive-integer check and renders. From there it is self-sustaining: last shifts to empty ⇒ 0, which defeats the once-a-day throttle so the check re-fires on every shell, each one rewriting the bogus count. Both reads are now quoted ("${(@f)…}"), and a non-numeric count is discarded before it can be written back, closing the race from the writer side too.
zsh/00-tools.zsh documented an atuin fallback that does not exist. The comment on _core_atuin_daemon_guard said an absent or stale daemon socket makes "every atuin call pay a failed connect and an error" and that "atuin then writes SQLite directly" — so a missing daemon "must cost latency". Measured against atuin 18.19.0, none of that holds: atuin history start exits 0, prints a well-formed history id, writes nothing to stderr, and discards the entry (verified for an absent socket, a stale socket file, and with and without --hook; the daemon-off control writes every row). The guard is therefore data-loss prevention, not a latency optimisation, and the "startup probe, not a watchdog" caveat is correspondingly sharper: a daemon that dies mid-session costs that shell every subsequent command, unrecorded and unannounced. Comment corrected; no behaviour change.
core.manifest advertised a keybinding that does not exist. Its zsh/35-fzf.zsh stanza named Ctrl-F/R for the fzf widgets; zsh/40-bindings.zsh binds ^T, and there is no ^F binding anywhere in Core — PARITY.md even records that zsh moved off Ctrl+F. A one-token error, but in the file the system calls its contract, vendored verbatim into eight repos, so it misinformed eight copies at once. Now Ctrl-T/R.
Three PORTING-MATRIX.md footnotes asserted "nothing installs this" against repos that do, and two of them contradicted each other: - ¹⁷ said jnv is in no Brewfile; dotfiles-MacBook/Brewfile carries it. Scoped to Linux, with macOS named as the exception. - ¹⁹ said no repo installs gping; the same Brewfile carries it. Same scoping. - ¹² listed gping among Gentoo's GURU-overlay atoms while ¹⁹ said nothing installs it. ¹⁹ was right: gping appears nowhere in dotfiles-Gentoo's guru_install list, its packages.txt, or its bootstrap.sh at all. Dropped from ¹².
The matrix sent Kali to mise/cargo for tree-sitter-cli, which it apt-installs. dotfiles-Kali/install/packages.txt carries the plain apt name and its bootstrap.sh has no tree-sitter installer, so the ³ footnote pointed at a path the repo never takes.
Footnote ⁹ named an AUR package that does not exist. It said sesh is "Packaged in the AUR (sesh)"; the AUR has no package under that bare name. The real one is sesh-bin, which declares provides/conflicts on sesh — so paru -S sesh resolves anyway, which is precisely why the wrong name read as correct. Confirmed against the AUR RPC: an info lookup for sesh returns nothing, and a name search returns eight packages, none of them a bare sesh, ruling out a source-build entry alongside sesh-bin. The Arch cell on the sesh row still reads AUR, which was always accurate; only the footnote was wrong.
lib/bootstrap-lib.sh still gave the atuin advice v4.9.3 corrected. It told you to re-apply a backed-up local config "via ATUIN_* env" with no carve-out — the exact pattern that release proved does not work for the ten keys atuin/config.toml sets. This was the last surviving instance; PORTING-MATRIX.md, examples/README.md and both OS layers were already correct.
A cross-reference dangled one release after it was written. The v4.8.0 correction note pointed at "the [Unreleased] entry on the daemon opt-in"; cutting v4.9.3 promoted that entry, leaving the pointer aimed at an empty section. Now names [v4.9.3] — the hazard of referring to [Unreleased] from a dated section at all.
Core told you it fans out to nine OS repos. It fans out to eight. scripts/os-repos.txt has been the canonical fleet — and has documented dotfiles-Windows and dotfiles-Debian as deliberately absent — for several releases, but five comments still asserted the old count: .github/workflows/release.yml, .github/workflows/ci.yml, scripts/update-nvim-plugins.sh, scripts/test-core.sh, and scripts/audit-core.sh. ARCHITECTURE.md is deliberately unchanged: "one Core plus nine machine repos" counts machine repos including Windows and is correct — the two numbers are both right in their own sentence, which is exactly why a find-and-replace would have broken it. (.github/workflows/release.yml, .github/workflows/ci.yml, scripts/update-nvim-plugins.sh, scripts/test-core.sh, scripts/audit-core.sh)
gsync was documented as an alias in a file deleted in v4. It is a function — zsh/20-aliases.zsh says so two lines above the definition, and explains why (a dotfiles path containing whitespace must stay one word). Three places carried the stale zsh/aliases.zsh path, a filename that has not existed since the v4 NN-name.zsh renumbering. (core.manifest, zsh/completions/_gsync, .bin/sync-upstream.sh)
blib_link_core's own comments under-sold what it links. The doc header omitted lazygit, jujutsu and the seeded sesh config; the tools group banner omitted jujutsu and atuin — both of which the code directly beneath it links. The complete enumeration already existed at the top of the file, so both now point at it as the canonical list. (lib/bootstrap-lib.sh)
Pre-v4 module names in comments that describe current behaviour. tools.zsh, options.zsh, ui.zsh and maint.zsh have been 00-tools.zsh, 10-options.zsh, 05-ui.zsh and 55-maint.zsh since v4. Note blib_migrate_v4 deliberately keeps the unnumbered names — it exists to delete stale pre-v4 symlinks, so there the old spelling is the correct one. (core.manifest, lib/bootstrap-lib.sh)
PORTING-MATRIX.md promised bootstrap installs that do not exist. The ³ marker means "bootstrap.sh installs it best-effort", but six cells carried it with no installer behind them — ouch and jujutsu on Gentoo and Kali, ast-grep and shfmt on Gentoo — verified against each repo's bootstrap.sh and install/packages.txt. Kali does install ast-grep, so that cell keeps its ³. A new ²¹ marker records the honest state, reusing the detect-only shape jnv¹⁷ and gping¹⁹ already established: available, not installed. It also covers four rows that are macOS-Brewfile-only in practice (hyperfine, shellcheck, shfmt, ouch — no Linux repo installs any of them), and lazygit on Kali, the sharpest case: every other Linux repo installs it, Kali installs it nowhere, and Core ships alias lg='lazygit' regardless. This is the same overclaim already corrected once for openSUSE. Alpine's ouch cell also gains the ¹⁴ testing-repo footnote every comparable cell already had. (PORTING-MATRIX.md)
The atuin-daemon table read as shipped state when it is mostly a recipe. The exports are wired on two of the seven Core-vendoring machines the table covers (Fedora, Alpine) — now marked ✔, with the other five labelled as the documented recipe and Windows called out as neither, being out of scope. The marker is per machine rather than per row, since the systemd row holds a wired Fedora next to four unwired ones. Defense is dropped from the systemd row: that row tells you to put exports in os/<os>.zsh, and Defense is distro-agnostic with no os/ layer, as the same file says under "Repo status". The Built: list also omitted Defense entirely. (PORTING-MATRIX.md)
dotfiles-Defense is now recorded as the documented scaffold exception. core.manifest claimed lib/bootstrap-lib.sh is sourced by each OS repo's bootstrap.sh; Defense hand-rolls its own link() and .zshrc heredoc instead. That is deliberate — Defense is a role layer stacking onto an already-provisioned host, where the OS repo underneath has already run the scaffold — so the claim is narrowed rather than the code changed. (core.manifest, PORTING-MATRIX.md)
README.md billed aliases.md as the "full" cheat sheet. It omits the function verbs core help indexes (fif, fbr, maint-*, op*). core help is the complete index and now says so; aliases.md is described as the curated companion. (README.md)
/doc-audit compared a release-pinned mirror against main, and reported a false positive. dotfiles-web's porting-matrix.md is diffed by its own CI against Core at releases/latest — the newest release tag, not main. The routine had no such carve-out, so it measured the page against main and called it "a pre-correction snapshot". It was not: it was byte-identical to Core at v4.9.3 and its check was green. Acting on that report re-mirrored main into a file whose contract is the tag and turned a passing check red, which is how it was caught. The routine now states the reference frame explicitly, and that a mirror lagging main while matching the newest release is correct, not drift. (.claude/commands/doc-audit.md)
The refresh row implied Arch has a refresh alias. It deliberately does not. sudo pacman -Sy was listed with no note, while dotfiles-Arch's os/arch.zsh explains at length that there is no -Sy alias on purpose — refresh-then-install is the partial-upgrade footgun, so it ships pacu (full -Syu) and pacout (checkupdates, which never touches the sync DB). New footnote ²³ records that the cell is completeness, not a recommendation. (PORTING-MATRIX.md)
fleet-drift now says how far behind main's tip an unreleased row still is (dotgibson/dotfiles-core#381). _classify measured the recorded sha against the release tag only, and git merge-base --is-ancestor is reflexive at both ends — so "on origin/main" was equally true of a repo synced this morning and one synced five weeks ago, and both printed the identical current (ahead of vX.Y.Z by N, on origin/main). A stalled fan-out was therefore invisible inside a green sweep: at the time of writing the whole fleet sat 56 commits past v4.9.3 while main had moved 111 past it, and nothing in the report named the 55 unvendored commits. The ahead-on-main row now appends , N behind its tip when that distance is non-zero. Report-only, and deliberately so. The current prefix, the • third state, the UNRELEASED tally, DRIFT/STALE/OFFLINEAGE and every exit code are unchanged — a green run stays green. The previous entry in this file taught readers that a fleet-drift wording change implied a verdict change; this one does not. Re-reddening the sweep once the fleet drifts far enough from main was considered and rejected: that is exactly the #371 failure mode where the fleet's ordinary between-release state pages a human, and the threshold would be unjustifiable. The two numbers now read as a pair — ahead of the tag says a release is owed, behind its tip says a make sync is owed. Zero omits the clause entirely rather than printing 0 behind its tip, which keeps an at-tip row byte-identical to its old wording — and makes the suite's existing …, on main) regex a live oracle for that case. _classify_subtree (dotfiles-Windows) deliberately gets nothing: its marker is re-stamped only when nvim/ changes, so a behind-main count there would report a lag for every Core commit that touched anything else — the exact false-BEHIND that the subtree path exists to eliminate.
/drift-triage can run the sweep it is built to interpret (dotgibson/dotfiles-core#381). The routine's own step 1 told it to run scripts/fleet-drift.sh — without the leading ./ that Bash(./scripts/fleet-drift.sh:*) matches, so every invocation was denied — and to pass the sibling fleet "via --add-dir", a Claude Code flag the script's parser rejects with a usage error. Neither is needed: --root already defaults to this repo's parent, which is where the fleet is checked out in CI too. The command is now spelled out literally as ./scripts/fleet-drift.sh --color never. The consequence was not a missing section but a wrong report: blocked from its primary tool, the routine reconstructed _classify's logic by hand from the core.lock markers, reached the opposite verdict from the script (red, when the sweep exits 0), and shipped it without a hedge. The command now forbids that explicitly — an unrun sweep is a finding to report, not a gap to fill in — and documents the three row states with the remediation each one actually takes, since a • unreleased row is fixed by cutting a release, never by the make sync the old text prescribed for everything.
The atuin latency question is closed, and the part that will never be measured is now recorded as a decision rather than a backlog (dotgibson/dotfiles-core#352). The measurable half is measured — see the bench-atuin-daemon.sh entries under Added. The remaining four rows (musl on real hardware, a real NFS/SMB home, bare metal, a real multi-pane session) need machines this project does not have and will not get, so atuin/config.toml now states plainly that their rationale stays borrowed from upstream on purpose. The mechanism is measured; what is borrowed is its magnitude on hardware nobody here runs. An open issue promising numbers that cannot arrive is worse than a documented decision not to chase them. Also corrects an overclaim this changelog and atuin/config.toml both carried: the earlier systemd-unit run was described as a second, independent Fedora host corroborating the new figures. It is the same machine — Fedora 44 / kernel 6.18.33.2 (WSL2) — measured days apart. That is reproducibility over time, not independent corroboration, and every figure in the record comes from one WSL2 host. Overstating corroboration is precisely the failure #352 was filed to catch, so it is fixed at every site that made the claim: atuin/config.toml, scripts/bench-atuin-daemon.sh's header, and both unreleased entries in this file — the one above and the earlier bench(fix) entry, which described the same run as "real Fedora hardware" too. That fourth site was missed on the first pass because the check that was supposed to prove the claim filtered CHANGELOG line numbers by a guessed section boundary instead of the actual ## [Unreleased] extent, and so excluded the line it needed to catch.
sd silently stopped matching across newlines, and its --version won't tell you. Upstream 1.1.0 made line-by-line processing the default and moved the old whole-file behaviour behind --across / -A. Nothing in Core breaks — sd is detect-only (HAVE_SD) and deliberately un-aliased, and no Core code shells out to it — but a multiline pattern in muscle memory or in a role script now matches nothing, leaves the input untouched, and still exits 0, so the caller carries on as if it had rewritten the file. Verified behaviourally rather than read off the release notes: sd 'alpha\nbeta' X on two-line input returns rc=0 with the input unchanged, and sd --across matches. This earns a PORTING-MATRIX.md footnote (²²) rather than a detection change for two separate reasons. Core needs no runtime change: nothing here calls sd, so there is nothing to gate. And the version string could not carry a gate anyway — the Homebrew 1.1.0 build self-reports sd 1.0.0, so HAVE_SD could never have keyed off it. Consumers that genuinely must know — a role script targeting both builds — feature-detect instead, with sd --help | grep -q -- --across, and add -A only when the probe says the flag exists; hard-coding it breaks the pre-1.1.0 builds this matrix tracks, which already match whole-file. Same class of footnote as batcat (⁴) and the mikefarah-vs-kislyuk yq split (⁶): the command is not quite what its name implies. Found by the weekly /tool-scout scan (#376).
pre-commit moved off a known-broken patch: 4.6.1 → 4.6.2. 4.6.2's sole content is a fix for a 4.6.1 regression in language: node hooks whose package.json declares a scripts.build key, under npm 11.x (pre-commit#3737). It is not fixing a live failure here — markdownlint-cli2 is Core's only node hook, and v0.23.2's manifest carries build-docker-image and friends but no plain build, so it misses the trigger condition. Taken anyway, on the principle that sitting on a patch upstream has already superseded is a bet the next hook addition doesn't collect. One line in scripts/tool-versions.env; the three consumers (ci.yml, scripts/setup.sh, .devcontainer/devcontainer.json) all read the variable, so no literal moved with it. No checksum refresh applies — PRECOMMIT is a pip install, not a raw release download, so it carries no *_SHA256 and is absent from both scripts/update-tool-checksums.sh and the audit's section 9b. No .pre-commit-config.yaml change either: the audit's version-consistency section gates PRECOMMIT_HOOKS_VERSION (the hook repo's rev:), never the pre-commit binary. Of the nine remaining pins, the eight gate tools were checked against upstream in the same pass and are current; CLAUDE_CODE_VERSION is the one exception, deliberately left at 2.1.222 with 2.1.227 available — it changes the scheduled routine bots' behavior, and moving it alongside an unrelated fix would make a later routine regression ambiguous to bisect. It moves on its own.
The daemon's contention claim now has a musl number. Measured in an Alpine 3.21 container (real Alpine userland, real musl, no systemd) against a glibc control on the same host, atuin 18.19.0, two runs each. The p50 win holds and is the most robust result so far (~1.4x on both libcs), but the far tail is where they diverge: on musl the p99 was worse with the daemon on both runs — a stable sign, where glibc gives a coin flip. That is the strongest evidence yet against selling the daemon as a tail fix, and it lands on the path Alpine actually ships. atuin/config.toml carries the table and the caveat that a container is not real hardware.
The @vN pinning policy is no longer stated as universal, because it is 27 of 28. dotfiles-Windows SHA-pins its auto-tag-call caller on purpose — immunity to a moved tag, traded against the auto-fan-out — and both RELEASE-RUNBOOK.md and RELEASE-STRATEGY.md read as though every caller tracks @v4. Worse, the runbook's own straggler sweep (grep -rl 'uses:.*@v4' across scripts/os-repos.txt) structurally cannot find it: Windows vendors no core/, so it is not in that list. It is therefore invisible to the grep and unmoved by the alias — currently several releases behind. Both documents now name the exception and say to check it by hand.
The daemon rationale in atuin/config.toml and zsh/00-tools.zsh now reports what was measured, and it is not the whole pitch. A container run reproducing the topology of the Alpine path (no systemd, XDG_RUNTIME_DIR unset) puts the median and p95 win at ~1.4× and 1.2–1.3× — real, and steady across runs. But p99 flips sign run to run and the maximum is consistently ~2× worse with the daemon on: it trades frequent small lock waits for rarer, larger stalls. "Removes the tail latency" was therefore an overclaim in both files and is now scoped to the typical command rather than the worst one. The autostart path's first command additionally pays ~+41 ms for the spawn. Still unmeasured and still needing hardware nobody has to hand: musl, the systemd-unit path, and a network home — where the claim is strongest and least tested.
Plugin pins rolled forward. Routine freshness sweep, landed by the bot and previously unrecorded here. Six Neovim plugins in nvim/lazy-lock.json (fzf-lua, nvim-lspconfig, nvim-tree.lua, nvim-treesitter, package-info.nvim, schemastore.nvim) and the zsh zsh-syntax-highlighting pin in zsh/45-plugins.zsh. Pins are what stop plugins floating silently into eight repos, so every roll is a change those repos receive on their next sync — CONTRIBUTING.md requires it in the changelog, and there is no carve-out for automation. (nvim/lazy-lock.json, zsh/45-plugins.zsh)
The atuin daemon opt-in never worked. ATUIN_DAEMON__ENABLED=true was silently ignored on every machine. Core shipped atuin/config.toml with [daemon] enabled = false written out explicitly, and that assertion is what broke it: atuin builds its config as defaults → environment → config file, adding the file source last (settings.rs — the Environment source goes in at the builder, the file at build_config() afterwards), and in the config crate the later source wins. So any key this file mentions **shadows its ATUIN_* override. The one key the whole per-OS design depends on being overridable was the one Core asserted. The fix is to write no value: enabled and autostart are now left unset. Upstream's own defaults are already false/false (settings.rs:1515-1516), so Core still ships the daemon off — off by default rather than off by assertion — and the override reaches it. Measured, not reasoned, against atuin 18.19.0 built from crates.io. atuin doctor reports the resolved daemon_enabled, and the client was straced for connect() on the socket, which is the only thing that distinguishes the two paths — exit codes cannot, because the client degrades silently to direct SQLite when the daemon is unreachable, which is exactly why this went unnoticed: | Config | daemon_enabled | connect(atuin.sock) | | --- | --- | --- | | enabled = false written + ATUIN_DAEMON__ENABLED=true | false | 0 calls | | key absent + ATUIN_DAEMON__ENABLED=true | true | 1 call | | no config file at all + ATUIN_DAEMON__ENABLED=true | true | — | | after this change, no env | false | — | | after this change, ATUIN_DAEMON__ENABLED=true | true | 1 call | What this was costing the fleet: dotfiles-Fedora's bootstrap installed and enabled a systemd unit that started a daemon no client ever talked to, and dotfiles-Alpine's exports were inert. Core's guard made it quieter still — it reads ATUIN_DAEMON__ENABLED from the environment, where it was set, so on Fedora it found the unit's socket present, stood down satisfied, and reported healthy while every write went straight to SQLite. Nothing was broken for a user; the feature simply did not exist. scripts/test-core.sh now asserts the two keys stay unset, negative-tested by putting enabled = false back and watching it fail. The check is static because the behavioural proof needs an atuin binary CI does not have. The same trap applies to any future per-machine key — asserting even its default disables the override — which is now stated in the config header, PORTING-MATRIX.md footnote 20, and beside the block itself. Three follow-ups from review, all of them the same defect wearing other hats: - The guard scanned only inside a literal [daemon] table, so the equally valid dotted form daemon.enabled = false at top level recreated the bug and passed green. Widening the regex was still the wrong shape — daemon = { enabled = false } and daemon . enabled = false are also valid and also deserialize to the same key, so a pattern match can only ever cover the spellings someone thought of. The guard now parses the TOML with tomllib (the idiom audit-core.sh's config gate already uses) and inspects the resolved daemon table, which is what atuin itself resolves. All four spellings negative-tested; an unparseable file fails distinctly rather than being read as clean. - The config header advertised export ATUIN_SEARCH_MODE=prefix as its example of an override — while the same file writes search_mode = "fuzzy", which makes that export silently ignored. Documenting the precedence trap and then demonstrating it was the worst of both. The example now uses sync_address, a key the file genuinely leaves unset, and the header names the ten settings that are deliberately not** overridable so the distinction is explicit rather than inferred. - The v4.8.0 upgrade note told adopters to port sync_address, auto_sync and filter_mode to ATUIN_* overrides. The first two work; filter_mode is written by this file and cannot. That entry now carries the correction inline rather than being quietly rewritten — it was wrong when shipped, and the record should say so. Verified against atuin 18.19.0 rather than assumed: with Core's config in place, ATUIN_SEARCH_MODE=prefix still resolves to fuzzy and ATUIN_FILTER_MODE=prefix still resolves to global.
psmux power pill — the battery segment the macOS tmux bar has. New psmux/scripts/psmux-power.ps1, rendered right-most in status-right, which is where Core puts it too (its last slot is #{@status_right_os}, the hook each OS repo fills). It is the Windows port of Core's tmux/scripts/tmux-battery.sh and uses that scale, so the two terminal bars agree: green ≥60 / yellow ≥20 / red <20, with the level glyph swapped for a charging bolt on AC and the colour still tracking the level. One deliberate divergence — Core prints nothing when there's no battery, so its segment vanishes on a desktop; here it falls back to Zebar's AC placeholder, a lone green md-power-plug , since an empty segment reads as a broken pill on a desktop-first host. Power state comes from SystemInformation.PowerStatus (one in-process GetSystemPowerStatus read), not Win32_Battery — a desktop returns nothing from the latter, so "no battery" and "the query failed" would be indistinguishable. Refreshed by the existing in-session timer alongside the VPN pill, so nothing new touches psmux's synchronous render path. psmux.conf seeds @pwr_pill with set -og (only-if-unset) so the desktop plug is right before the first tick, while a prefix + r reload can't clobber a live laptop reading. (psmux/, powershell/os/33-psmux-pill.ps1) Note this means the psmux bar and Zebar disagree between 40 and 60 % — deliberately. The bars are matched terminal-to-terminal (psmux ↔ Core tmux) and desktop-to-desktop (Zebar ↔ sketchybar), and those two references use different scales.
Test coverage for the power pill's every state. The dev box is a desktop, so the laptop branches would otherwise ship unexecuted. psmux-power.ps1 takes a -SimulateState testing seam (no host read, no poke) and tests/Repo.Tests.ps1 asserts each colour and glyph threshold — including that a charging 15 % battery stays red, which is the case a naive "on AC → blue" reading would silently hide.
The package-freshness check now validates its own inputs — a wedged scoop bucket is a finding, not a silent green. A bucket is a git clone, and a stuck clone keeps serving manifests from whatever commit it froze at. Those stale versions still parse and still compare as matching, so the check reported "everything's current" on data months old — wrong in the reassuring direction, the worst way for a check to fail. That is not hypothetical: on 2026-08-04 the local extras clone had been stuck mid-merge on an upstream rename (UD bucket/pycharm.json) since mid-July, so scoop status called lazygit and tailscale "latest version" while the CI bot correctly had them behind. The box contradicted CI and the box was wrong. Check-PackageFreshness.ps1 now checks every bucket it reads manifests from — present, a real clone, not stuck on a merge/rebase/cherry-pick, clean tree — and writes a report even when nothing looks outdated, since that silent-green case is the entire point. The warning leads the issue body, because it invalidates every row under it. Also catches a bucket the scoop bucket add loop failed to create (its catch is empty), which today degrades quietly into a "no manifest version" skip for every app in it. Unit-tested via a new DOTFILES_PKGFRESH_LIBONLY hook, matching the *_LIBONLY idiom the sync scripts use. (packages/Check-PackageFreshness.ps1, tests/Packages.Tests.ps1)
dotfiles-doctor now checks scoop bucket health too, because CI structurally can't. The guard above lives in a script whose CI runs on a fresh runner, where buckets are added moments earlier and are always clean — so it protects the local-run path but can never observe the box this actually happened on. The wedge was a local condition that made the machine disagree with the bot for three weeks, and the doctor is where "is this box healthy" belongs. New Scoop buckets row under Health & toolchain: 6 bucket(s) clean and pullable when fine, and on a fault it names the bucket, says why (stuck mid-merge (MERGE_HEAD), dirty tree, missing directory, not a clone) and hints the exact unwedge. warn, not fail — nothing is broken and no tool is missing; the box just can't be trusted to tell you what's current. The detector is reused from packages/Check-PackageFreshness.ps1 through its DOTFILES_PKGFRESH_LIBONLY hook rather than reimplemented, so there's one definition of "this bucket can't be trusted"; the dependency deliberately only points this way, since the freshness bot must stay self-contained for CI, where the Dotfiles module isn't installed. The whole probe is wrapped so a bucket check can never take down a doctor run. (powershell/Dotfiles/Doctor.Helpers.ps1, powershell/os/45-doctor.ps1, tests/Doctor.Tests.ps1)
The load-budget perf test was measuring the runner, not the code. Perf.Tests.ps1's "dot-sources the tool-independent fragments quickly" timed a single cold dot-source, so it also charged the fragments for PowerShell's one-time parse/compile and module autoload — work they don't do. On a shared GitHub runner that noise is unbounded, and on 2026-08-05 it landed a CI run at 3012 ms against the 3000 ms budget: a 0.4 % overshoot on a body whose real cost is roughly 100× under the gate. A re-run passed untouched, which is the tell. A red CI that actually means "the runner was busy" is worse than no gate at all, because it teaches you to re-run instead of read. Now: one untimed warm-up, then the fastest of three timed runs. Noise only ever adds time, so the minimum is the closest estimate of true load cost — while the regression this exists to catch (a network or subprocess call added to a load path) is slow on every run and still trips it. The 3000 ms budget is deliberately unchanged; raising it would have hidden the flake instead of removing it. (tests/Perf.Tests.ps1)
The VPN/IP pill never rendered — a PowerShell splatting bug. psmux-netinfo.ps1 poked the bar with psmux set -g @vpn_pill $text. In argument position a bare @name is PowerShell's splatting operator, so the undefined $vpn_pill expanded to nothing and the option name was dropped from the command line entirely; psmux received a single positional and silently discarded the whole command — exit 0, nothing on stderr, option never set. Every other layer (detection, cache file, timer, psmux.conf) was working, which is why it survived so long. Fixed by quoting '@vpn_pill' / '@vpn_fg'. (psmux/scripts/psmux-netinfo.ps1)
A config reload repainted a live pill in the wrong colour. @vpn_fg was defaulted with a plain set -g, so every prefix + r overwrote whatever the refresher last poked. Because the pill's text is never defaulted, the two halves then disagreed until the next tick — up to a full refresh interval — and these pills encode their state in the colour: an active tunnel kept showing its address in the no-tunnel green, losing the orange that is the entire signal. Both colour options now use set -og (only-if-unset), which still guarantees a non-empty colour on first paint. Caught on review of the same mistake in @pwr_fg, where it would paint a 15 % battery healthy-green. psmux set -g @vpn_pill '', but an empty-string argument is dropped on the way to the exe and the set no-ops exactly like the splat above. Clearing now uses set -gu (unset). psmux-pill-disable clears the segment too, instead of only stopping the timer.
Holding the prefix key shoved the IP pill two columns right. The prefix/mode indicator sits between #S and the pill with each branch padded to the same width — but the idle branch was three literal spaces, which psmux's parser collapsed to one (see the next entry), against a prefix branch of space + glyph + space that survived as three. The branches are now spaced with #{p<n>:} and are five rendered cells each, verified in both directions on a real terminal. A test asserts the three branches stay equal width, since eyeballing this is exactly what failed before.
Multi-space gaps in the status bar were rendering as a single space. psmux parses option values as split_whitespace() + join(" "), so every run of spaces collapses to one, quoted or not — which means the twelve-space cwd→clock gap added in #163 had never actually widened anything. Bar gaps now use #{p<n>:}, which pads an empty body at render time — after the parser has had its way — and is the same idiom Core's tmux.conf already uses (#{p19:}), so the two configs now read the same. The session→IP gap is wider as a result, and a test forbids multi-space runs in status-left/status-right so this can't silently regress. Note #{p<n>:} only works written directly in the config: a format arriving via a user option is not re-expanded, which is the same rule that keeps a #[…] style run from working inside @vpn_pill.
Two stale psmux config tests. They asserted the pill was read via #(cmd /c type %LOCALAPPDATA%…) and passed only because that string still appeared in the comment block describing the retired transport — they had stopped testing anything real. Repointed at the live @vpn_pill / @pwr_pill segments, plus a static guard that every psmux set in the repo quotes its @option name, since the splatting bug above is invisible at runtime. (tests/Repo.Tests.ps1)
jnv — interactive JSON explorer (fleet parity with Core's HAVE_JNV). Added to packages/scoopfile.json (scoop Main). A jq-filter editor with a collapsible viewer that fills the "explore an unfamiliar JSON response" gap between jq (transform) and gron (grep). Its own command with no alias — jnv file.json or pipe into it — like jq/yq/ gron. This also retires the old jless-was-left-out caveat in docs/TOOLS.md: jnv is the packaged interactive explorer now.
web — terminal web browser verb (parity with Core's web). Added a guarded web function in core/00-aliases.ps1 that resolves w3m→lynx→links→elinks and runs the first present (skipped entirely when none is installed, matching Core). w3m has no scoop manifest, so the host packages lynx (Core's own next fallback; scoop Main) in scoopfile.json. Unlike Core's headless path, $BROWSER is never exported — the Windows host is GUI-first, so web stays an explicit opt-in verb.
**Command-block separators (parity with Core's _cmd_block_*). A thin full-width rule is drawn above each prompt that followed a command, colored by exit status — dim (#414868) on success, red (#f7768e) on failure — turning scrollback into scannable blocks. Ported as precmd/preexec (not** a key handler, so it can't collide with PSReadLine vi-mode): the AddToHistoryHandler sets $global:DotCmdBlockRan (a bare Enter never accepts a line, so no rule is drawn on an empty prompt), and Invoke-Starship-PreCommand draws the rule via [Console]::Write. Colour tracks $LASTEXITCODE (the status reliable at that point); pure-cmdlet failures still surface in starship's [status]. (powershell/core/10-tools.ps1)
Zebar caffeine / keep-awake indicator. A placeholder matching sketchybar's caffeinate.sh — grey asleep, yellow awake — in the left island. Visual only for now: it renders state but doesn't yet drive a keep-awake mechanism on the host (see the Caffeine component comment in the HTML). (desktop/zebar/vanilla-clear/)
Zebar battery shows an AC-power placeholder on desktops. A machine with no battery rendered nothing at all, leaving a gap in the right island; it now shows a green plug glyph. (desktop/zebar/vanilla-clear/)
Re-vendored nvim/ from Core (was v4.4.0-3, now current). Brings the Core changes that had not yet reached the host: the regex Tree-sitter parser (silences :checkhealth noice's "regex parser is not installed" cmdline-highlighting warning), the new :checkhealth gerrrt LSP / formatter / linter readiness sections, and the servers/init.lua read-only status() export those sections consume. nvim/.core-ref updated to the synced commit. (nvim/, via nvim-sync.ps1)
Re-synced nvim/ to Core main a53ac4f — a follow-up mirror picking up the lazy-lock.json plugin-pin refresh (4 SHAs); nvim/.core-ref re-pointed from the pre-merge branch tip to main. starship.toml verified byte-identical to Core (no sync needed). (nvim/lazy-lock.json, nvim/.core-ref)
Windows Terminal cursor → bar — cursorShape filledBox → bar to match MacBook's ghostty cursor-style = bar for cross-terminal parity. (windows-terminal/settings.json)
psmux status bar → Core's centered floating-island look. Ported Core tmux.conf's island redesign to psmux/psmux.conf: a 2-line, centered, transparent bar (status 2 + blank status-format[1], status-justify centre — later absolute-centre, see Fixed — status-style bg=default, bg=default pill caps + pane borders) with flat underlined window tabs and monitor-activity • dots for unseen output (psmux has no monitor-bell) — replacing the old left-justified opaque-pill bar. All five psmux features were probed as supported (psmux 3.3.7) before porting. Stays within psmux's no-shell-out / no-process-table rules: the cwd pill keeps #{b:pane_path} (OSC 7) and Core's nvim-gated pane_current_path segment is intentionally not ported. (psmux/psmux.conf)
Zebar adopts sketchybar's floating-islands design, and PARITY.md now describes it. The macOS bar had drifted to a 3-island look (transparent bar + bordered panels) without the shared contract being updated, so PARITY.md was false for one host. Resolved by adopting, not reverting: the bar goes transparent and .left/.center/.right each become a rounded island (rgba(29,32,47,0.93) fill, 2px rim, r=9) accented blue/magenta/ green. Weather moves into the left island (stable-width, non-urgent); two grey │ separators chunk the right island into I/O · load · power, each gated on its own group so a provider-startup transient can't leave a stray separator leading the island. PARITY.md (identical copy in dotfiles-MacBook) rewritten to match: three islands, weather left, transparent bar geometry, blur off, purple un-reserved and orange added to the palette. Not render-verified on a Windows host — reload Zebar and eyeball. (desktop/zebar/vanilla-clear/, desktop/PARITY.md)
psmux bar is chip-less. Dropped the rounded pill caps (@cap_l/@cap_r) from the session / cwd / clock / IP segments in favour of plain coloured icon+text on the transparent bar — matching sketchybar, Zebar, and PARITY.md's "items are chip-less" spec. Also fixes the prefix and copy-mode glyphs being clipped by the cap they sat against. (psmux/psmux.conf, psmux/scripts/psmux-netinfo.ps1)
Default psmux session renamed main → Gerrrt — both the 30-windows.ps1 auto-launch and the mux verb default in 32-psmux.ps1, with the docs/comments that still said main updated to match. (psmux/, docs/TOOLS.md, TERMINAL_WORKFLOW_GUIDE.md)
Zebar weather reads °F instead of °C (fahrenheitTemp). (desktop/zebar/vanilla-clear/vanilla-clear.html)
Zebar workspace pills match sketchybar's aerospace.sh. Only the focused workspace is highlighted (blue background, dark text); every other one is a plain grey number with no chip — GlazeWM's .displayed distinction is deliberately dropped for macOS parity, since aerospace shows only the single focused workspace. (desktop/zebar/vanilla-clear/styles.css)
Zebar spacing and font tuning from live use on a large external monitor. --item-gap 20px → 8px to match sketchybar's per-item padding (padding_left 4 + padding_right 4); left-island items dropped from a 16px to an 8px margin so both islands read at the same density; --bar-font-size 16px → 18px, since sketchybar's ~17pt suits a laptop panel but reads too small on a large panel (still fits the 28px island; 20px is the next comfortable step). (desktop/zebar/vanilla-clear/styles.css)
psmux tabs no longer drift off-center — status-justify absolute-centre. Plain centre (Core's value) centers the window list in the gap between status-left and status-right, so the host's variable-width session pill (wider while prefix is active) and the #{b:pane_path} cwd in status-right pushed the tabs off the true middle — most visibly as a jump when a pane running nvim widened the right float. Switched to absolute-centre, which anchors the tabs to the bar's absolute center regardless of either float's width. Deliberate divergence from Core's centre (see docs/PORTING-NOTES.md); probed on psmux 3.3.7. This is the real fix for the tab-shifting the equal-width prefix cell below was working around. (psmux/psmux.conf)
psmux IP / VPN pill rendered blank. Two distinct causes, found in that order. First, psmux.conf ran set -gq @vpn_pill "", which clobbered the refresher's poked value on every source-file reload — removed, so #{@vpn_pill} persists what the refresher sets. The segment still rendered empty, because the option was poked as a single pre-styled string ('#[fg=#9ece6a,bold]<glyph> <ip>') and an option value embedding a #[…] style run is not re-interpreted when the format expands it. That's why psmux-pill-status showed a populated cache (a plain file write, a separate code path) while the bar stayed empty. Split the transport to mirror the proven @tn_* colour pattern: @vpn_pill carries plain text only, @vpn_fg carries the accent hex, applied in status-left as #[fg=#{@vpn_fg}]#{@vpn_pill}. Only the colour is defaulted in the conf (so it's never empty on first paint); defaulting the text is what caused the original clobber. (psmux/psmux.conf, psmux/scripts/psmux-netinfo.ps1)
psmux prefix indicator style leaked into the window tabs. A #[default] reset after #{@vpn_pill} stops the pill's bold/fg bleeding into the tabs. The indicator was also widened to an equal-width padded cell ( / idle ) so its branches couldn't shift the tabs — kept for stable width, though absolute-centre above is what actually holds the tabs still. (psmux/psmux.conf)
psmux nvim cwd jammed against the clock. Two passes: the status-right cwd↔clock gap sat inside a #{?} branch and psmux trims in-branch trailing spaces, so it was moved outside the branch — then widened (6 → 12 spaces) once the branch fix made the gap actually render and it was still too tight to read. (psmux/psmux.conf)
psmux config warning: unknown option 'monitor-bell'. psmux 3.3.7 doesn't implement monitor-bell at all — its CLI setw/set returns exit 0 (so the capability probe was a false positive) but the config parser rejects it on load. Removed the setting; monitor-activity (which psmux does support) stays, so activity dots still work — only the bell dot is inert. (psmux/psmux.conf)
Zebar network readout rendered white instead of blue. The .network module had no color class, so only its glyph got the global blue while the ↓↑ throughput values fell back to fg (white). Added .network { color: var(--tn-blue) } so icon and values are blue, matching sketchybar's network.sh (icon + label accent). (desktop/zebar/vanilla-clear/styles.css)
:checkhealth gerrrt no longer false-warns about the clipboard on the host. It read "Core's cross-OS clipboard scripts are not on PATH (clip: found, clip-paste: missing)" — misleading, since clip only resolved to Windows' built-in clip.exe and the Unix/WSL clip/clip-paste ladder does not apply on the host: config/clipboard.lua wires the clip-windows provider (clip.exe copy + PowerShell paste) instead. Fixed upstream in Core (health.lua now detects native Windows via has("win32") and defers to :checkhealth vim.provider for the live backend) and pulled in with the nvim re-vendor above.
Documented the psmux :checkhealth tmux cosmetic wart in docs/PORTING-NOTES.md: psmux has no show-option verb, so Neovim's built-in vim.health tmux probe shows ❌ ERRORs and a false "true color could not be detected" ⚠️ — cosmetic only (psmux renders 24-bit colour natively; nothing functional is affected), and not shimmed on purpose.
Refreshed the stale "re-vendor nvim/" manual step (the full tree is now vendored via nvim-sync.ps1, and the old <leader>rc keymap wart is fixed upstream), and dropped the now-inaccurate "Known Windows wart" banner nvim-sync.ps1 printed after each sync.
Dotfiles.psd1 Author is now dotgibson, not the Gerrrt personal account. The repos moved to the org, but the module manifest still presented the personal account as the owner — the last spot in the fleet doing so. Metadata only: nothing resolves this field, so it's a naming/identity fix rather than a functional one. The remaining Gerrrt references are all correct and deliberately untouched — the nvim/lua/gerrrt/ namespace and Gerrrt* highlight groups (internal identifiers, not paths), historical CHANGELOG entries recording the migration itself, and attribution to Gerrrt/make-windows-pretty / Gerrrt/yasb-glazewm-config, which are genuinely external upstreams still living on that account.
Check-PackageFreshness.ps1 no longer reports padded version strings as updates. The lock is captured from winget export, which pads versions to four components, while winget show reports the source's own form — so 2.7.10.0 in the lock and 2.7.10 upstream are one build written two ways. The check compared them as raw strings, so three of the four packages in its 2026-07-21 report (Microsoft.WSL, QL-Win.QuickLook, CharlesMilette.TranslucentTB) were flagged as behind when they were current — and would have been flagged again every week, since re-pinning cannot fix a difference that isn't real. New pure helper Test-PackageVersionMatch in PackageLock.ps1 compares component-wise with absent trailing components read as 0, and falls back to exact string equality when either side isn't purely numeric-dotted (prereleases, scoop's date+hash strings, nightly), where there's no safe numeric reading. Applied to both the scoop and winget comparison sites. Unit-tested offline in tests/Packages.Tests.ps1.
Added
- Fixaudit-core.sh §5d — a gate for the pipefail + SIGPIPE trap this repo keeps hitting. Under set -o pipefail, piping into a reader that exits early turns a success into a failure: grep -q stops on its first match, awk on its exit, head after N lines, the writer takes EPIPE and dies with 141, and pipefail reports the pipeline as failed even though the reader matched. Three occurrences so far. Two were found and fixed by hand — a 4000-line git show into grep -q reporting "no heading" on a file that had one, and ldd --version | grep -qi musl reading false on every musl box, whose assertion is still named "the pipefail trap this repo has hit before". The third broke main: nvim-reachability.sh invented two orphans because a visited module's lookup returned 141. Each fix included a sweep of the tree, correct at the time and unable to cover code written afterwards. The gate is scoped to a shell-string producer (printf/echo) feeding an early-exiting reader, in files that actually set -o pipefail. That shape converts to a herestring with no behavioural difference and no reason to prefer the pipe, so a finding is never a judgement call. sed <file> | head -n1 — a file producer, ~15 instances — is deliberately out of scope: converting those is not free, and a gate that fires fifteen times on working code is a gate someone turns off. Four existing instances converted (check-modern.sh, parity-check.sh, test-core.sh ×2). All fed small values and none was a live bug — which is precisely why a hand sweep leaves them, and why the next author copies the shape somewhere the producer is large. The scanner lives in scripts/lib/common.sh as _core_pipefail_hits, beside _core_fail_digest and for the same reason: so test-core.sh can drive it on fixtures. A gate for a bug that has recurred three times is only worth having if it demonstrably fires, and probe-testing caught a defect in this one before it shipped — it used to scan any file that merely mentioned pipefail in a comment, which is the false-positive class that gets a gate switched off. Eight assertions pin both halves: the three banned reader forms are caught, and the herestring fix, a comment describing the hazard, a file with no pipefail, a file producer, and the library's own definition are all left alone.
- Perfaudit-core.sh §4b — the nvim orphan backstop core.manifest claimed already existed. core.manifest lists nvim/ as a directory rather than per-file, because a vendored lazy.nvim tree churns wholesale and per-file listing would be noise. The stated justification was that "verify-core.sh (byte-for-byte vs upstream) is the orphan backstop here instead" — but verify-core.sh has never existed in this repo. So from the day nvim/ went directory-granular, §1's manifest⇄fs check auto-listed every new path under it and nothing else looked: a lua module nothing loads could sit in the tree indefinitely and fan out to all eight OS repos, silently. luacheck does not help — it lints the files it is handed and does no reachability analysis. §4b walks the load graph instead — a real traversal, not an "is this name mentioned anywhere" scan. That distinction is the whole point: a mention-scan passes two dead modules that require each other (a disconnected cycle, non-zero indegree, reachable from nothing), and passes a module named only in another file's comment. Both are exactly the orphan this exists to catch. So it inventories every module, strips lua comments, reads each file's edges, then walks outward from the roots and flags every module never visited. Only real load expressions count as edges — the module name must be preceded by require (covering require("x"), require "x", and pcall(require, "x")) or by lazy's import =. Matching every quoted gerrrt.* string would still be a mention scan: health.lua deliberately peeks package.loaded["gerrrt.servers"] precisely so it does not load the registry, and counting that as an edge would let the whole servers/ arm look reachable from the health root even with every real require("gerrrt.servers") deleted. Comment stripping handles --[[ … ]] blocks across lines too, since a line-only stripper leaves the block interior searchable. Two roots, both genuine entry points rather than exemptions: nvim/init.lua, and gerrrt.health — which Neovim discovers by runtimepath for :checkhealth, so nothing requires it and nothing should. Two edges cannot be read literally from source and are resolved during the walk: a directory import (gerrrt.plugins names a directory, so a target with no file expands to its target.* children, as lazy does), and the dynamic require in servers/init.lua, which does pcall(require, "gerrrt.servers." .. name) over its servers list — so visiting gerrrt.servers expands to the listed names, that registry being the only static evidence those modules are wanted. The edge KIND is carried through the walk, because the two resolve differently at a missing target: import expands to children, but a require with no module behind it is a dangling require lua raises at runtime, and is reported. Treating every fileless target as a directory import meant require("gerrrt.utils") silently marked every gerrrt.utils.* child reachable. The inventory itself is validated, since the walk is only as sound as its name→file map: .init is stripped for a real */init.lua path only (doing it on the module string let a file named foo.init.lua masquerade as module foo), a dot inside a filename is reported as unaddressable (lua resolves gerrrt.a.b through a/b.lua, never a.b.lua), and two files claiming one module id fail — lua loads exactly one of them, so the other is dead config that would otherwise ride on its twin's reachability. The registry is also checked both ways, because a generic "unreachable" is a worse message than the truth: a module in no list entry is dead config; a list entry with no module file is a runtime load error servers/init.lua reports at startup. A missing or unparseable registry fails closed — silently skipping it would disable the entire servers/ arm, which is how this class of gap starts in the first place. Verified against planted fixtures for every class it claims to catch — orphaned utils/ module, stray top-level module, disconnected require cycle, comment-only mention, multiline block comment, package.loaded peek, require() of a directory, lazy import matching nothing, duplicate module id, unaddressable dotted filename, unlisted LSP module, registry entry with no file, unparseable registry, missing registry — plus the two exemptions (a false positive on health.lua or plugins/ would make the gate unusable). Each negative fixture asserts the finding text and exit status 1, so the documented CLI contract is covered too. The real tree is clean: all 97 lua modules under nvim/lua/gerrrt/ are reachable today. That module set is the gate's scope — nvim/init.lua is the entry point it walks from rather than a vertex, and lazy-lock.json and .luacheckrc are not lua modules at all. bash 3.2 safe, so the macos-latest CI leg runs it. Closes the nvim/ half of #454.
- Fixlib/bootstrap-lib.sh now owns privilege escalation, so a bootstrap stops hard-coding sudo. blib_resolve_su [--require] resolves the escalator once into BLIB_SU (an explicitly set value always wins, including an empty one; else root needs nothing, else sudo, else doas), and blib_priv is the public way to run a privileged command. Every OS bootstrap wrote sudo inline at roughly a dozen call sites, which is wrong on precisely the machines a bootstrap meets first: fedora:latest and alpine:3.20 ship no sudo, and neither does a WSL distro's first boot (root, before /etc/wsl.conf installs the default user) or a minimal Server image. Those runs died at the first package-manager line with sudo: command not found — exit 127 under set -e, before doing anything at all. It is also why bootstrap-test.yml can exercise only --links-only, and must pass BLIB_SU= to manage even that. --require makes "not root and no escalator" a hard error for a provisioning run, while a links-only run correctly continues (wiring symlinks needs no privileges).
- Featureblib_sudo_keepalive_start / blib_sudo_keepalive_stop — no more invisible password prompts. A bootstrap's privileged calls are interleaved with from-source cargo/go builds that take minutes, comfortably outliving sudo's 5-minute timestamp. sudo writes its prompt to stderr and reads from the TTY, so a later call whose stderr is redirected (>/dev/null 2>&1, ubiquitous in these scripts) stopped dead at a prompt nobody could see: no output, no progress, indistinguishable from a hang, and reproducible only on a box slow enough to cross the timeout. Prime once up front, refresh in the background, and return non-zero if that first authentication fails so the caller can abort before half-provisioning. A no-op under doas (no refreshable timestamp) and as root.
- Fixblib_user_bindirs_on_path — stop the presence guards lying. A bootstrap's command -v <tool> guards decide whether to spend minutes building from source, but they are answered by the PATH of whatever shell launched the bootstrap — on a fresh box, bash. cargo install writes ~/.cargo/bin and go install writes $GOBIN (~/.local/bin by convention here), while ~/.cargo/bin reaches PATH only via the OS zsh layer — i.e. only inside a Core shell that does not exist yet. So every guard reported "missing" and every re-run rebuilt the entire from-source tool set. Adds only directories that exist, and never twice.
- Featureblib_note_fail / blib_failed_count / blib_failures_report — a half-provisioned box now says so. A bootstrap is full of steps that must not abort the run (a COPR that is down, a rate-limited API, a crate that fails to build), so each is written || true — and the script then printed "bootstrap complete" and exited 0 regardless, making a box that got none of its extra tooling indistinguishable from a good one, to CI and operator alike. blib_failures_report returns non-zero when anything was recorded, which is the contract a caller maps onto its own --strict flag. All four are covered by a new hermetic section in scripts/test-core.sh (no package manager, no network, no privileges), including the bash 3.2 set -u empty-array rule that would otherwise crash the report on the happy path.
- FeaturePORTABILITY.md — how to write Core that survives the fan-out. The rules were real and consistently followed, but recorded only in ~8 scattered code comments, so they were unteachable to a new contributor and unenforced for new files. That is the likely root cause of the Homebrew paths that sat in maint/ and tmux/scripts/. It documents the bash 3.2 floor (with the banned constructs and their portable forms), the BSD/busybox coreutils traps, the shim pattern with the full inventory of shipped shims, what to do when a capability genuinely cannot be probed, and why the have() probe is redefined per loading context on purpose.
- FixVENDORING.md — the same contract from an OS repo's side. Previously scattered across ARCHITECTURE.md, RELEASE-RUNBOOK.md and a source comment, so a downstream maintainer had no single answer to: which core/ paths may I touch, what does core.lock mean, which number band may I claim, how do I upstream a fix. Includes the footgun that was documented only in zsh/loader.zsh — a fragment dropped in a gap in the Core band (say 22-foo.zsh) is gated as Core and silently vanishes under CORE_PROFILE=minimal.
- FeatureCODE_OF_CONDUCT.md — the one standard community-health file that was missing while the README actively solicits contributions.
- PerfCore now performs a real bootstrap link run in its own suite. bootstrap-test.yml asserts the symlink graph, but it is workflow_call-only and dotfiles-core ships no bootstrap.sh — so it only ever runs from the eight OS repos. Core unit-tested the blib_* helpers and never linked anything, which meant a bootstrap-lib.sh regression was caught downstream, in eight repos, instead of here. Seven assertions now link the actual Core tree into a sandbox $HOME/$XDG_CONFIG_HOME and check the graph a consumer depends on: every numbered fragment lands flat in $ZSH_CFG (the load-order contract loader.zsh globs), loader.zsh itself is linked, nvim/ resolves as a directory symlink, tmux/starship/lazygit/jj/gitconfig/vimrc land at their promised destinations, clip and clip-paste are executable on ~/.local/bin, the seeded files (local.gitconfig, sesh.toml) are real copies rather than symlinks — a symlink there would track a user's git identity back into Core — and a second pass is a no-op that backs nothing up. Hermetic: the tpm directory is pre-seeded so the one network call in the function is skipped.
- Fixscripts/sync-core.sh has tests. The highest-blast-radius script in the repo — it gates on the audit, git subtree pulls into eight working trees, and stamps core.lock — had no coverage at all. Its only proof was sync-fanout.yml running it for real against the live fleet, i.e. the fleet was the test. Twelve assertions on hermetic fixtures (a miniature of the real topology: a vendored origin, a local checkout, and a throwaway fleet, with audit-core.sh stubbed so the gate can be driven red and green in-process). Every case is a refusal or an idempotency property, because that is how this script fails: a broken guard does not throw, it fans a bad tree out to eight repos and reports success. Covered: a red audit refuses the fan-out and refuses before mutating anything; local HEAD ≠ remote tip refuses (what you audited is not what would vendor); an uncloned repo and a core/-less repo are skipped, not failed; dotfiles-Windows appears in neither the fleet file nor the fallback array; --dry-run prints the plan and commits nothing; core.lock lands at the repo root with the full sha, version and branch; the tree is clean afterwards so the next run is not self-blocked; re-syncing an unchanged sha manufactures no commit; and a dirty target is refused, counted failed, and does not abandon the repos after it.
Changed
- ConfigA release tag can no longer exist before its commit is on main. make tag used to commit and tag in one step, leaving a local vX.Y.Z on a commit that was not yet merged. That window is not closable by discipline: --no-follow-tags governs your push, while the tag lives in shared .git state any other process can push. It happened. During the v4.11.0 cut a concurrent session pushed its own branch with push.followTags set, carried the release tag to origin, and fired release.yml and sync-fanout.yml against an unmerged commit — publishing a Release and opening eight vendor PRs across the fleet against a commit that was never on main. Nothing merged, because sync-fanout opens PRs and never merges them, but the number had to be retired: release tags are immutable by ruleset, so v4.11.0 could not be re-pointed. The invariant is structural now, not procedural — a vX.Y.Z tag only ever exists on a commit already on origin/main. make tag commits and creates no tag at all, so a stray push has nothing to carry. make publish runs after the PR merges and refuses unless origin/main actually carries this core.version, then tags origin/main and pushes. --push is withdrawn — its whole semantic was the hazard — and fails with a pointer to --publish. Phase 2 tags the release commit, not origin/main's tip. core.version does not change again until the next release, so "the tip carries this version" stays true for every commit that lands afterwards — tagging the tip would sweep work still under [Unreleased] into the release, and release.yml builds the Release body from the [vX.Y.Z] section, so that work would ship undescribed. It resolves the commit that set core.version to this value and tags that, reporting when the tip has moved on. It also validates that commit's [vX.Y.Z] section before creating any tag — that it exists and is non-empty, using release.yml's own awk so the two cannot disagree about what empty means. release.yml builds the Release body from that section and rejects an empty one, and release.sh will promote an empty [Unreleased] without complaint; publishing first and discovering either afterwards leaves an immutable tag on a release that cannot be published, burning the version for a reason knowable up front. make release's printed recipe is updated to match. It still ended with git tag -a + git push --tags, so an operator following the output it generates would have recreated exactly the pre-merge tag this change exists to eliminate. Both refs go up in a single --atomic push, with a --force-with-lease on the vN alias. Pushed separately they can half-land: vX.Y.Z published while vN is stale fires the workflows against a stale alias, and a re-run then refuses because the immutable tag already exists. The lease rejects the push if another publisher moved vN after this run read it, and an ancestry check covers the gap before that read: whatever vN points at must be an ancestor of the commit being tagged, so the alias can only ever move forward. Both are needed — a publisher finishing before the read is seen as this run's own expected value, so the lease alone would be satisfied while vN rolled backward. This also makes the merge method irrelevant to the tag. Eleven behavioural assertions cover it — including that the tag does not follow a tip that advanced after the release merged; the script previously had none, which is how the ordering survived.
- FixThe audit now names the behavioural assertion that failed, in the failure line itself. It said behavioral tests failed — run: ./scripts/test-core.sh, which sends the operator away to reproduce a result the run already had. For an intermittent failure that is advice that cannot be taken: the re-run passes and the evidence is gone. That is not hypothetical — it cost two occurrences of an unattributed flake, both lost because the ✗ scrolled past far above the summary and only the summary survived being piped through tail. The suite's output is already buffered for the background run, so the names cost one grep and travel wherever the fail line travels: the summary block, --json, the CI job log, a truncated paste in an issue. Not a CI annotation — fail() writes to stderr and ci.yml runs audit-core.sh directly with nothing emitting ::error::, and claiming a destination this does not reach would be the same overclaim the digest exists to prevent. The names are joined without rewriting the records, so a message containing a literal | (nine assertions do, 'exec … || exec …' cannot fall back among them) is not spaced out into false boundaries — two failures reading as four is worse than terse in the one line someone has when they cannot reproduce the failure. Up to three are named, then a count (+N more) rather than a silent truncation, because "one flaky assertion" and "the whole section is down" need telling apart before deciding to re-run or investigate. A run that exits non-zero having printed no ✗ at all — a crash, a kill, a timeout — now says that, instead of an empty list beside a red line. Matched after stripping SGR escapes rather than anchoring on a bare ✗: fail() prefixes the mark with $c_red, so an anchored match finds nothing whenever colour is on — a detector that would go quiet in exactly the runs someone is watching. The serial path (CORE_AUDIT_SERIAL=1) keeps the old line; its output is not captured, and piping it to capture would cost the live colour output that mode exists to give. The rendering lives in scripts/lib/common.sh as _core_fail_digest so the suite can test it, which matters more here than usual: every branch of it fails quietly, producing a plausible line that has silently lost the name — indistinguishable from the flake merely not being nameable. Proving those by making a real gate fail would mean recursively invoking the audit or hand-injecting a fault, and CI repeats neither, so five assertions drive it on fixtures instead: a coloured ✗ is still extracted, five failures render as three names plus a true total, exactly three grow no (+0 more) tail, a message carrying its own literal || survives verbatim rather than gaining false boundaries, and both a marker-less log and an unreadable file yield empty so a crash is never misreported as assertions. Confirmed as real regression tests by mutation — dropping the escape-strip makes the coloured case yield nothing, dropping the overflow notice reddens that case alone, and the pipe fixture fails against the join this entry replaces.
- ConfigARCHITECTURE.md now names Core's two deliberate exceptions instead of leaving them to be rediscovered as drift. zsh/55-maint.zsh was already excepted in writing at the gate; zsh/60-update.zsh — ~480 lines of seven-package-manager logic, including a Tumbleweed check to choose zypper dup over zypper up — was justified only in a code comment. The reasoning is sound (one verb, N backends, exactly like bin/clip) and now says so where the layering rule is stated.
- FixThe maint runner no longer names an OS prefix; the scheduler unit supplies the PATH. A scheduler starts the runner with a stripped environment, which is why the Homebrew prefixes were hardcoded. maint-install now captures the live PATH of the shell installing it and bakes it into the unit — Environment="PATH=…" (systemd), an EnvironmentVariables dict (launchd, XML-escaped), and an env-prefixed command (cron, POSIX single-quoted and then %-escaped, in that order — cron hands its command field to /bin/sh, so an unquoted or double-quoted value containing $(…) or a backtick would be evaluated on every scheduled run). Whatever prefix this OS uses is already correct in that PATH, so the OS supplies the truth and Core hardcodes nothing. The brew step is now gated on have brew alone. Action required on an existing schedule: a unit written before this change carries no PATH, so the runner falls back to the POSIX floor and the brew/mise steps skip silently — the job still succeeds while doing less. Re-run maint-install once. maint-status detects this and says so rather than leaving it to be noticed.
- Featuretmux-cheat.sh discovers a brew prefix instead of naming one — $HOMEBREW_PREFIX (exported by brew shellenv, so the tmux server usually carries it), falling back to brew --prefix. When neither resolves it adds nothing and takes the existing pager fallback: a missing tool degrades visibly, where a wrong absolute path was a silent lie on every non-brew machine.
Fixed
- SecurityA failing linter gate named itself and nothing else. Five sections — luacheck, shellcheck, markdownlint, actionlint, gitleaks — ran their tool with >/dev/null 2>&1 and reported a one-line verdict, so a red run said ✗ markdownlint reported issues with no rule, no file and no line. Each ended with a "run it yourself" hint, which is fine locally and useless in CI — the one place the tool is installed, the finding is already computed, and re-running it costs a push and a full CI cycle per guess. Diagnosing a single MD049 violation this way took three round-trips. Output is now captured and printed beneath the ✗, via a shared fail_detail in scripts/lib/common.sh: stderr (so --json keeps stdout parseable), indented (so it reads as detail, not as further findings), and capped at CORE_FAIL_DETAIL_LINES (40) so a pathological run cannot bury the summary it is meant to explain. gitleaks also gains -v --no-color, without which it prints only leaks found: N and the file/line/rule stay hidden — the same non-answer. Printing its report is safe precisely because --redact is already in use: the value is replaced with REDACTED, so the report names the file, line, rule and fingerprint without reproducing the secret.
- Fixcore-doctor reported ✗ bat on Debian/Ubuntu/Kali for a tool that was installed and fully wired (#418). Those distros ship the binary as batcat; 00-tools.zsh resolves it into $BAT_BIN, and cat, catp, MANPAGER, the fzf file preview and fif's preview all ran on it. The report still called it absent — two lines above its own resolved section printing bat → batcat — and listed bat under "install missing", advising an install of something already present. The cause was an asymmetry between the only two renamed tools. 20-aliases.zsh gave fd an alias under its canonical name and bat none, so bat was untypeable by the name its README, man page and every upstream recipe use. bat now carries the matching alias bat="$BAT_BIN" (a no-op alias bat=bat where the name is already canonical; zsh does not re-expand an alias to its own name). That alias is not what makes the report honest, though it looks like it would: zsh's command -v resolves aliases, so fd's ✓ had been coming from the alias rather than from PATH all along. The doctor now resolves each row through a new _core_doctor_bin — one definition shared by the human render and --json, so they cannot drift — which maps fd/bat to $FD_BIN/$BAT_BIN and everything else to itself. Presence, the install-missing list and the JSON tools object all follow the real binary; the JSON keys stay canonical (.tools.bat, never .tools.batcat) for existing consumers. Resolving there also fixed a second defect the alias could never have reached. core-doctor -v forks "$tool" --version, and a parameter expansion is never alias-expanded — so on Debian the probe ran fd, hit command not found, and had the error swallowed by the pipeline: the row rendered as a bare, versionless ✓ fd. Both rows now fork the resolved binary and print their version. Five cases in scripts/test-core.sh pin it against a stubbed PATH (Debian names, canonical names, neither), with the doctor assertions deliberately run without 20-aliases.zsh loaded so a ✓ can only come from the resolver.
- FixPORTING-MATRIX.md's carapace = go³ cells named an install path that cannot be followed on any platform. Footnote ³ promises go install where a tool is unpackaged, and the carapace row pointed openSUSE and Kali straight at it. That install cannot succeed for any published version, for two independent reasons: carapace-bin's go.mod carries replace directives (spf13/pflag, kevinburke/ssh_config), and go install pkg@version refuses any module that does; and the generated sources (pkg/{actions,conditions}/*_generated.go) are not committed, so even a plain go build on a clone fails until cmd/carapace/main.go's go:generate lines have run. Checked exhaustively rather than inferred from the current release: across all 184 tags from v0.0.3 (2020-08-31) to v1.7.3 (2026-06-30), 184 carry a replace directive and 0 commit the generated sources. That scope is the operative part — go install takes any @version, and pinning an older one fails identically. Nor is it a transient break to wait out: upstream's own .goreleaser.yml runs go generate ./cmd/... as a pre-build hook, and the AUR's from-source PKGBUILD does the same, so this is the intended build shape. The three cells now point at a new footnote ²⁷ carrying a route per target — the upstream .rpm for openSUSE (the block dotfiles-Fedora's bootstrap.sh already ships and has proven), the .deb for Kali/Debian, and the AUR carapace-bin for Arch (the prebuilt one; the AUR's bare carapace is a from-source, x86_64-only build). Alpine and Gentoo were already correct and are now documented as verified rather than merely unmarked. ²⁷ also records what the release-URL route costs — no repo is added, so nothing upgrades carapace afterwards — the unsigned-artifact wrinkle that makes zypper -n stricter than dnf here, and the source build as the escape hatch with its real binary size (81.6 MB released, ~114 MB unstripped). Footnote ³ gained a pointer so the general go install promise is not read back onto this row. dotfiles-Arch, dotfiles-Kali and dotfiles-openSUSE still make the impossible call in their bootstrap.sh, failing invisibly because _dotfiles_go_install discards the explanation; each is tracked in its own repo against this footnote. (PORTING-MATRIX.md)
- FixThe nvim reachability gate invented orphans on main. The membership lookups piped into an early-exiting reader — printf '%s\n' "$visited" | grep -qxF "$m" — while the script runs under set -o pipefail. grep -q exits on its first match, the writer takes EPIPE and dies with 141, and pipefail makes the pipeline non-zero even though the reader matched: a module that is visited reads as unvisited and is reported as an orphan, with printf: write error: Broken pipe captured as a finding alongside it. Timing-dependent — the writer must still be writing when the reader exits — so it passed every PR run and failed on the push to main. Measured on a large input, the piped form gave 20/20 false negatives and the herestring 0/20. Every lookup now feeds its input by herestring, awk … <<<"$mods" included, since awk's exit closes the pipe the same way.
- Fixblib_set_login_shell could throw away a complete, correct wiring over its last, purely cosmetic step. It runs at the very end of wire_links, after every symlink is already in place — but neither the /etc/shells append nor chsh tolerated failure, so under the caller's set -e a host with a read-only /etc (a container), a restricted chsh, or an LDAP/SSSD-backed account aborted the whole bootstrap. Worse, the operator saw a bare tee: /etc/shells: Permission denied and no indication of what had or had not been done. Both steps now warn and continue, naming the manual command to finish the job.
- Fixgrep -q on a large piped producer read a match as a failure under pipefail. The new origin/main CHANGELOG guard piped a 4000-line file into grep -q, which exits the moment it matches — leaving git show to die of SIGPIPE, and set -o pipefail then surfaced git's 141 rather than grep's 0. The check reported "no heading" on a file that had one. Captured to a variable instead. Swept the rest of the tree for the same shape: the other instances pipe small printf/find output that fits the pipe buffer, so they never trip it, and the one borderline case in test-core.sh was made immune anyway.
- FixThe atuin autostart apparatus gate now tells a slow box apart from a broken detector. The gate proves the box can bind and connect an AF_UNIX socket with python3 alone, then runs a known-good stub and treats any verdict other than holds as a regression in verify-atuin-guard.sh — deliberately, because the obvious "skip unless it holds" form uses the code under test as its own apparatus check and would let a real regression skip every assertion below while leaving the audit green.
- Configcore-doctor no longer reports a false ○ (idle) for starship and carapace. _core_wired probed only starship_precmd and _carapace, but both tools renamed the functions their init emits — starship 1.24.2 emits prompt_starship_precmd and carapace-bin 1.5.7 emits _carapace_completer, and neither emits the old name at all. Since Core sources each tool's own init (_cache_eval starship starship init zsh), the probe silently went stale as the tools moved, so every box on a current starship or carapace saw ○ (idle) for an integration that was demonstrably driving the prompt and completion (measured: 1760 carapace-bridged commands, PROMPT set by starship). That is the exact failure the probe exists to prevent, inverted — a misleading ○ instead of a misleading ✓. Both arms now accept the old and current names, so boxes pinned to older releases keep reporting wired.
- FixThe atuin autostart apparatus gate no longer reds when the box is merely slow. The gate proves the box can bind and connect an AF_UNIX socket with python3 alone, then runs a known-good stub and treats any verdict other than holds as a regression in verify-atuin-guard.sh — deliberately, because the obvious "skip unless it holds" form uses the code under test as its own apparatus check, and would let a real regression skip every assertion below while leaving the audit green. The strictness was right; the deadline was not. §J4 runs at CORE_ATVERIFY_POLL=3, chosen so the many negative cases do not idle away a long bound — but for the one stub that is supposed to succeed at everything, that bound is not idle waiting, it is a deadline: 300ms for a spawned daemon to bind and answer. A loaded runner misses it, the verifier declines with "a daemon started by hand … never answered" exactly as designed, and the gate rendered that property of the box as a defect in the detector. It reddened an audit leg for a change that had nothing to do with atuin. The tempting repair — skip on unmeasurable — is wrong, and the reason is recorded in the code because it is easy to re-derive incorrectly: that verdict is the verifier's fail-closed answer for a family of causes, and most are deterministic and are the detector (a renamed or duplicated anchor, control-arm row accounting that no longer matches, and internal: no verdict was reached (this is a bug in verify-atuin-guard.sh)). Skipping on it would silence sixteen assertions while the subject announces its own bug, and no amount of retrying separates those from slowness, since every one of them repeats. So nothing skips. The known-good run simply gets a deadline with real headroom — 30 ticks instead of 3, plus one retry — while every negative case keeps the tight bound. A transient stall now has to land twice inside a 10x-wider window to be seen at all. Measured on the repo's own fixture with all four arms holding: 10.9s at 3 ticks, 14.0s at 30, 23.4s at 100 — so this costs about three seconds of wall clock, once per suite, and 30 rather than 100 because --premise autostart also spends the bound proving unreachability, which no amount of promptness shortens. The gate keeps the property that matters: it cannot go quiet, because every verdict other than holds still reddens it. The three failures are now told apart — moved (miscategorising correct behaviour), unmeasurable (declining where it should measure, carrying the verifier's own reason), and no parseable verdict at all (the apparatus failing to report, carrying stderr — the Alpine shape where a stray line merged into the JSON).
- FixThe atuin autostart suite no longer reports an unmeasurable run as an upstream finding. verify-atuin-guard.sh has three verdicts on purpose — holds, moved, and unmeasurable for "the apparatus could not be trusted, never a finding about upstream" — but the socket-only-stop assertion in scripts/test-core.sh compared against holds and swept everything else into a single else, so a declined run printed the exact claim the third verdict exists to prevent: that a zombie daemon had kept committing into later arms. It is the only arm in that section expecting the POSITIVE verdict from an otherwise well-behaved stub, so it alone inherits every environmental way a run can honestly decline. The section runs at CORE_ATVERIFY_POLL=3 — 300ms for the manual-spawn control's daemon to bind and answer — which a loaded box misses, yielding "a daemon started by hand never answered … An apparatus limit, not a finding" with nothing having survived. That reddened audit-alpine on an unrelated docs-only PR; a rerun of the identical commit went green. The three states are now distinguished: holds with no survivor passes, unmeasurable skips with the verifier's own reason surfaced, and moved fails as the real finding. A run that produces no parseable verdict at all is reported as its own outcome carrying stderr, rather than being read as moved — that shape has a history here, being how §J4 first went red on Alpine when a stray musl-side line merged into the JSON. The assertion does not go quiet in exchange — the survivor half is now checked unconditionally and stays a failure under any verdict, because a live daemon is a leak whether or not the run could measure. A contaminated control cannot hide behind the skip either: the opening control runs before any daemon exists, the spawn control while the socket is still present, and the closing drain control only after the owner pid is confirmed dead — so the zombie's rows have no route to unmeasurable, only to moved or a survivor.
- SecurityA concurrent test run no longer fails the audit with a sandbox leak that never happened. verify-atuin-guard.sh --premise autostart built its sandbox at /tmp/atverify.XXXXXX, and the test-core.sh assertion that a completed run leaves no sandbox behind enumerated that prefix globally — snapshot before, snapshot after, anything new is a leak. /tmp has other writers, so a second suite running on the same box during the window was counted as the first run's leak. Two worktrees, two agents, or simply make audit in one terminal while make tag audits in another was enough. It cost a real make tag — leaked 1 new sandbox dir(s) on the repo's most consequential command, where the operator's natural next move is to re-run or reach for TAG_SKIP_AUDIT=1. A release gate that teaches the operator to skip it is worse than no gate. Sandboxes now carry a per-run tag — /tmp/atverify.<tag>.XXXXXX, from the new CORE_ATVERIFY_TAG — and the assertion globs only its own. The tag defaults to the script's pid, so make verify-atuin-guard and atuin-guard-verify.yml pass nothing and still get a prefix no concurrent run can collide with; two live processes cannot share a pid. It is validated as 1-16 characters of [A-Za-z0-9_-] and rejected rather than sanitized, because it becomes a path component and a caller that globs its own tag needs the tag it passed. The cap is an AF_UNIX budget, not style: sun_path ends near 108 bytes and the daemon socket sits inside the sandbox, which is the same reason /tmp is hardcoded there instead of $TMPDIR. An empty tag is rejected rather than defaulted, which is why the knob reads ${…-$$} and not the ${…:-…} its two neighbours use. An empty value is not a caller asking for the default — it is a caller whose tag expression came out empty — and accepting it would sandbox under the pid while the caller globbed /tmp/atverify..*, matching nothing and greening the leak assertion forever. That is the same vacuous pass the self-check exists to catch, arriving by a different door. The validation runs in the C locale, and this is a defect that was shipping rather than a precaution: POSIX defines a range like [A-Z] by collation rather than codepoint, and on glibc under en_US.utf8 the unpinned pattern accepts tág — measured on the Ubuntu CI leg, not reasoned about, so the ASCII-only contract was not being enforced there at all. It is invisible from macOS, where all 84 installed UTF-8 locales reject the same sample, which is exactly why Core cannot take one userland's answer for the fleet's. The byte cap is the same fault one step downstream: {1,16} counts characters, so sixteen multibyte ones are up to 64 bytes and the limit stops being the AF_UNIX budget it exists to be. Downstream, not separate — every character in [A-Za-z0-9_-] is single-byte ASCII, so the count can only diverge from the byte length once collation has already leaked a non-ASCII character in. The suite reports how much of this it actually exercised, rather than implying more. It asks the box for its installed UTF-8 locales (locale -a, falling back to named candidates on musl, which ships no such command) and looks for one under which the unpinned pattern really accepts a non-ASCII sample. Finding one, it names it and the case genuinely fails if the pin is removed; finding none, the result states the count and says the pin is unexercised there, asserted by contract only. Across the fleet that reads: Ubuntu exercised under en_US.utf8, while macOS (84 installed), Arch (1 installed) and Alpine (7 candidates, since musl ships no locale) report contract-only — so one leg proves the fix and the other three say honestly that they cannot. The no-match case then runs under LC_ALL=C rather than an empty LC_ALL, which is not "no locale" at all but a fall-through to the caller's LANG: an unprobed locale that could be the very one that accepts the sample, making the run exercise the pin while the line claimed it had not. Two earlier drafts of this check were vacuous — one probed for multibyte decoding, which a locale can do while still collating á outside [A-Za-z], so it passed identically with the pin removed. That is the shape this file already exists to refuse, and the coverage line is now part of the assertion rather than a comment about it. Two assertions, because narrowing a glob and blinding it look identical from a green run. The leak check now plants a foreign-tagged sandbox inside its own window and still requires a clean delta; a companion case plants one foreign and one of its own and requires the delta to name its own and only its own. The existing self-check — which fails loudly when the glob cannot enumerate at all, after an unfollowed /tmp symlink once made this pass vacuously on macOS — is unchanged, and matters more now: a tag that never reached the script would empty both snapshots the same way.
- Fixmaint-status now reports a scheduler unit whose runner path no longer exists. _maint_unit_needs_refresh only ever asked whether the unit carried a PATH capture, so the other way a scheduled job dies silently went unreported: move the consuming repo and the scheduler keeps firing at the absolute runner path frozen into the unit at install time. Found on a real machine, where a launchd agent had been pointing at a path that had not existed for months. Nothing surfaced it from any angle. maint-status printed the timer happily, launchctl list showed exit status 0 because the job had not fired since the move, and maint-run kept working — it resolves the runner relative to the live config rather than reading the unit, which is exactly why the breakage stayed invisible. The detector now also reads the runner back out of the unit — ProgramArguments[1] from the launchd plist, the path after ExecStart=/usr/bin/env bash in the systemd service, the command past cron's single-quoted PATH= prefix — and flags it when it does not resolve. Both causes are fixed by re-running maint-install, so the hint now says which happened: a stale unit predating the PATH capture is a snapshot to refresh, a dead runner path usually means the repo moved. Each arm matches the exact shape maint-install renders and stays quiet on anything else, because a "close enough" parse turns a live job into a false death notice: the systemd and cron arms read a command, not a path field, so a hand-edited … bash /runner --quiet or … bash /runner >>/log must not be read as one long, nonexistent path; and the launchd array must be ProgramArguments' own value rather than the next array in the plist. A recorded path must also be absolute, which maint-install always writes: a relative one would be resolved by [[ -f ]] against whatever directory maint-status was invoked from, making the verdict a property of the caller rather than of the unit. A box with no schedule installed stays quiet too. Every token is located by position rather than by appearance: launchd's argv[0] must be the interpreter, and cron's PATH= value is consumed as a real single-quoted token, so a command that merely contains text resembling the interpreter — inside the assignment, or inside a later quoted argument — can never have its argument read back as our runner. On the launchd side the encoded forms a plist may legally use (", ') are decoded so the hint names the real filename, and anything undecodable (a numeric character reference, an unknown entity) is refused for the same reason the rest is: a filename that cannot be reconstructed is not evidence of anything. The two causes can coexist, and a unit predating the PATH capture is if anything the likeliest to have been orphaned by a move as well — so the runner is inspected first and path is the fallback. Reporting the milder cause there would tell the operator that some steps will skip on a job that does not run at all. A % in the recorded runner disqualifies it in both command-reading arms, because in neither is the literal text what runs: systemd expands % specifiers in ExecStart — the expansion _maint_systemd_escape already doubles against in Environment= — and cron reads % as its newline metacharacter. Thirty-one behavioral assertions — twelve scheduler states, four that a recorded path is extracted correctly (two read back verbatim, plus escaped-quote scanning and ' decoding), twelve that an extended command, a displaced array, a relative path, a spliced-in program, a quoted look-alike, an undecodable reference, or a %-bearing value is refused rather than mis-parsed, and three that a dead runner outranks a stale PATH. The healthy fixtures point at a runner that really exists, or the whole section would pass vacuously.
- FixThe release cut no longer tells the operator to do something the repo forbids. RELEASE-RUNBOOK.md §1.1 step 4 said "merge commit, not squash", and tag-release.sh printed the same hint twice. Merge commits are disabled — mergeCommitAllowed and rebaseMergeAllowed are false, and main's ruleset pins allowed_merge_methods: ["squash"] — so the instruction was impossible to follow, and it was printed at the worst possible moment: mid-cut, by the repo's highest-stakes command, where the natural reaction is to assume the ruleset is misconfigured and go change it. v4.10.0 had already shipped as a squash (cd4278e, one parent) in silent contradiction. The "not squash" clause was never load-bearing. It was descriptive — added in #106 (first shipped in v2.1.1) to record how releases merged then, since #95, the v2.0.0 release, had landed as a real merge commit back when merge commits were still enabled — and left behind when they were turned off. What makes the recipe correct is step 5 tagging origin/main, the post-merge tip, so release.yml's core.version-at-the-tagged-commit guard, git describe, and the vN alias are all satisfied by a squashed tip. RELEASE-RUNBOOK.md now records that reasoning under §"Why squash is fine", including the instruction to trust the repo over the docs if they ever disagree again. tag-release.sh now names no merge method rather than swapping one hardcoded claim for another. Deriving the wording from the live setting would need gh and a network call, which its offline-safe next-steps output cannot take, and there is no settings-as-code file to read instead. Naming a method was never actionable anyway — GitHub only offers the methods a repo enables, so the operator cannot pick a disallowed one. The hints now state the property that actually matters (step 2 tags origin/main, so the merge method cannot affect the tag), which has no way to go stale.
- FixThe boundary scan no longer strips comments at all. Stripping was a false-negative machine: # is a comment in shell and TOML but the length operator in Lua, so local p = t[#t] .. "<prefix>/bin" was truncated and passed; a delimiter inside a string is code, so export P="#<prefix>/bin" was truncated too; and a line inside a heredoc or a Lua long-bracket string is runtime data however it starts. Each fix uncovered the next, because getting it right needs a parser for all five grammars the gate now scans. The rule is flat instead: a manifested Core file must not contain an OS-absolute path anywhere, prose included — name the prefix rather than spelling it. Two comments in maint/ and tmux/scripts/ were reworded to comply. That costs a wording choice and buys a gate with no hiding places. The one sanctioned exemption is now redacted rather than dropped. Removing the whole LaunchAgents line exempted everything else on it, so a second literal riding along on a legitimate assignment evaded the gate; only the sanctioned segment is replaced now, and the rest of the line is scanned normally. Verified against the old filter: with _x="<prefix>/bin" appended to a LaunchAgents line, the line-drop passed it and the redaction catches it.
- FeatureV4-PROPOSAL.md no longer claims v4 is unreleased. Its status block said "IMPLEMENTED … pending the v4.0.0 release cut" and described the work as sitting on a branch — ten minor releases after v4.0.0 shipped. It is now marked as the historical design record it is, pointing at ARCHITECTURE.md / PORTABILITY.md / VENDORING.md for how the shipped system actually behaves.
- FixThe Core⇄OS boundary gate was green while two Core files carried Homebrew paths. audit-core.sh §5c rejects OS-absolute paths in portable Core, but its file list stopped at zsh/*.zsh plus the symlinked configs — so bin/, maint/, and tmux/scripts/, all manifested Core that ships to eight repos, were never scanned. They were not clean: maint/dotfiles-maint.sh hardcoded /opt/homebrew/{bin,sbin} and /home/linuxbrew/.linuxbrew/bin in its PATH and probed both by absolute path to run brew shellenv, and tmux/scripts/tmux-cheat.sh did the same in its pop-up PATH. The rule was documented, believed enforced, and was not — on seven of the eight target machines those paths do not exist. The gate's scope is now derived from core.manifest rather than hand-kept. That list had fallen behind three separate times — first the symlinked configs, then the bin//maint//tmux/scripts/ executables, and even then it still omitted zsh/completions/*, lib/ux.sh, lib/bootstrap-lib.sh and .bin/sync-upstream.sh. Every omission was the same bug, so the fix is structural: the manifest already is the definition of "what is Core", and a file added to it is scanned automatically. The blind spot cannot silently reopen, because reopening it would mean the file is not Core at all — which the manifest gate already fails on. Coverage went from 19 files to 167 (including the vendored nvim/ tree). The gate is also unconditional now: it used to be SCOPE_SHELL-gated, but it is pure sed+grep and cross-cutting, so a narrowed --scope run must not be able to skip a fan-out-correctness check. The one exemption — zsh/55-maint.zsh, whose launchd arm legitimately writes ~/Library/LaunchAgents — is now per-line rather than per-file. Skipping the whole module would have re-opened the blind spot inside it: an accidental /opt/homebrew added to maint-install, or to any other function there, would have sailed through. Only the LaunchAgents lines are dropped; everything else in the file is scanned. Verified the way a gate change has to be: the previous tree is red under the new scope and green under the old one, which is the only evidence that the widening bites.
- Perfmaint-install now escapes the runner path it writes into every scheduler unit. The write-side half of the % problem the entry above only closed on the read side: the captured PATH was already escaped three different ways, one per scheduler grammar — but the runner alongside it went in raw, and it is no more of a constant: it is wherever the consuming repo happens to have been cloned. A single metacharacter in that path produced a broken schedule, and all three failures were silent or nearly so: - systemd expands % specifiers in ExecStart= (%h = home directory, %i = instance, …), so a repo under …/a%h/… ran a different path entirely — or the unit refused to load outright on an unknown one. It substitutes variables there too, so a component literally named ${HOME} was equally not the path that ran. The Environment= line one row above was already protected against the specifiers — and performs no variable substitution at all, which is why $ needs its own pass rather than a wider shared helper. The argument was also unquoted, so systemd split the runner on whitespace, and a " or \ in the name carried unit-file syntax rather than being part of the filename. - cron treats % as its newline metacharacter: the command was truncated there and the remainder handed to it as stdin, so the job simply stopped running. maint-install already escaped % in the PATH portion and not in the runner. The runner was unquoted besides, so a space split the command and a $(…) or a backtick in the path was code, evaluated on every scheduled run. - launchd got &, < or > straight into ProgramArguments, yielding a malformed plist that launchctl load rejects. The PATH value two lines below was already escaped. Each field now goes through the escape its own grammar needs: the systemd runner is written quoted, through the Environment= helper plus a command-line-only $ → $$ pass (quoting is what reduces whitespace, " and \ to the same substitutions % and $ already needed), the cron runner through the same single-quote-then-escape-% pair as the cron PATH, and the launchd runner — along with the two log paths, which had the same hole — through the plist's XML escape. The crontab entry is also emitted with print -r rather than echo. maint-install runs under emulate -L zsh, where the echo builtin interprets backslash escapes — so two characters in a directory name were enough to corrupt the table that the careful quoting above had just produced: \n split the entry across two lines and \c truncated it outright, leaving a schedule that silently was not the one anyone asked for. _maint_unit_runner decodes each new encoding symmetrically, so maint-status keeps naming the real filename. It stays as strict as it was, and the strictness is the same rule in three places: a value the reader cannot reconstruct is not evidence of anything, so it is refused rather than guessed at. The systemd arm therefore refuses a closing quote with argv after it, a surviving % specifier, and a surviving $VAR reference — the text in the file is then not the path systemd runs, and resolving either would mean reimplementing systemd's specifier table and reading the unit's environment block. The cron arm refuses a bare % — one that is not our own \% — because sh quoting is no defence there: cron translates the field before sh ever sees it, so the command is truncated at that % whatever the quotes say. That test has to run before the \% decode, which would otherwise destroy the evidence of which kind of % it was. The launchd arm already applied the same rule to an undecodable entity reference. The older unquoted shapes still parse, because a unit on disk is only rewritten when the operator re-runs maint-install — and a % in one of those is still refused outright by _maint_lone_arg, which remains the right answer there: nothing escaped it, so the recorded text genuinely is not the path that runs. Twelve further assertions: one round-trip per scheduler through a runner path holding % $ ${} & < > " \ ', a space, and the two-character sequences \n and \c — installed, read back verbatim, and reported as current rather than as a dead runner — one per scheduler confirming the same artifact through a party that is not this codebase (/bin/sh parses the cron command back after applying cron's own \% pass, plistlib parses the plist, and the systemd ExecStart is pinned against a literal expectation), one that the crontab entry is a single marker-terminated line, and five refusals for the quoted shapes. That one is stated as a pair deliberately: with this fixture the two echo corruptions cancel in the line count — \n adds a newline and \c removes the final one — so a bare "is it one line" check reads green on a table that is one wrapped fragment plus one unterminated one. Reaching the marker is what truncation cannot fake. A round-trip through our own reader alone would pass a matched pair of wrong escapes, and a fixture whose backslash pair is not a recognized escape passes the echo hazard without ever exercising it — the first revision of this one used \g and did exactly that. The whole block skips, rather than passing vacuously, on a filesystem that will not take " or \ in a name. The pre-existing cron render assertion now anchors on the runner's closing quote, so dropping the quoting fails there rather than on the one box whose path has a space.
Security
- FixCaller-supplied workflow inputs no longer reach a run: body as code. auto-tag-call.yml spliced ${{ inputs.bump }} straight into its script in a job holding contents: write and persist-credentials: true, so a caller passing bump: 'patch"; …; #' could run arbitrary code with the tag-push token. It was the one place the fleet broke the rule notify-web-call.yml states outright — "a caller-supplied string must not be able to write shell". Both bump and release now arrive through env:, and bump is checked against a patch|minor|major allowlist at runtime — workflow_call inputs cannot be type: choice (that is workflow_dispatch-only), so the type system will not do it. A typo now fails with the valid set instead of reaching auto-tag.sh's arg parser. claude-routines-call.yml had the same shape with ${{ inputs.distro }} in a job holding CLAUDE_CODE_OAUTH_TOKEN; it now goes through env: too, and is likewise allowlisted to the six distro names its own input contract already documents — env: makes the value shell-safe, but the Claude prompt is an instruction channel, so an arbitrary string there remains a prompt-injection vector. No run: body in any workflow interpolates an expression any more. Neither rejection path echoes the raw value back. The runner parses stdout line by line, so a multiline input can open a new ::…:: command on the following line and forge or suppress annotations no matter how well it is shell-quoted; both paths strip CR/LF/%/: and truncate first, mirroring how atuin-guard-verify.yml already handles upstream-derived text.
Added
- FixThe autostart stand-down is measured now, not assumed (dotgibson/dotfiles-core#402). _core_atuin_daemon_guard stands down entirely under ATUIN_DAEMON__AUTOSTART — it unhooks itself from precmd_functions and never probes — because atuin is supposed to supervise its own daemon there. That covers Alpine and macOS, two of the eight machines, and on those two it was the only mitigation. Nothing measured it: the weekly detector never set the variable, so a green run said nothing about those rows, and an upstream regression would have cost them their history with no symptom — the same failure mode as #366. scripts/verify-atuin-guard.sh gains --premise discard|autostart (default discard) plus make verify-atuin-guard-autostart, and the weekly workflow gains measure-autostart and report-autostart jobs with their own issue titles. One premise, one verdict, one title: an autostart finding needs a different remedy from a discard one, and would otherwise arrive under a heading that misdescribes it. The default target still starts no background process — asserted by construction, since the stub logs every invocation it receives. The measurement's own discipline is the interesting part. Because this premise is caused rather than observed, "autostart did not spawn a daemon" and "this box cannot host a daemon" are the same observation — so a manual-spawn control runs first and its failure is unmeasurable, never a finding about upstream. A second control tries a manual bind over a stale socket and records the answer, which is what separates "the client will not re-spawn after a crash" from "the daemon cannot bind over a leftover inode". Teardown goes through atuin daemon stop and is proven by a connect rather than believed from an exit status, escalating to the socket's owning PID and then a signal; the run refuses to spawn at all on a build lacking either subcommand, because a daemon it cannot reap would be left writing into a tree the exit trap is about to delete. Two upstream facts this established on 18.19.0, both recorded in PORTING-MATRIX.md: the stale-socket shape is the load-bearing one — every atuin history start is a fresh process, so "fire-and-forget" can only mean a crashed daemon's leftover inode defeats the spawn — and the healing lives in the client, since atuin daemon start alone refuses over a stale inode with Address already in use while the autostart path unlinks it first. Also measured, and load-bearing for the arms: with a daemon serving, history start writes nothing and the row lands on history end. /tool-scout loses a standing upstream question it could only ever have answered from release notes, and the report names the trap in its own remedy: "make the guard stop standing down" is the fix that breaks those two machines, because the degrade path sets ATUIN_DAEMON__ENABLED=false and under autostart that removes the spawn itself.
- Fixgit-absorb — auto-route staged hunks into the earlier commit each belongs to (dotgibson/dotfiles-core#394). 00-tools.zsh now detects git-absorb and sets HAVE_GIT_ABSORB. It works out which earlier commit each staged hunk belongs to and writes the fixup! commits for you; git/gitconfig already sets rebase.autosquash = true, so git rebase -i folds them in without further ceremony. It is the automatic counterpart to the git fix alias Core has always shipped — git fix <sha> when you know the target commit, git absorb when you don't. That [alias] line now carries a comment saying so. This is the house-style ideal: a tool that needs no alias at all. It installs as git-absorb on PATH, which git dispatches as the git absorb subcommand, so it shadows nothing and 20-aliases.zsh gains only a note explaining why there's nothing to add. Detection exists purely so core-doctor can report it, where it joins the dev / repo group. One documented caveat: the probe is command -v git-absorb, so a distro that installed the binary into git's libexec/git-core instead of a PATH directory would give a working git absorb and an unset flag — no mainstream package does, and probing git absorb --version would add a git fork to every interactive shell, which 00-tools.zsh exists to avoid. It is also the first core-doctor --json key that is not a bare identifier, so the function's docstring now says which parsers care: the key is emitted quoted and the JSON is valid, but jq's dot shorthand reads .tools.git-absorb as a subtraction — consumers write .tools["git-absorb"]. Packaged essentially everywhere and installed nowhere on Linux, so it takes the ²¹ "available, not installed" shape and joins that footnote's macOS-only bullet alongside lnav. Verified against each distro's own package pages: Arch extra, Alpine community, Gentoo dev-vcs/git-absorb stable on amd64 in the main tree (no GURU), Homebrew and Debian/Kali all on 0.9.0 — note repology reports the Debian source package as rust-git-absorb while the binary you install is git-absorb. openSUSE Tumbleweed is the one laggard at 0.6.17, the gap #394 flagged, now confirmed rather than snapshotted. New PORTING-MATRIX.md row and footnote ²⁶. (zsh/00-tools.zsh, zsh/20-aliases.zsh, zsh/30-functions.zsh, git/gitconfig, PORTING-MATRIX.md)
- Fixwatchexec — event-driven repetition, the third corner of a triangle Core had two of (dotgibson/dotfiles-core#393). 00-tools.zsh now detects watchexec and sets HAVE_WATCHEXEC. viddy re-runs a command on a timer and hyperfine re-runs it a fixed count while measuring; nothing re-ran it when files changed — the "re-run the tests when I save" verb (watchexec -e py -- pytest). Own command, inert without the binary, and deliberately not aliased to watch: 20-aliases.zsh already points watch at viddy, and collapsing "re-run on a timer" into "re-run on a change" would silently hand you the wrong one. It also opens a new dev / repo group in both of core-doctor's inventories. It is the one tool in the matrix that nothing in the fleet installs, macOS included — unlike lnav, the MacBook Brewfile doesn't carry it either, so every machine is opt-in. Arch extra, openSUSE Tumbleweed, Alpine community (native musl) and Homebrew are all on 2.5.1; Gentoo has it in GURU only at 2.5.0 — and it is deliberately kept out of footnote ¹²'s GURU list, which enumerates what dotfiles-Gentoo actually installs, not what exists (the gping¹⁹ precedent). Fedora and Debian/Kali don't package it at all — confirmed against Fedora's own package search, not a repology snapshot — so those two take cargo install --locked watchexec-cli; note the crate is watchexec-cli, because plain watchexec on crates.io is the library and installs no binary. New PORTING-MATRIX.md row and footnote ²⁵. (zsh/00-tools.zsh, zsh/20-aliases.zsh, zsh/30-functions.zsh, PORTING-MATRIX.md)
- Featurelnav — the missing "read a log as a log" verb (dotgibson/dotfiles-core#392). 00-tools.zsh now detects lnav and sets HAVE_LNAV. Core had no tool for this category at all: bat/rg read a log as lines, jq/gron/jnv read it as JSON, glow as markdown — none of them knows that a log is a sequence of timestamped records. lnav autodetects the common formats, merges several files into one timeline ordered by timestamp, follows like tail -f, and exposes the parsed records to SQL. It's its own command with no alias (like jq/gron/jnv) and is inert without the binary, so nothing changes on a box that doesn't have it. Unlike the Rust/Go tools already in the matrix it is a C++ CLI, so there is no cargo install/go install escape hatch — but it doesn't need one: upstream ships static musl binaries each release (lnav-0.14.0-linux-musl-x86_64.zip plus an arm64 twin), so the fallback on an unpackaged or lagging box is "unzip the official build", not "compile it". It is detect-only on Linux: no Linux repo's install/packages.txt carries it and no bootstrap.sh installs it, so the flag lights up only once you install it yourself; macOS is the exception, where the Brewfile has carried it since 2026-07-15. That is hyperfine/shellcheck/shfmt/ouch's situation, so lnav joins that bullet in footnote ²¹ and its row carries ²¹ alongside its own ²⁴. Every version was read off the distro's own package page rather than a repology snapshot, and Fedora is reported per release because it is a versioned distro and one unqualified "current" hides the answer: F45/Rawhide 0.14.0, F44 0.13.2, F43 0.12.4. Arch extra, openSUSE Tumbleweed and Homebrew are on 0.14.0, and Alpine has a native musl build in community. Two targets lag enough to name: Gentoo at 0.11.2 — the only version in the tree, and the package needs a new maintainer — and Kali/Debian at 0.13.2. On any of them the upstream static musl zip gets you 0.14.0 without waiting for the package. New PORTING-MATRIX.md row and footnote ²⁴, and lnav joins the data / net group in both of core-doctor's inventories. (zsh/00-tools.zsh, zsh/20-aliases.zsh, zsh/30-functions.zsh, PORTING-MATRIX.md)
- FixOSC 133 semantic prompt marks — [ / ] jump between prompts in tmux copy mode (dotgibson/dotfiles-core#391). Core emitted no OSC sequences at all, while tmux has parsed OSC 133 since 3.4 and exposed previous-prompt / next-prompt in copy mode the whole time — the capability was already paid for on every machine (the fleet floor is Gentoo's 3.6a) and simply unused. zsh/00-tools.zsh now marks prompts and tmux/tmux.reset.conf binds [ / ] in copy-mode-vi to jump between them, turning "scroll up hunting for where that command started" into a keypress. No new file, no binary, no core.manifest change; { / } are deliberately left alone (vi previous/next-paragraph), and no version gate is needed. The A mark lives in $PROMPT, and that is measured rather than preferred. The obvious implementation — emit \e]133;A\e\\ from precmd, next to the command-block rule that already runs there — does not work, and fails silently: zsh's prompt preamble ends in ED (\e[J, "erase to end of screen") over the very line the mark was just written to, and tmux drops a line's prompt flag when that line is cleared. Measured on tmux 3.7b, previous-prompt then does not move at all — the feature looks implemented and does nothing. Embedding it in PROMPT as a zero-width %{…%} escape means it is re-emitted on every prompt draw, after that ED, which is also why every other shell integration marks prompts this way. The hook that applies it is APPENDED to precmd_functions, so it runs after starship_precmd re-sets PROMPT wholesale, and is idempotent for the box where PROMPT is static and would otherwise grow one mark per prompt. 45-plugins.zsh carries the same mark on the transient prompt: collapsing a finished prompt to ❖ redraws that line too, and scrollback is exactly what previous-prompt jumps through. Only A and C are marks tmux documents a dependence on, so that is the subset; D;<exit> is emitted anyway because it is free from the exit code already captured and is what non-tmux OSC 133 consumers read for per-command status. C/D stay hook output — being cleared costs them nothing, since nothing reads them back off the grid. The marks stand down in two places: under Ghostty's own shell integration, but only OUTSIDE tmux — GHOSTTY_SHELL_FEATURES is exported and reaches the tmux server, while Ghostty injects into the initial shell only, so guarding on the variable alone would have silenced the marks in exactly the place they are spent — and on TERM=dumb, which would render them as literal ]133;A garbage. Fourteen behavioural cases in scripts/test-core.sh pin all of it. One premise of #391 did not survive measurement and is recorded here so it is not re-derived: that _cmd_block_precmd returning its last print's status rather than the command's left starship_precmd reading the wrong $?. zsh saves and restores $? around each hook in precmd_functions — measured on 5.9, every hook sees the command's code regardless of what the one before it returned, a non-zero return does not stop the rest of the chain, and it never reaches the prompt's own %(?..). The hook now returns $ec anyway, as the contract the D;<exit> mark is written against, but nothing was broken and nothing user-visible changed — including starship's error indicator, which was always correct. The same correction applies to the "run our precmd FIRST so $? is the command's" comment that line has carried since P12: the ordering is worth keeping for OUTPUT order, not for $?.
- PerfThe atuin daemon's systemd path is measured, and the tail claim holds on it (dotgibson/dotfiles-core#352). Adoption's whole justification was that the daemon owning the SQLite writes removes the DB-lock contention every shell and tmux pane pays, and it had never been measured here for want of a systemd box. --systemd has now run — seven times, on Fedora 44 under WSL2 with a real user manager, a real transient unit, glibc 2.43 and atuin 18.19.0 — and every run passed the three checks a first real run had to: the unit stayed active for the whole arm, its MainPID owned the listening socket, and the row deltas were exact. On prompt latency — history start alone, the call _atuin_preexec blocks the shell on, and the only figure quotable as latency — with the history DB on a local ext4 home, three runs: p50 1.55× / 1.55× / 1.60×, p95 2.50× / 2.33× / 2.17×, p99 2.91× / 3.25× / 1.35× faster. The median win was already known; the tail win is the part that was borrowed from upstream and is now measured — with the caveat the third run makes plain, that the p99 sign is stable while its magnitude is not, so the tail belongs in the record as "faster in every run, 1.4–3.3×" rather than as a number. It agrees in direction with the one earlier blocking-call run (p99 improving 49–69%) — but that was the same machine days earlier, not a second host, so it is reproducibility over time and not independent corroboration. Total write work (start + end, which the hook backgrounds) improves at p50/p95 too, but its p99 remains unresolved — expected, since end is exactly where the two metrics diverge. Storage backing turned out to be the confounder, which is the part worth carrying forward. Same harness, same host, same day, prompt-latency p99: tmpfs flips sign run to run (2.23× slower, then 1.48× and 2.04× faster), ext4 wins in every run (1.35–3.25×), and a high-latency non-local filesystem wins 26–43× — daemon-off p99 there is 0.8–1.1 seconds while daemon-on stays flat at ~27 ms. Without a real fsync there is barely a lock to contend over, so the mechanism only becomes visible where storage is slow enough for lock hold time to matter. That is the likeliest explanation for this repo's contradictory older figures, whose storage was never recorded — likeliest, not established, since nobody re-ran them. The harness now prints the sandbox filesystem on every run, not only under CORE_ATBENCH_BASE, and adds "durable storage" to the not-covered list when the DB lands on tmpfs, because the default sandbox lives in /tmp — which is precisely where the tail is least readable. atuin/config.toml records all of it, relabels the older pair-timed and unknown-storage tables as the weaker evidence they are, and names the cheap re-run that would settle the musl question (the same Alpine container with the DB on real disk). The UNVALIDATED-SYSTEMD marker is retired across the harness, Makefile and the suite; what replaces it on the user-visible surface is the caveat that outlives the runs — not bare metal, not a real multi-pane session, not musl on hardware, and a 9p mount is a proxy for a network home rather than one. The autostart spawn cost reproduced on real disk at +42.16 ms (p50), in line with the ~+41/+45 ms seen in containers.
- FixThe modernization floor now bans allow-unsafe-pr-checkout, the one input that can re-open a "pwn request" in this fleet. actions/checkout v7 (2026-06-18) started refusing to check out fork-PR code under pull_request_target / workflow_run — a repository:, ref:, or head SHA resolving to the fork — and backported that enforcement on 2026-07-20 to v3.7.0/v4.4.0/v5.1.0/v6.1.0/v7.0.1. The single escape hatch is an input GitHub deliberately named to be easy to spot in code review and static analysis, so scripts/modern-baseline.yml now greps for it as a rule-1 banned pattern. Nothing had to be fixed first: the string appears nowhere, there is no pull_request_target in the fleet, and the lone workflow_run trigger (sync-fanout.yml) checks out a released tag rather than a fork ref — so this is purely preventative, and it fans out N-way to the OS and role repos that consume lint-call.yml@v4 and friends. dotfiles-Kali / dotfiles-Defense are exactly the repos where someone might one day reach for pull_request_target. Rule 1's existing grep -HnF sweep already covers both .github/workflows/ and .github/actions/, so no new enforcement branch was needed in check-modern.sh. Also refreshes the banned_runners rationale with ubuntu-22.04's now-fully-published schedule — deprecation opens 2026-09-17, brownouts 2027-03-23/03-30/04-06/04-13, fully unsupported 2027-04-17 (actions/runner-images#14254); comment-only, the ban itself has been in place since it was added pre-emptively.
- Perfscripts/bench-atuin-daemon.sh — the atuin daemon's latency claim is no longer purely borrowed. Adoption's whole justification is that the daemon owns the SQLite writes so shells stop contending for the DB lock, and that was cited from upstream, never measured here. The script measures the per-command pair a shell hook actually runs (history start + history end) under N concurrent writers sharing one seeded history DB, daemon off vs on, reported as p50/p95/p99 — plus the daemon-spawn cost the first command pays on the autostart path, which is unique to machines with no service manager. Report-only and deliberately not part of make audit (it needs a real atuin binary and starts a background daemon); make bench-atuin runs it, and it SKIPs cleanly on a box without atuin. It also asserts behaviourally something the hermetic suite can only take on faith from upstream's settings.rs: that atuin, with XDG_RUNTIME_DIR unset, binds exactly the socket path _core_atuin_daemon_guard probes. The harness now also covers the two paths it structurally could not, and enforces a rule that stops it lying. --systemd measures the systemd-unit path: env -i guaranteed XDG_RUNTIME_DIR was unset, so atuin's default $XDG_RUNTIME_DIR/atuin.sock — the Fedora shape, and the other branch of _core_atuin_daemon_guard's expression — was unreachable by construction. It runs the daemon from a sandbox-scoped transient unit (systemd-run --user), never your atuin-daemon.service, points XDG_RUNTIME_DIR at the sandbox so it cannot collide with a real daemon, asserts the unit's MainPID actually holds the listening socket, and skips rather than degrades without a user bus — reporting no-systemd numbers under a systemd label is the one thing the flag exists to prevent. It shipped unvalidated — written where systemd-run --user could not reach a bus, so only its fail-closed skip path had ever executed — and has since been validated against a real user manager; see the entry below. CORE_ATBENCH_BASE puts the sandbox HOME and history DB on a network home, with the socket deliberately decoupled onto a short local path — AF_UNIX does not work on NFS/SMB and sun_path caps near 108 bytes — and discloses the cost of that, which is that such a run no longer exercises atuin's default socket resolution, so the socket-agreement claim is withdrawn rather than weakened. And every arm must now prove its writes landed: the DB's row delta has to equal the samples the arm claims, or the arm is not reported. That is not hypothetical — with the daemon enabled and unreachable, atuin 18.19.0 exits 0, prints a well-formed history id, writes nothing to stderr and discards the entry (atuinsh/atuin#3561), which is the fastest table this script can produce for work that never happened. The check covers both halves, since history end updates the row rather than inserting one and a pure row count would sail past a silently-discarded end.
- FixThe atuin daemon bench's fail-closed surface is now pinned by the suite (scripts/test-core.sh Section J2). make audit can never run the bench itself — it needs a real atuin, a real zsh and a background daemon — which is precisely why the parts that are hermetic are worth asserting: that --help documents every knob including the scope caveat a figure must be quoted with, that an unknown argument still exits 2, that a malformed CORE_ATBENCH_WRITERS or CORE_ATBENCH_BASE exits 2 rather than skipping (WRITERS=0 otherwise makes every arm vacuously complete and vacuously row-correct), and that --systemd against a stubbed busless systemctl skips with no results table — the no-degradation requirement expressed executably rather than asserted in prose. The row-count SQL is extracted from the script and executed against a synthetic table, the same "run it, don't pattern-match it" idiom Section J uses on the example unit's ExecStart.
- SecurityThe guard's upstream premise is now measured weekly in CI — and the check it replaces could report "all clear" from an apparatus that had never written a row (dotgibson/dotfiles-core#383). _core_atuin_daemon_guard is a workaround for one measured fact: on atuin 18.19.0, with the daemon enabled and its socket unreachable, atuin history start exits 0, prints an id, stays silent on stderr and discards the entry (atuinsh/atuin#3561). A persistent precmd hook, a throttled connect(2) on the prompt path, and a one-way degrade in every interactive shell across eight repos are justified by that fact alone. The copy-paste recipe that carried the standing re-verification failed open. It seeded its database through the unreachable-daemon path, so on a build that discards, the database was never created; its row count masked every failure as 0; and before and after were therefore both 0, which is the premise-holds signature. It was right by luck, not by measurement — and any apparatus failure at all, a missing python3 or an unreadable DB, read the same way. Measuring "the row count did not go up" without first proving the apparatus can write measures nothing. scripts/verify-atuin-guard.sh replaces it and reports three verdicts rather than two, because the third is the one that matters: holds, moved, and unmeasurable — the last meaning the apparatus could not be trusted, which is emphatically not good news and never collapses into holds. A daemon-off control arm runs first and must write exactly one row before any verdict is allowed. Both unreachable shapes the guard claims to catch are measured — an absent socket and a stale socket file left by a crashed daemon — and each is proven unreachable first, by a bounded connect(2) and the /proc/net/unix LISTEN scan, so a delta of zero can never rest on a socket that was quietly healthy. Exit codes are the verdict (0/1/3), which deliberately breaks this repo's skip-and-exit-0 idiom in one place: exit 0 is a positive assertion about upstream, so a bare box must not be able to produce it. .github/workflows/atuin-guard-verify.yml runs it every Tuesday at 13:00 UTC against whatever atuin upstream ships that week — the one thing in this repo deliberately not pinned, because a pinned atuin would re-measure a version whose behaviour is already recorded and miss the next release, which is the one that actually costs history. That inversion is bounded structurally rather than by trust, in three jobs: resolve holds a token and verifies the download's checksum and GitHub build-provenance attestation but never executes a byte of it (gh attestation verify has no anonymous mode, which is what forces the split); measure holds no token at all and is the only job that runs upstream code, refusing to proceed unless the asset still hashes to the digest resolve attested; report holds issues: write and never sees the binary, consuming only an opaque base64 blob. holds files nothing — a bot that opens an issue weekly to say nothing changed gets muted. Review hardening, because the first cut of this got three of them wrong in ways that matter. The detector now measures four arms, not two — {absent, stale} x {--hook, plain} — because atuin's own init zsh emits atuin history start --hook -- "$1", so the plain form is a path no shell in the fleet actually runs and a change scoped to hook mode could have broken every prompt while the detector reported holds. An unreadable database mid-run is now unmeasurable, not moved: atuin_db_rows returns -1 on a failed read, after - before then goes negative, and the verdict block read that as "the row count changed" — the same apparatus-versus-upstream conflation the control arm exists to prevent, pointing the other way. And a history id is checked for shape, not merely non-emptiness: the premise is that the shell gets an id it can hand to history end, so a deprecation notice on stdout must not read as a pass. In the workflow, a value derived from an upstream file can no longer forge job outputs (a multi-line .sha256 could append ok=true after ok=false and get an unattested asset measured), and a verifier that exits outside the documented 0/1/3 — or leaves an unparseable verdict.json — now fails the job instead of passing for a quiet week. A second control arm runs last, and it closes two holes at once. The opening control proves the apparatus at t=0 only — but a database that stops being writable mid-run still reads fine, so the -1 sentinel never fires, all four arms report an honest-looking delta of 0, and the run reports holds from an apparatus that had quietly died. It also probes the premise the four arms structurally cannot see: the one-way degrade is correct only while atuin discards during the outage, and an atuin that spooled those entries would leave exactly the same absence behind — then flush them on the next successful write, landing five rows on the closing arm instead of one. That would invert the reasoning zsh/00-tools.zsh degrades on, so > 1 is a moved finding and < 1 is unmeasurable; the verdict vocabulary is unchanged, and the only new outcomes are ways to not reach holds. What it still cannot see is stated rather than implied: a spool only a live daemon would drain needs a daemon spawned to observe. Finally, the report no longer disclaims coverage it has. Its scope paragraph went on saying "--hook is not exercised" after the matrix was widened to four arms — in the same output as a reason that said "all four arms (absent/stale x hook/plain)" — and the assertion that should have caught it grepped for two nouns the false sentence also contained. The coverage claim is now derived from the arms that actually ran, in both renderers, because a hand-written one is a second copy of the matrix and the second copy is the one that rots; the test checks the report and the JSON from a single run for agreement rather than for keywords. The row-count SQL and its fail-closed -1 now live in scripts/lib/atuin-db.sh, shared with scripts/bench-atuin-daemon.sh: both rest on the same claim about atuin's schema, so a forked copy would let one gate keep believing a model the other had already found stale. zsh/00-tools.zsh gains a machine-readable # CORE_ATUIN_GUARD_VERIFIED_AGAINST= anchor, because grepping the surrounding prose — which also names 18.16.1 — is how a detector silently starts comparing against the wrong version.
- Fix/tool-scout now re-checks the workarounds whose justification can expire (dotgibson/dotfiles-core#383). _core_atuin_daemon_guard is not a preference — it is a workaround for one measured upstream fact (atuin 18.19.0 discards a history entry when the daemon is enabled and its socket is unreachable, atuinsh/atuin#3561), and every millisecond it spends on the prompt path is justified by that fact alone. Nothing was watching whether it stayed true: atuin is not pinned in mise/config.toml and has no renovate.json entry, so a version bump arrives silently on whichever machine updates first, and the behaviour has already changed once in the direction that makes it harder to notice (18.16.1 failed loudly; 18.19.0 fails silently). The routine now carries a standing re-verification list and the version each workaround was verified against. The measurement is deliberately not there and must not be copied back: that is what scripts/verify-atuin-guard.sh and its weekly job are for (entry above), and the recipe that used to live in the routine is the one that failed open. What is left is the half a script cannot do — compare the anchor in zsh/00-tools.zsh to atuin's newest release, lead with any open verdict issue, and weigh the remedy as an eight-repo change — plus the upstream questions no measurement here can reach: whether atuin still health-checks its own daemon under autostart (the guard stands down entirely there, and that is the only mitigation on Alpine and macOS), and whether it has gained a client-side buffer that would invert the one-way degrade. A changelog that does not mention the bug is still not evidence the bug is gone, so a release past the anchor is a finding in its own right. Re-verifications lead the report rather than competing inside the ranked shortlist, and "nothing is due" must be said out loud, since silence reads the same as forgetting.
Fixed
- FixThree latent faults in scripts/verify-atuin-guard.sh, each harmless while nothing spawned and each load-bearing once something does (dotgibson/dotfiles-core#402). run_one captured stdout with $( ), which blocks on pipe EOF rather than child exit — a client that daemonized without reopening stdio would have hung the arm forever, and timeout could not have cut it, since it signals only its direct child. A hit bound rendered as a finding: rc 124 reached the verdict block as "exit code is 124, was 0" and reported an apparatus limit as moved; it is now unmeasurable with a named reason. And AT_ENV was assigned inside measure(), so under set -u any trap reading it after an early return died exactly when cleanup mattered most.
- Perfcore-doctor was silently blind to twelve tools Core already detects. 00-tools.zsh probes 38 binaries into HAVE_* flags, but the health report only ever knew about 29 — ast-grep, difft, gping, hyperfine, jj, jnv, ouch, shellcheck, shfmt, tldr, uv, viddy were detected and then reported by neither the human report nor --json. Every one of them had been adopted without touching the doctor, over several releases, and nothing caught it. All twelve are now reported. The two inventories are one inventory. groups (human) and alltools (--json) were hand-synced literals; both now derive from a single _CORE_DOCTOR_GROUPS definition, so they cannot disagree by construction. The parity test is kept as the guard against a second literal reappearing. A new test closes the gap parity structurally cannot see — it reads the _have lines out of zsh/00-tools.zsh and requires the inventory to cover them, so a future adoption that skips the doctor fails the suite by name. (Direction is one-way on purpose: op has no HAVE_OP, and fd/bat are set from FD_BIN/BAT_BIN after resolving fdfind/batcat.) The group definition now carries the membership rule, so additions land somewhere defensible instead of at the end. The legend was scoped to what is true. It read ✗ falls back to classic, which held when the report was mostly command replacements. Most of the inventory is now opt-in tooling that shadows nothing — there is no classic ast-grep or jj — so it now reads ✗ absent; the replacements below fall back to the classic command. The terminal browser stays deliberately absent from the report: BROWSER_BIN picks from w3m/lynx/links2/links/ elinks, so a fixed w3m row would read ✗ on a box running lynx perfectly well. (zsh/30-functions.zsh, scripts/test-core.sh)
- Fixcore-doctor's install hint advertised a paste-ready command that could not run. It printed <manager> <every missing tool> as one line — sudo dnf install rg lnav …. But apt/dnf/zypper/pacman all abort the whole transaction on a single unresolvable name, and unresolvable names are the common case here, not the edge: these are command names, while the package is frequently called something else (rg=ripgrep, delta=git-delta, fd=fd-find on Debian, dust=du-dust, yq=go-yq, op=1password-cli), and several tools are not packaged at all on some targets (sesh anywhere, watchexec on Fedora/Kali, carapace and yazi on Kali). So one bad entry silently blocked the good ones — sudo dnf install rg alone already failed. At least 12 of the inventory names break the line on some box, which is why this is not fixed with an exclusion list; and the alternative, a per-distro command→package map, is a rot-prone duplicate of PORTING-MATRIX.md. The hint now prints the missing tools as names, states that they are command names and that packages differ, gives the manager verb as a per-tool template (sudo dnf install <pkg>), and points at the matrix for the authoritative name. A new test asserts the template form is present and that the verb is never followed by a real tool name, so the batch form cannot come back. (zsh/30-functions.zsh, scripts/test-core.sh)
- Fixcore-doctor -v printed _v=0.26.1 garbage instead of version annotations — the flag never worked. local _v sat inside the per-tool loop in _core_doctor_render, and zsh prints name=value when local re-declares a parameter that already holds one (TYPESET_SILENT is off, including under emulate -L zsh). So the first tool annotated its ✓ correctly and every tool after it emitted a bare _v=… line into the report, which is why the leak needs two present tools to show up at all. Declaring _v once alongside gi/tool/line fixes it; the assignment inside the loop is unchanged. This shipped broken and survived because nothing drove the -v path — the suite only asserted that the default render emits a group heading and that --json carries its top-level keys. There is now a hermetic case that stubs _core_have plus two shadowing tool functions and asserts both that versions render and that no _v= line appears. It uses two tools deliberately: a single-tool version of the same test passes against the unfixed code and would have guarded nothing. (zsh/30-functions.zsh, scripts/test-core.sh)
- PerfA timed-out package probe was logged as "0 upgradable" — an up-to-date box — instead of "unknown" (dotgibson/dotfiles-core#380). Every arm of the maint runner's upgradable-count chain was count=$(_to "$MAINT_PKGCOUNT_TIMEOUT" <mgr> | grep -c …), and that shape cannot tell the two apart: when timeout SIGTERMs a stalled manager there is no output, grep -c prints 0, and grep's non-zero status — the pipeline's, since it is the last stage — is discarded by the assignment. So the count=-1 "we don't know" sentinel was bypassed on precisely the failure the timeout had been added to survive (a mirror that accepts the connection and then stalls), and the daily log asserted the box was current when nothing had been measured. Counting now goes through a _pkgcount helper that captures first and gates on how the probe died — 124, the GNU/gtimeout expiry status, or >=128, killed by a signal — rather than on the manager's status. That second arm is not belt-and-braces: BusyBox timeout reports its SIGTERM as 143, not as 124, so a 124-only gate was green on every leg of the CI matrix except Alpine, where it still logged a stalled manager as 0. Gating on the manager's status instead would have been wrong in the other direction, because these managers use exit status to mean things: dnf check-update exits 100 when updates exist, pacman -Qu and checkupdates exit non-zero when there are none, so a general non-zero gate would have reported "unknown" on the healthy path. The pacman -Qu arm stays unwrapped and counted directly: it reads the local DB, cannot stall, and its 0 is real. The log line now says count UNAVAILABLE with the bound that was exceeded, instead of printing the sentinel as -1 upgradable. The nudge was never affected either way — it needs a positive count — so this was a log-and-cache honesty defect, which is the whole reason the sentinel exists. Two new tests cover it: one drives the real timeout against a manager that stalls (the pre-existing case stubs _to away by design) and reports the observed status, and one pins the BusyBox spelling on every host rather than only on the Alpine leg.
- PerfThe up nudge's cache could still be written malformed by the shell that claims the throttle slot (dotgibson/dotfiles-core#380). The writer-side normalisation added with the reader-side quoting set count to empty on a fresh box, and the claim-slot write then persisted that empty value — producing exactly the "\n<epoch>" file the fix was supposed to prevent, whose unquoted (f) split slides the epoch into the count slot and prints "1786128391 updates available". Only the quoted "${(@f)…}" read was actually holding the line. It now normalises to the -1 sentinel, so the cache is well-formed at rest: it reads back cleanly, _pkgup_notice's <1-> gate rejects it, and the nudge stays silent until the backgrounded refresh lands a real number. The comments claiming the race was closed from both ends now describe what the code does.
- Fixux_spin could take down a set -euo pipefail caller after its animation loop, and went silent when its busy-spin guard fired (dotgibson/dotfiles-core#380). The loop body was normalised (|| :) on the ground that this library is sourced — bootstrap.sh runs under set -euo pipefail — but every statement after done was still bare. A failed cursor-restore printf therefore aborted the caller with the cursor still hidden and the wrapped child still running: the identical end state documented for the unnormalised sleep. A failure in either result branch aborted between wait and rm -f, leaking the mktemp file. All of them are normalised now; rc is captured and returned explicitly, so nothing the caller sees changed. Separately, both spinners now leave one static (still running…) frame when the busy-spin guard trips. ux_spin cleared the line before the blocking wait, so a long command on a box with a broken pacing primitive showed nothing at all for the rest of the run — indistinguishable from the hang the elapsed-time readout exists to rule out — while _core_spin left a frozen glyph, which reads as "wedged". The wording, not the glyph, is what distinguishes "the animation gave up" from "the command did"; the glyph itself comes from the existing frame set, so the non-UTF-8 fallback is honoured rather than a hardcoded braille cell. The two mirrors agree again.
- Perfbench-atuin-daemon.sh started the daemon with a spelling the shipped unit deliberately probes for (dotgibson/dotfiles-core#380). Both starters ran atuin daemon start unconditionally, while examples/atuin-daemon.service goes to real trouble to ask the binary which spelling it has, because the subcommand does not exist on older builds. The bench failed closed there — socket never appears, ON arm dropped, no wrong number printed — but the diagnostic blamed the daemon rather than the spelling, on exactly the machines most likely to hit it. The bench now runs the same probe once at startup and reuses the answer in both the plain and --systemd paths. daemon stop in the teardown is left alone on purpose: it is already best-effort and the kill below it is what actually stops the process.
- FixThe weekly fleet-drift sweep reported the fleet's ordinary state as a failure. The sweep anchors to the latest released Core tag — deliberately, to avoid a false "BEHIND by N" on every unreleased commit — but make sync has never fanned out from a tag: sync-core.sh resolves git ls-remote <remote> main and vendors the branch tip, which is why the core_tag it stamps looks like v4.9.3-56-g44a44fc. Those two facts contradict each other on every day between releases. _classify treated anything not byte-identical to the reference as drift, so a single sync in an unreleased week put all eight Unix repos at "AHEAD by N", exit 1, a red run and a filed issue — while printing "run make sync", the one action guaranteed to push them further ahead. Green was only ever the accident of the fleet happening to sit exactly on a tag commit. A recorded commit ahead of the reference and an ancestor of origin/main (falling back to main) now reads as current: it carries newer Core off the released lineage, the opposite of the staleness this dashboard exists to find. The tolerance is deliberately narrow — ahead but not on main means the repo was synced from something that is not Core's release lineage, and still fails, as do behind, diverged, and a marker this clone cannot measure. With no mainline ref resolvable at all it fails closed and says so rather than green-lighting a lineage it could not check. This is the same false positive dd4f529 fixed for dotfiles-Windows in _classify_subtree; the Unix repos had carried it since. Two things that made the red run hard to read are fixed with it. The header printed the reference's sha beside the current branch — Fleet drift vs Core f95fc2b88218 (main) — so a sweep anchored to v4.9.3 looked like a comparison against main's tip; it now names what was actually resolved. And make sync is advised only for repos that genuinely lag, since it would overwrite an off-lineage marker rather than reconcile it. scripts/test-core.sh now drives the classifier hermetically against a throwaway Core — a tag, commits past it, and an off-main commit — pinning every verdict, including that real staleness still fails. Green is not the same as finished, so ahead-on-main rows get their own verdict rather than a plain ✓: a yellow •, plus a closing N repo(s) carrying UNRELEASED Core tally that prints even under --quiet. That tally — not the exit code — is now where "the fleet is running Core newer than any release" lives. It is a state that is fine to run and wrong to leave indefinitely, and flattening it into the same green as a properly pinned fleet would have traded one bad signal for a missing one. The fix it names is a release, not a sync.
- Fix--strict printed a red row and still exited 0. It is documented — in the header and in Exit: — to turn a not-checked-out repo from a skip into a failure, and it did bump the counter and print red, but it never set the drift flag, so the script returned success and every caller read the run as clean. A repo that was never cloned is now drift; it is deliberately not counted as stale, because make sync cannot repair a repo that isn't there, and the closing advice no longer offers a recipe that would not work.
- FixAn unresolvable --ref was silently answered with a different question. --ref nosuchref fell through the resolution ladder to origin/main and reported against it, while the banner still named nosuchref — the same class of mislabel as the header bug above, and worse, because the caller had been explicit. The fallback ladder exists for the default path (no tags yet, or a clone too shallow to reach one); an explicit ref that does not resolve is now a usage error (exit 2).
- FixSix documents still described a bot deleted a month earlier — including the routine whose job is to watch it. Core retired .github/dependabot.yml when the fleet moved to the shared Renovate preset in v3.2.0, but the prose never followed. .claude/commands/freshness-triage.md told the triage routine to expect dependabot.yml PRs, while scripts/freshness-dashboard.sh was already counting author:app/renovate — so the routine was briefed to hunt for an author that can never appear, in the same repo whose dashboard knew better. CONTRIBUTING.md sent contributors to dependabot.yml for the commit-prefix convention, a file that has not existed since v3.2.0. The rest were comments in freshness.yml, claude-routines.yml, and update-plugins.sh explaining the freshness bot's reason for existing by contrast with the wrong bot. All six now name Renovate, and those that pointed at a config file point at renovate.json; the triage brief — the one document that has to act on the answer — additionally carries the ci(deps): prefix and the app/renovate author signature a run should look for, which the passing mentions elsewhere do not need. The routine brief additionally gains what #377's own caveat exposed: Renovate parks bumps on a Dependency Dashboard issue that opens no PR, so an empty PR queue is not an empty bump queue — and since reading it needs gh issue list, outside this command's allowed-tools, the brief now says to report the dashboard as unchecked rather than conclude "nothing to triage". Makefile's update-hooks help was corrected differently, by deletion: it justified the target with "dependabot has no pre-commit ecosystem", and Renovate does ship a pre-commit manager. The dependency dashboard (#186) settles it — Renovate detects only devcontainer, github-actions, mise, and renovate-config here, so the target is still load-bearing — but that is a fact about the org preset's current configuration, living in dotgibson/.github, and encoding it in a help string is how the previous claim rotted. The line now states what the target does and nothing about which bot doesn't do it. Historical CHANGELOG.md entries are left alone — they were true when written.
- PerfA shell that outlived atuin's daemon recorded nothing, silently, for the rest of its life. _core_atuin_daemon_guard was a startup probe: one zsocket connect at the first precmd, then it unhooked itself. That covers a shell started after the daemon went away and nothing else — so a long-lived tmux pane whose daemon stopped underneath it (the ordinary case under systemd Linger=no, where the user daemon dies with the last login session) kept handing every command to a socket nobody was listening on. atuin 18.19.0 neither falls back nor complains: atuin history start exits 0, prints a well-formed history id, writes nothing to stderr and discards the entry (atuinsh/atuin#3561 — and note the direction of travel, since 18.16.1 at least failed loudly). A day of history, gone, with no symptom until you go looking for a command you know you ran. The guard is now a watchdog. It stays on precmd and re-probes, throttled to at most one connect(2) every 60 seconds; when the window has not elapsed the per-prompt cost is a single arithmetic expression over three integers — no fork, no syscall — which is the honest version of this layer's startup-cost discipline. What that discipline forbids on the prompt path is an unconditional syscall, not the compare that decides whether to make one. The probe itself measures ~0.06–0.10 ms against a local unix socket, so the window could be far shorter; it is 60 s because precmd fires per prompt, not per second — which already bounds the probe rate by how fast you type — and because the throttle's real job is the socket path that is not local, where connect(2) can block with no timeout available. CORE_ATUIN_PROBE_INTERVAL is the escape hatch for such a box. Three properties are deliberate, and the suite now pins each. Degradation is one-way: the first failed connect disables the daemon for that shell and unhooks the guard for good. Direct writes always work, so a false positive — a probe landing in the shipped unit's RestartSec=3 gap, say — costs only the lock relief until the next shell, whereas the opposite error costs the history; and during that gap atuin is discarding, so degrading early is still right. The warning is mid-session only: a shell already degraded at its first prompt stays as silent as it has always been (nothing changed under it, and machines that simply never run the daemon must not learn a line of startup noise), while a shell that had a working daemon and lost it prints one _core_warn line — "once" being structural, since the degrade path unhooks before it warns. And the throttle fails safe: its deadline is honoured only while it is at most one window away, so a backwards NTP step, a resume from suspend, or the $EPOCHSECONDS/$SECONDS fallback changing source mid-shell all fall through to a probe rather than parking the watchdog for the length of the jump. core-doctor now says which of the two degradations happened, and core-doctor --json grows an atuin_daemon object so a statusline can see a silently degraded shell without the user going looking. The prose has been corrected along with the code, because it asserted the opposite trade: zsh/00-tools.zsh claimed "it is a startup probe, NOT a watchdog" and that "re-probing every precmd would put a connect(2) in the prompt path", and examples/atuin-daemon.service told you sessions already open "keep discarding". atuin/config.toml and PORTING-MATRIX.md footnote ²⁰ are updated to match.
- FixThe spinner could peg a CPU core for the entire length of the command it was decorating. _core_spin's animation loop is paced entirely by _core_nap, and _core_nap cannot report failure: it swallows both arms (zselect … 2>/dev/null, sleep 0.1 2>/dev/null) and unconditionally returns 0. On a box where neither can actually sleep — no zsh/zselect module and no usable sleep — the 100ms tick silently became an unthrottled spin. Measured on the real function under a pty: 100% CPU for the command's full duration, versus 0% with the guard, same wall time and same exit status either way. The animation is cosmetic and the wait is what matters, so the loop now detects a nap that is not pacing it (>200 iterations inside 5s, unreachable with a working tick) and stops animating rather than stopping the command, falling through to the blocking wait. lib/ux.sh's ux_spin carried the same shape around a bare sleep 0.1 and gets the same guard, keeping the bash and zsh spinners the deliberate mirrors they are documented to be. Two things made this hard to see and are worth recording: the loop is unreachable without a tty ([[ ! -t 2 ]] runs the command directly, so every captured or piped run takes the passthrough path), and where gum is installed _core_spin delegates real binaries to gum spin — leaving the hand-rolled loop live only for function arguments, which is exactly what up passes it (_pkgup_list_to). The new regression test therefore drives a real pty with a function argument, and asserts on the iteration count (~201 guarded, six figures unguarded) rather than on CPU%, which is not deterministic enough for CI.
- Fix/os-package-availability cited line numbers it never read. The macbook run on 2026-08-09 (dotfiles-MacBook#120) filed a correct green verdict — all 76 Brewfile entries re-verified as resolving — but pointed two of its citations at the wrong entries: dust at Brewfile:53 when :53 is duf, and gnu-sed at :64 when :64 is visidata. The names were right and the packages resolve, so nothing was mis-diagnosed; the reference was simply to a neighbouring line. Package lists make this the cheapest possible error — they are dense, every entry inserted above a name shifts it, and neighbours look alike — and the routine's reporting rules asked for file:line without ever saying to confirm the line. The prompt now requires reading the line before citing it, and forbids carrying a number over from a previous run's report, inferring it from a nearby entry, or quoting a grep hit it has not re-checked. A green run with wrong line numbers is the corrosive case: it invites the reader to distrust the citations that are correct, which is the whole value of an availability audit.
- FixPORTING-MATRIX.md claimed two Gentoo atoms that do not exist. The Gentoo run of /os-package-availability on 2026-08-09 (dotfiles-Gentoo#80) returned a clean verdict for install/packages.txt — every atom in the Gentoo install list still resolves — but caught the matrix asserting main-tree packaging for two tools that are not in ::gentoo at all: dev-vcs/jujutsu (row + footnote 8) and dev-go/shfmt (row + footnote 7). Both 404 on packages.gentoo.org and return nothing on search. Re-verification went one step further than the report and closed off the obvious fallback: neither is in GURU either — the overlay carries no dev-vcs/jj, and the dev-go/shfmt atom exists in no repository anywhere (the third-party overlays that ship shfmt call it dev-util/shfmt). Both Gentoo cells now render as cargo³/go³, the footnote-3 convention already used for ouch, ast-grep, and sesh, and footnotes 7 and 8 say plainly that the tool is absent from the main tree and the overlay rather than naming an atom a reader would try to emerge. Footnote 7 had hedged ("verify the exact package on first stamp"), which is exactly the hedge that lets a wrong atom survive a review — a name in the Gentoo column reads as a promise that emerge <atom> works, and for these two it never did. No OS repo's packages.txt needed an edit: jj and shfmt are opt-in/dev tooling and were already carried in none of them, so nothing was ever installing the wrong name. The same pass caught a second, older error in footnote 8 that had nothing to do with Gentoo: the cargo fallback was written as cargo install jujutsu, and jujutsu is not the crate that installs jj. It is a stub pinned at 0.7.2 whose own description reads "You don't want this crate - you want the jj-cli crate"; the real one is jj-cli (0.44.0). That fallback is what the Debian/Kali cargo³ cell points at too, so the one wrong crate name had been the documented install route for every unpackaged distro, not just the two rows this change touches. It now reads cargo install --locked jj-cli, matching the --locked form the OS bootstraps already use for their own cargo builds.
- PerfEvery atuin bench figure ever produced was labelled latency and was not. The harness timed history start and history end in one span, but a shell hook does not pay for the two calls the same way: atuin's _atuin_preexec takes start in a command substitution, so the prompt blocks on it, while _atuin_precmd fires end into a detached background subshell — (atuin history end ... &) — where it costs the box and nothing else. Timing the pair measures total write work; only start is time a human waits. The writer now takes a timestamp between the two calls (both metrics from one pass, so the tables are strictly comparable — same samples, same contention) and the results print as two clearly separated tables, latency first, each saying what it may be quoted for. This is not a presentational fix: it puts the far-tail conclusion back in question. The recorded finding that the daemon trades frequent small waits for rarer, larger stalls comes entirely from pair-timed runs, while the one measurement that timed only the blocking call (Fedora 44 under WSL2, systemd unit) found p99 improving 49–69%. end is precisely where the two would diverge — with the daemon off it is the slower call, and with it on they equalise. atuin/config.toml and zsh/00-tools.zsh now relabel their figures as total write work and mark the tail question open in both directions rather than settled against the daemon; the p50/p95 win is unaffected and still holds on every host tried. No new measurements are claimed here — this change makes the re-measurement possible. The parser is the risky half and is tested accordingly (test-core.sh Section J2): the previous one split each file on all whitespace, so two-column input would have flattened into one distribution of double the length — a table that looks completely normal and is completely wrong. The stats block is now extracted and executed against synthetic samples whose two columns differ, pinning that each table reports its own, and a malformed sample line refuses the arm instead of being coerced.
- FixThe atuin bench dropped an arm roughly one run in eight, and the reason looked like atuin misbehaving under contention. history start is not the only write a command makes: meta.db (and the key beside it) are created lazily by the first history end. The warmup in db_reset ran history start alone, so meta.db did not exist when the writers launched and all N of them raced to create and migrate it on their first history end — one losing on UNIQUE constraint failed: _sqlx_migrations.version, which aborted that writer at iteration 1 and cost the whole arm. It was always the first arm, because meta.db survived a db_reset that only ever removed history.db. The warmup now runs a complete start+end pair, and the snapshot/restore covers the whole data directory rather than one file — which also delivers what the old comment already claimed: records.db grew monotonically across arms before this, so each arm was measured against a bigger sync store than the one before it, exactly the variable being controlled for.
- FixThe bench could never detect musl, and mislabelled the one run where that mattered. ldd --version 2>&1 | grep -qi musl looks right but cannot work under the set -o pipefail in force at the top of the script: musl's ldd exits non-zero after printing its banner, so the pipeline fails even though grep matched. Every musl run therefore reported unknown libc and went on to list musl among the things it had not covered — on the one run where that was false, and on the cheapest of the remaining gaps. Now the output is captured and matched as a string.
- FixThe showcase was never told a release had happened, and had not been since the notification was written. notify-web.yml listens for release: published, but the Release is created by release.yml running gh release create under the built-in GITHUB_TOKEN — and an event raised by GITHUB_TOKEN never starts another workflow run (the same recursion guard that stops a GITHUB_TOKEN push from firing pull_request). So the Release published, the event was inert, and dotfiles-web's repository_dispatch: types: [core-release] received not one POST in its lifetime. Nothing about the dispatch itself was broken — right event type, right target, working token — which is why it read as healthy from both ends. User-visible downstream: the site's only remaining refresh was a Tuesday cron, so its committed generated.json sat two releases behind (v4.7.1 against Core's 4.9.3) and every published install command was pinned to a stale --branch. release.yml now dispatches core-release itself from a job after publish, where no guard applies; notify-web-call.yml grew an event_type input (default refresh, so the @v4 callers across the fleet are untouched), validated against an allowlist because a typo'd type POSTs 204 and triggers nothing. That job is best_effort, because sync-fanout gates on this workflow's overall conclusion and a failed notification must never be able to stop a published tag from reaching the OS repos. notify-web.yml keeps its release: trigger for a Release published by hand from the UI, and now documents the trap so the dead path isn't mistaken for the live one.
- Fix/os-package-availability could query a single release and still return "Clean" — the one verdict the routine exists to rule out. Step 1 said to confirm each name "still exists in this distro's repos" without ever saying which releases to look in, so a run against one release could not distinguish "present everywhere" from "already dropped in the next release" — and would report a version read from stable as evidence the name resolves, full stop. That is not hypothetical: the Fedora run filed a Clean verdict while tealdeer and procs had both gone orphan and neither had been rebuilt for rawhide/F45, quoting their F43/F44 versions as passes. Both still install today and break on the F45 upgrade, which is exactly the early warning this audit is for. The routine now picks targets by release model: versioned distros (Fedora, openSUSE Leap, Alpine stable) need every currently-supported stable release plus that distro's own development branch where one exists, while rolling targets (Arch, Gentoo, Homebrew, Kali, Tumbleweed) have a single current repo that is itself full coverage. It also requires every quoted version to name the release it came from; classifies "in stable, gone from that distro's development branch" as Drifted rather than a pass; and requires a Clean verdict to state its release coverage and reconcile N-checked against N-in-list, so a partial run has to call itself partial.
- Fixclaude-routines-call.yml ran the routines from a frozen v3 checkout. The reusable workflow checks out dotfiles-core to get the routine prompt, PORTING-MATRIX.md and the pinned CLI, and pinned that checkout to ref: v3 — directly under a comment reading "Core@v4 at ROOT … (v4 = the current major, matching the @v4 callers)". v3 is frozen at v3.9.0 (2026-07-19) while the line has since reached v4.9.3, and the routine prompt differs between the two, so every scheduled run has been executing the v3.9.0 prompt no matter what shipped in v4 — including the fix above. Bumped to v4 so the callers and the content they run agree.
- Configlint-call.yml and auto-tag-call.yml ran the fleet from the same frozen v3 checkout. The defect above was not confined to the routines workflow — these two reusable workflows carry it in the three remaining pins, and the lint one is the consequential half. Both check out dotfiles-core for the pinned scripts/tool-versions.env, the setup-core-tools composite and the release scripts, and pinned that checkout to ref: v3 while every comment beside them declared v4 (lint-call.yml:57 reads "v4 = the current major, matching the @v4 callers"; the auto-tag step said "pin to the SAME major line callers pin this workflow to (@v4)" and then pinned v3 in the same breath). So every OS repo's lint gate has been running v3.9.0's pinned tools — shellcheck 0.10.0, shfmt 3.8.0, actionlint 1.7.8 — while Core lints itself with 0.11.0 / 3.13.1 / 1.7.12: the fleet was held to a weaker gate than the repo defining it. Bumped all three pins to the moving v4 alias. Measured before bumping, against dotfiles-Fedora with the gate's exact SHELLCHECK_OPTS and file selection: shellcheck 0.10.0 → 0.11.0 is byte-identical (exit 0, no findings either way) and actionlint 1.7.8 → 1.7.12 likewise. shfmt is advisory by construction — the step wraps it in an if/else that swallows the drift exit rather than setting continue-on-error (lint-call.yml:156-170), so new formatting opinions in 3.13.1 can only warn; note that a genuine shfmt install failure still reds the step, which is the point of not using continue-on-error. So the bump is expected to be a no-op for the blocking legs rather than a new-findings event — verified on one repo, not all eight.
- Securityup and the maintenance runner could hang forever, invisibly, on a package manager that stopped to ask a question. dnf5 verifies repository metadata signatures against a per-repo, per-user keyring (<cachedir>/<repo>/pubring), not the rpm keyring. So a repo with repo_gpgcheck=1 whose signing key only ever reached root's keyring — the ordinary outcome of a bootstrap that runs sudo rpm --import and then sudo dnf install — re-prompts to import it on every non-root --refresh, and since a declined import is never persisted, it prompts again forever. Every probe that hits this runs with stdout captured by $(...) and stderr sent to /dev/null, so the question is invisible while it holds the terminal. _pkgup_count/_pkgup_list were documented as backgrounded, where zsh's nomonitor hands a job /dev/null stdin and the shape was accidentally safe — but up calls _pkgup_refresh in the foreground once the upgrade finishes, so it inherits the terminal and up prints Complete! and then never returns. The runner's upgradable-count block is not a step(), so it had no stdin discipline at all and the run stopped dead after the last ✓ with no error. Pin stdin so the probes cannot be prompted regardless of caller, give step() the same treatment — which closes the identical exposure on the git-credential and tpm paths — and bound the count probe with _to (MAINT_PKGCOUNT_TIMEOUT, default 180s) for the separate case of a mirror that accepts the connection and then stalls. The redirect goes on the case/fi, not the function definition: in zsh f() { … } </dev/null binds at definition time and does nothing at call time, so it reads as correct in review while fixing nothing. Regression tests assert the probes cannot consume the caller's stdin rather than asserting they don't hang — same property, but it fails instead of wedging a suite that has no timeout anywhere.
- Fixexamples/atuin-daemon.service started the daemon by a deprecated name, and that failure mode is silent. ExecStart ran atuin daemon; 18.19.0 warns on every start and points at atuin daemon start. On its own that is cosmetic — but with the daemon enabled and unreachable, atuin exits 0, prints a well-formed history id, writes nothing to stderr and discards the entry. So the day the old spelling is removed, ExecStart fails and Restart=on-failure/RestartSec=3 retries it forever with nothing ever listening. Scoped honestly: a Core shell started after that is fine — _core_atuin_daemon_guard probes the socket at its first precmd, finds nothing, and forces the daemon off so atuin writes SQLite directly. The exposure is shells that had already completed that one-shot probe while the daemon was alive, and anyone consuming this unit without Core's guard — which, examples/ being a copy-paste target, is precisely who it is written for. The unit now asks the binary which spelling it has and execs that, because the subcommand does not exist on older atuin and this file is copy-pasted onto machines Core does not control. Two things that do not work and are pinned by tests: exec A || exec B (once exec succeeds the process is replaced, so a non-zero exit can never reach the ||), and probing with atuin daemon --help (exits 0 on both spellings, so it proves nothing — which is why dotfiles-Fedora's existing capability probe would have installed a unit the binary rejects). New scripts/test-core.sh Section J covers the file, including systemd-analyze verify; note it remains classified repo-meta by ci-classify, so an examples-only change still gates nothing.
- Fixexec zsh — the documented first step after a bootstrap — dropped you into zsh-newuser-install with no Core loaded. The managed ~/.zshrc exports ZDOTDIR=$XDG_CONFIG_HOME/zsh, but nothing ever created $ZDOTDIR/.zshrc. The first shell was fine (ZDOTDIR unset ⇒ zsh reads ~/.zshrc); every zsh started from inside it inherited the export, found none of .zshenv/.zprofile/.zshrc/.zlogin there, and was treated as a brand-new user. The wizard was the visible half — the real damage was a shell with no fragments, no plugins, no prompt. On a non-TTY there was no wizard at all, just a silently empty shell; and the wizard's own option (0) writes a comment-only $ZDOTDIR/.zshrc, permanently suppressing it while permanently keeping the shell empty. blib_write_zshrc_loader now seeds $ZDOTDIR/.zshrc as a link to ~/.zshrc (via blib_link, so it backs up, is dry-run aware, and is idempotent) — including on the already-managed early-return path, so boxes bootstrapped before this fix are reconciled on the next run rather than only on a fresh write. Note scripts/bench-core.sh and scripts/new-os-repo.sh already built the coherent $ZDOTDIR model, which is exactly why the suite never caught this; Section I of scripts/test-core.sh now asserts it.
- PerfThe update nudge could report a Unix timestamp as the package count — e.g. 1786128391 updates available. _PKGUP_CACHE is positional ("<count>\n<epoch>") but both readers split it with an unquoted ${(f)…}, and zsh drops empty fields from an unquoted expansion. The empty count is not something _pkgup_refresh can write — it normalises an empty result to -1. It comes from the startup hook itself: on the first shell of a fresh box there is no cache, so the count it reads is empty, and claiming the throttle slot persists that empty field alongside a fresh timestamp while the background refresh is still in flight. Read back unquoted, the leading empty field vanishes and the epoch shifts into the count slot — where it passes the <1-> positive-integer check and renders. From there it is self-sustaining: last shifts to empty ⇒ 0, which defeats the once-a-day throttle so the check re-fires on every shell, each one rewriting the bogus count. Both reads are now quoted ("${(@f)…}"), and a non-numeric count is discarded before it can be written back, closing the race from the writer side too.
- Perfzsh/00-tools.zsh documented an atuin fallback that does not exist. The comment on _core_atuin_daemon_guard said an absent or stale daemon socket makes "every atuin call pay a failed connect and an error" and that "atuin then writes SQLite directly" — so a missing daemon "must cost latency". Measured against atuin 18.19.0, none of that holds: atuin history start exits 0, prints a well-formed history id, writes nothing to stderr, and discards the entry (verified for an absent socket, a stale socket file, and with and without --hook; the daemon-off control writes every row). The guard is therefore data-loss prevention, not a latency optimisation, and the "startup probe, not a watchdog" caveat is correspondingly sharper: a daemon that dies mid-session costs that shell every subsequent command, unrecorded and unannounced. Comment corrected; no behaviour change.
- Configcore.manifest advertised a keybinding that does not exist. Its zsh/35-fzf.zsh stanza named Ctrl-F/R for the fzf widgets; zsh/40-bindings.zsh binds ^T, and there is no ^F binding anywhere in Core — PARITY.md even records that zsh moved off Ctrl+F. A one-token error, but in the file the system calls its contract, vendored verbatim into eight repos, so it misinformed eight copies at once. Now Ctrl-T/R.
- FixThree PORTING-MATRIX.md footnotes asserted "nothing installs this" against repos that do, and two of them contradicted each other: - ¹⁷ said jnv is in no Brewfile; dotfiles-MacBook/Brewfile carries it. Scoped to Linux, with macOS named as the exception. - ¹⁹ said no repo installs gping; the same Brewfile carries it. Same scoping. - ¹² listed gping among Gentoo's GURU-overlay atoms while ¹⁹ said nothing installs it. ¹⁹ was right: gping appears nowhere in dotfiles-Gentoo's guru_install list, its packages.txt, or its bootstrap.sh at all. Dropped from ¹².
- FixThe matrix sent Kali to mise/cargo for tree-sitter-cli, which it apt-installs. dotfiles-Kali/install/packages.txt carries the plain apt name and its bootstrap.sh has no tree-sitter installer, so the ³ footnote pointed at a path the repo never takes.
- FixFootnote ⁹ named an AUR package that does not exist. It said sesh is "Packaged in the AUR (sesh)"; the AUR has no package under that bare name. The real one is sesh-bin, which declares provides/conflicts on sesh — so paru -S sesh resolves anyway, which is precisely why the wrong name read as correct. Confirmed against the AUR RPC: an info lookup for sesh returns nothing, and a name search returns eight packages, none of them a bare sesh, ruling out a source-build entry alongside sesh-bin. The Arch cell on the sesh row still reads AUR, which was always accurate; only the footnote was wrong.
- Fixlib/bootstrap-lib.sh still gave the atuin advice v4.9.3 corrected. It told you to re-apply a backed-up local config "via ATUIN_* env" with no carve-out — the exact pattern that release proved does not work for the ten keys atuin/config.toml sets. This was the last surviving instance; PORTING-MATRIX.md, examples/README.md and both OS layers were already correct.
- FixA cross-reference dangled one release after it was written. The v4.8.0 correction note pointed at "the [Unreleased] entry on the daemon opt-in"; cutting v4.9.3 promoted that entry, leaving the pointer aimed at an empty section. Now names [v4.9.3] — the hazard of referring to [Unreleased] from a dated section at all.
- FixCore told you it fans out to nine OS repos. It fans out to eight. scripts/os-repos.txt has been the canonical fleet — and has documented dotfiles-Windows and dotfiles-Debian as deliberately absent — for several releases, but five comments still asserted the old count: .github/workflows/release.yml, .github/workflows/ci.yml, scripts/update-nvim-plugins.sh, scripts/test-core.sh, and scripts/audit-core.sh. ARCHITECTURE.md is deliberately unchanged: "one Core plus nine machine repos" counts machine repos including Windows and is correct — the two numbers are both right in their own sentence, which is exactly why a find-and-replace would have broken it. (.github/workflows/release.yml, .github/workflows/ci.yml, scripts/update-nvim-plugins.sh, scripts/test-core.sh, scripts/audit-core.sh)
- Configgsync was documented as an alias in a file deleted in v4. It is a function — zsh/20-aliases.zsh says so two lines above the definition, and explains why (a dotfiles path containing whitespace must stay one word). Three places carried the stale zsh/aliases.zsh path, a filename that has not existed since the v4 NN-name.zsh renumbering. (core.manifest, zsh/completions/_gsync, .bin/sync-upstream.sh)
- Configblib_link_core's own comments under-sold what it links. The doc header omitted lazygit, jujutsu and the seeded sesh config; the tools group banner omitted jujutsu and atuin — both of which the code directly beneath it links. The complete enumeration already existed at the top of the file, so both now point at it as the canonical list. (lib/bootstrap-lib.sh)
- FixPre-v4 module names in comments that describe current behaviour. tools.zsh, options.zsh, ui.zsh and maint.zsh have been 00-tools.zsh, 10-options.zsh, 05-ui.zsh and 55-maint.zsh since v4. Note blib_migrate_v4 deliberately keeps the unnumbered names — it exists to delete stale pre-v4 symlinks, so there the old spelling is the correct one. (core.manifest, lib/bootstrap-lib.sh)
- FixPORTING-MATRIX.md promised bootstrap installs that do not exist. The ³ marker means "bootstrap.sh installs it best-effort", but six cells carried it with no installer behind them — ouch and jujutsu on Gentoo and Kali, ast-grep and shfmt on Gentoo — verified against each repo's bootstrap.sh and install/packages.txt. Kali does install ast-grep, so that cell keeps its ³. A new ²¹ marker records the honest state, reusing the detect-only shape jnv¹⁷ and gping¹⁹ already established: available, not installed. It also covers four rows that are macOS-Brewfile-only in practice (hyperfine, shellcheck, shfmt, ouch — no Linux repo installs any of them), and lazygit on Kali, the sharpest case: every other Linux repo installs it, Kali installs it nowhere, and Core ships alias lg='lazygit' regardless. This is the same overclaim already corrected once for openSUSE. Alpine's ouch cell also gains the ¹⁴ testing-repo footnote every comparable cell already had. (PORTING-MATRIX.md)
- FixThe atuin-daemon table read as shipped state when it is mostly a recipe. The exports are wired on two of the seven Core-vendoring machines the table covers (Fedora, Alpine) — now marked ✔, with the other five labelled as the documented recipe and Windows called out as neither, being out of scope. The marker is per machine rather than per row, since the systemd row holds a wired Fedora next to four unwired ones. Defense is dropped from the systemd row: that row tells you to put exports in os/<os>.zsh, and Defense is distro-agnostic with no os/ layer, as the same file says under "Repo status". The Built: list also omitted Defense entirely. (PORTING-MATRIX.md)
- Configdotfiles-Defense is now recorded as the documented scaffold exception. core.manifest claimed lib/bootstrap-lib.sh is sourced by each OS repo's bootstrap.sh; Defense hand-rolls its own link() and .zshrc heredoc instead. That is deliberate — Defense is a role layer stacking onto an already-provisioned host, where the OS repo underneath has already run the scaffold — so the claim is narrowed rather than the code changed. (core.manifest, PORTING-MATRIX.md)
- FixREADME.md billed aliases.md as the "full" cheat sheet. It omits the function verbs core help indexes (fif, fbr, maint-*, op*). core help is the complete index and now says so; aliases.md is described as the curated companion. (README.md)
- Fix/doc-audit compared a release-pinned mirror against main, and reported a false positive. dotfiles-web's porting-matrix.md is diffed by its own CI against Core at releases/latest — the newest release tag, not main. The routine had no such carve-out, so it measured the page against main and called it "a pre-correction snapshot". It was not: it was byte-identical to Core at v4.9.3 and its check was green. Acting on that report re-mirrored main into a file whose contract is the tag and turned a passing check red, which is how it was caught. The routine now states the reference frame explicitly, and that a mirror lagging main while matching the newest release is correct, not drift. (.claude/commands/doc-audit.md)
- FeatureThe refresh row implied Arch has a refresh alias. It deliberately does not. sudo pacman -Sy was listed with no note, while dotfiles-Arch's os/arch.zsh explains at length that there is no -Sy alias on purpose — refresh-then-install is the partial-upgrade footgun, so it ships pacu (full -Syu) and pacout (checkupdates, which never touches the sync DB). New footnote ²³ records that the cell is completeness, not a recommendation. (PORTING-MATRIX.md)
Changed
- Configfleet-drift now says how far behind main's tip an unreleased row still is (dotgibson/dotfiles-core#381). _classify measured the recorded sha against the release tag only, and git merge-base --is-ancestor is reflexive at both ends — so "on origin/main" was equally true of a repo synced this morning and one synced five weeks ago, and both printed the identical current (ahead of vX.Y.Z by N, on origin/main). A stalled fan-out was therefore invisible inside a green sweep: at the time of writing the whole fleet sat 56 commits past v4.9.3 while main had moved 111 past it, and nothing in the report named the 55 unvendored commits. The ahead-on-main row now appends , N behind its tip when that distance is non-zero. Report-only, and deliberately so. The current prefix, the • third state, the UNRELEASED tally, DRIFT/STALE/OFFLINEAGE and every exit code are unchanged — a green run stays green. The previous entry in this file taught readers that a fleet-drift wording change implied a verdict change; this one does not. Re-reddening the sweep once the fleet drifts far enough from main was considered and rejected: that is exactly the #371 failure mode where the fleet's ordinary between-release state pages a human, and the threshold would be unjustifiable. The two numbers now read as a pair — ahead of the tag says a release is owed, behind its tip says a make sync is owed. Zero omits the clause entirely rather than printing 0 behind its tip, which keeps an at-tip row byte-identical to its old wording — and makes the suite's existing …, on main) regex a live oracle for that case. _classify_subtree (dotfiles-Windows) deliberately gets nothing: its marker is re-stamped only when nvim/ changes, so a behind-main count there would report a lag for every Core commit that touched anything else — the exact false-BEHIND that the subtree path exists to eliminate.
- Fix/drift-triage can run the sweep it is built to interpret (dotgibson/dotfiles-core#381). The routine's own step 1 told it to run scripts/fleet-drift.sh — without the leading ./ that Bash(./scripts/fleet-drift.sh:*) matches, so every invocation was denied — and to pass the sibling fleet "via --add-dir", a Claude Code flag the script's parser rejects with a usage error. Neither is needed: --root already defaults to this repo's parent, which is where the fleet is checked out in CI too. The command is now spelled out literally as ./scripts/fleet-drift.sh --color never. The consequence was not a missing section but a wrong report: blocked from its primary tool, the routine reconstructed _classify's logic by hand from the core.lock markers, reached the opposite verdict from the script (red, when the sweep exits 0), and shipped it without a hedge. The command now forbids that explicitly — an unrun sweep is a finding to report, not a gap to fill in — and documents the three row states with the remediation each one actually takes, since a • unreleased row is fixed by cutting a release, never by the make sync the old text prescribed for everything.
- PerfThe atuin latency question is closed, and the part that will never be measured is now recorded as a decision rather than a backlog (dotgibson/dotfiles-core#352). The measurable half is measured — see the bench-atuin-daemon.sh entries under Added. The remaining four rows (musl on real hardware, a real NFS/SMB home, bare metal, a real multi-pane session) need machines this project does not have and will not get, so atuin/config.toml now states plainly that their rationale stays borrowed from upstream on purpose. The mechanism is measured; what is borrowed is its magnitude on hardware nobody here runs. An open issue promising numbers that cannot arrive is worse than a documented decision not to chase them. Also corrects an overclaim this changelog and atuin/config.toml both carried: the earlier systemd-unit run was described as a second, independent Fedora host corroborating the new figures. It is the same machine — Fedora 44 / kernel 6.18.33.2 (WSL2) — measured days apart. That is reproducibility over time, not independent corroboration, and every figure in the record comes from one WSL2 host. Overstating corroboration is precisely the failure #352 was filed to catch, so it is fixed at every site that made the claim: atuin/config.toml, scripts/bench-atuin-daemon.sh's header, and both unreleased entries in this file — the one above and the earlier bench(fix) entry, which described the same run as "real Fedora hardware" too. That fourth site was missed on the first pass because the check that was supposed to prove the claim filtered CHANGELOG line numbers by a guessed section boundary instead of the actual ## [Unreleased] extent, and so excluded the line it needed to catch.
- Configsd silently stopped matching across newlines, and its --version won't tell you. Upstream 1.1.0 made line-by-line processing the default and moved the old whole-file behaviour behind --across / -A. Nothing in Core breaks — sd is detect-only (HAVE_SD) and deliberately un-aliased, and no Core code shells out to it — but a multiline pattern in muscle memory or in a role script now matches nothing, leaves the input untouched, and still exits 0, so the caller carries on as if it had rewritten the file. Verified behaviourally rather than read off the release notes: sd 'alpha\nbeta' X on two-line input returns rc=0 with the input unchanged, and sd --across matches. This earns a PORTING-MATRIX.md footnote (²²) rather than a detection change for two separate reasons. Core needs no runtime change: nothing here calls sd, so there is nothing to gate. And the version string could not carry a gate anyway — the Homebrew 1.1.0 build self-reports sd 1.0.0, so HAVE_SD could never have keyed off it. Consumers that genuinely must know — a role script targeting both builds — feature-detect instead, with sd --help | grep -q -- --across, and add -A only when the probe says the flag exists; hard-coding it breaks the pre-1.1.0 builds this matrix tracks, which already match whole-file. Same class of footnote as batcat (⁴) and the mikefarah-vs-kislyuk yq split (⁶): the command is not quite what its name implies. Found by the weekly /tool-scout scan (#376).
- Fixpre-commit moved off a known-broken patch: 4.6.1 → 4.6.2. 4.6.2's sole content is a fix for a 4.6.1 regression in language: node hooks whose package.json declares a scripts.build key, under npm 11.x (pre-commit#3737). It is not fixing a live failure here — markdownlint-cli2 is Core's only node hook, and v0.23.2's manifest carries build-docker-image and friends but no plain build, so it misses the trigger condition. Taken anyway, on the principle that sitting on a patch upstream has already superseded is a bet the next hook addition doesn't collect. One line in scripts/tool-versions.env; the three consumers (ci.yml, scripts/setup.sh, .devcontainer/devcontainer.json) all read the variable, so no literal moved with it. No checksum refresh applies — PRECOMMIT is a pip install, not a raw release download, so it carries no *_SHA256 and is absent from both scripts/update-tool-checksums.sh and the audit's section 9b. No .pre-commit-config.yaml change either: the audit's version-consistency section gates PRECOMMIT_HOOKS_VERSION (the hook repo's rev:), never the pre-commit binary. Of the nine remaining pins, the eight gate tools were checked against upstream in the same pass and are current; CLAUDE_CODE_VERSION is the one exception, deliberately left at 2.1.222 with 2.1.227 available — it changes the scheduled routine bots' behavior, and moving it alongside an unrelated fix would make a later routine regression ambiguous to bisect. It moves on its own.
- FixThe daemon's contention claim now has a musl number. Measured in an Alpine 3.21 container (real Alpine userland, real musl, no systemd) against a glibc control on the same host, atuin 18.19.0, two runs each. The p50 win holds and is the most robust result so far (~1.4x on both libcs), but the far tail is where they diverge: on musl the p99 was worse with the daemon on both runs — a stable sign, where glibc gives a coin flip. That is the strongest evidence yet against selling the daemon as a tail fix, and it lands on the path Alpine actually ships. atuin/config.toml carries the table and the caveat that a container is not real hardware.
- ConfigThe @vN pinning policy is no longer stated as universal, because it is 27 of 28. dotfiles-Windows SHA-pins its auto-tag-call caller on purpose — immunity to a moved tag, traded against the auto-fan-out — and both RELEASE-RUNBOOK.md and RELEASE-STRATEGY.md read as though every caller tracks @v4. Worse, the runbook's own straggler sweep (grep -rl 'uses:.*@v4' across scripts/os-repos.txt) structurally cannot find it: Windows vendors no core/, so it is not in that list. It is therefore invisible to the grep and unmoved by the alias — currently several releases behind. Both documents now name the exception and say to check it by hand.
- PerfThe daemon rationale in atuin/config.toml and zsh/00-tools.zsh now reports what was measured, and it is not the whole pitch. A container run reproducing the topology of the Alpine path (no systemd, XDG_RUNTIME_DIR unset) puts the median and p95 win at ~1.4× and 1.2–1.3× — real, and steady across runs. But p99 flips sign run to run and the maximum is consistently ~2× worse with the daemon on: it trades frequent small lock waits for rarer, larger stalls. "Removes the tail latency" was therefore an overclaim in both files and is now scoped to the typical command rather than the worst one. The autostart path's first command additionally pays ~+41 ms for the spawn. Still unmeasured and still needing hardware nobody has to hand: musl, the systemd-unit path, and a network home — where the claim is strongest and least tested.
- PerfPlugin pins rolled forward. Routine freshness sweep, landed by the bot and previously unrecorded here. Six Neovim plugins in nvim/lazy-lock.json (fzf-lua, nvim-lspconfig, nvim-tree.lua, nvim-treesitter, package-info.nvim, schemastore.nvim) and the zsh zsh-syntax-highlighting pin in zsh/45-plugins.zsh. Pins are what stop plugins floating silently into eight repos, so every roll is a change those repos receive on their next sync — CONTRIBUTING.md requires it in the changelog, and there is no carve-out for automation. (nvim/lazy-lock.json, zsh/45-plugins.zsh)
Fixed
- FixThe atuin daemon opt-in never worked. ATUIN_DAEMON__ENABLED=true was silently ignored on every machine. Core shipped atuin/config.toml with [daemon] enabled = false written out explicitly, and that assertion is what broke it: atuin builds its config as defaults → environment → config file, adding the file source last (settings.rs — the Environment source goes in at the builder, the file at build_config() afterwards), and in the config crate the later source wins. So any key this file mentions **shadows its ATUIN_* override. The one key the whole per-OS design depends on being overridable was the one Core asserted. The fix is to write no value: enabled and autostart are now left unset. Upstream's own defaults are already false/false (settings.rs:1515-1516), so Core still ships the daemon off — off by default rather than off by assertion — and the override reaches it. Measured, not reasoned, against atuin 18.19.0 built from crates.io. atuin doctor reports the resolved daemon_enabled, and the client was straced for connect() on the socket, which is the only thing that distinguishes the two paths — exit codes cannot, because the client degrades silently to direct SQLite when the daemon is unreachable, which is exactly why this went unnoticed: | Config | daemon_enabled | connect(atuin.sock) | | --- | --- | --- | | enabled = false written + ATUIN_DAEMON__ENABLED=true | false | 0 calls | | key absent + ATUIN_DAEMON__ENABLED=true | true | 1 call | | no config file at all + ATUIN_DAEMON__ENABLED=true | true | — | | after this change, no env | false | — | | after this change, ATUIN_DAEMON__ENABLED=true | true | 1 call | What this was costing the fleet: dotfiles-Fedora's bootstrap installed and enabled a systemd unit that started a daemon no client ever talked to, and dotfiles-Alpine's exports were inert. Core's guard made it quieter still — it reads ATUIN_DAEMON__ENABLED from the environment, where it was set, so on Fedora it found the unit's socket present, stood down satisfied, and reported healthy while every write went straight to SQLite. Nothing was broken for a user; the feature simply did not exist. scripts/test-core.sh now asserts the two keys stay unset, negative-tested by putting enabled = false back and watching it fail. The check is static because the behavioural proof needs an atuin binary CI does not have. The same trap applies to any future per-machine key — asserting even its default disables the override — which is now stated in the config header, PORTING-MATRIX.md footnote 20, and beside the block itself. Three follow-ups from review, all of them the same defect wearing other hats: - The guard scanned only inside a literal [daemon] table, so the equally valid dotted form daemon.enabled = false at top level recreated the bug and passed green. Widening the regex was still the wrong shape — daemon = { enabled = false } and daemon . enabled = false are also valid and also deserialize to the same key, so a pattern match can only ever cover the spellings someone thought of. The guard now parses the TOML with tomllib (the idiom audit-core.sh's config gate already uses) and inspects the resolved daemon table, which is what atuin itself resolves. All four spellings negative-tested; an unparseable file fails distinctly rather than being read as clean. - The config header advertised export ATUIN_SEARCH_MODE=prefix as its example of an override — while the same file writes search_mode = "fuzzy", which makes that export silently ignored. Documenting the precedence trap and then demonstrating it was the worst of both. The example now uses sync_address, a key the file genuinely leaves unset, and the header names the ten settings that are deliberately not** overridable so the distinction is explicit rather than inferred. - The v4.8.0 upgrade note told adopters to port sync_address, auto_sync and filter_mode to ATUIN_* overrides. The first two work; filter_mode is written by this file and cannot. That entry now carries the correction inline rather than being quietly rewritten — it was wrong when shipped, and the record should say so. Verified against atuin 18.19.0 rather than assumed: with Core's config in place, ATUIN_SEARCH_MODE=prefix still resolves to fuzzy and ATUIN_FILTER_MODE=prefix still resolves to global.
Added
- Fixpsmux power pill — the battery segment the macOS tmux bar has. New psmux/scripts/psmux-power.ps1, rendered right-most in status-right, which is where Core puts it too (its last slot is #{@status_right_os}, the hook each OS repo fills). It is the Windows port of Core's tmux/scripts/tmux-battery.sh and uses that scale, so the two terminal bars agree: green ≥60 / yellow ≥20 / red <20, with the level glyph swapped for a charging bolt on AC and the colour still tracking the level. One deliberate divergence — Core prints nothing when there's no battery, so its segment vanishes on a desktop; here it falls back to Zebar's AC placeholder, a lone green md-power-plug , since an empty segment reads as a broken pill on a desktop-first host. Power state comes from SystemInformation.PowerStatus (one in-process GetSystemPowerStatus read), not Win32_Battery — a desktop returns nothing from the latter, so "no battery" and "the query failed" would be indistinguishable. Refreshed by the existing in-session timer alongside the VPN pill, so nothing new touches psmux's synchronous render path. psmux.conf seeds @pwr_pill with set -og (only-if-unset) so the desktop plug is right before the first tick, while a prefix + r reload can't clobber a live laptop reading. (psmux/, powershell/os/33-psmux-pill.ps1) Note this means the psmux bar and Zebar disagree between 40 and 60 % — deliberately. The bars are matched terminal-to-terminal (psmux ↔ Core tmux) and desktop-to-desktop (Zebar ↔ sketchybar), and those two references use different scales.
- FeatureTest coverage for the power pill's every state. The dev box is a desktop, so the laptop branches would otherwise ship unexecuted. psmux-power.ps1 takes a -SimulateState testing seam (no host read, no poke) and tests/Repo.Tests.ps1 asserts each colour and glyph threshold — including that a charging 15 % battery stays red, which is the case a naive "on AC → blue" reading would silently hide.
- FixThe package-freshness check now validates its own inputs — a wedged scoop bucket is a finding, not a silent green. A bucket is a git clone, and a stuck clone keeps serving manifests from whatever commit it froze at. Those stale versions still parse and still compare as matching, so the check reported "everything's current" on data months old — wrong in the reassuring direction, the worst way for a check to fail. That is not hypothetical: on 2026-08-04 the local extras clone had been stuck mid-merge on an upstream rename (UD bucket/pycharm.json) since mid-July, so scoop status called lazygit and tailscale "latest version" while the CI bot correctly had them behind. The box contradicted CI and the box was wrong. Check-PackageFreshness.ps1 now checks every bucket it reads manifests from — present, a real clone, not stuck on a merge/rebase/cherry-pick, clean tree — and writes a report even when nothing looks outdated, since that silent-green case is the entire point. The warning leads the issue body, because it invalidates every row under it. Also catches a bucket the scoop bucket add loop failed to create (its catch is empty), which today degrades quietly into a "no manifest version" skip for every app in it. Unit-tested via a new DOTFILES_PKGFRESH_LIBONLY hook, matching the *_LIBONLY idiom the sync scripts use. (packages/Check-PackageFreshness.ps1, tests/Packages.Tests.ps1)
- Fixdotfiles-doctor now checks scoop bucket health too, because CI structurally can't. The guard above lives in a script whose CI runs on a fresh runner, where buckets are added moments earlier and are always clean — so it protects the local-run path but can never observe the box this actually happened on. The wedge was a local condition that made the machine disagree with the bot for three weeks, and the doctor is where "is this box healthy" belongs. New Scoop buckets row under Health & toolchain: 6 bucket(s) clean and pullable when fine, and on a fault it names the bucket, says why (stuck mid-merge (MERGE_HEAD), dirty tree, missing directory, not a clone) and hints the exact unwedge. warn, not fail — nothing is broken and no tool is missing; the box just can't be trusted to tell you what's current. The detector is reused from packages/Check-PackageFreshness.ps1 through its DOTFILES_PKGFRESH_LIBONLY hook rather than reimplemented, so there's one definition of "this bucket can't be trusted"; the dependency deliberately only points this way, since the freshness bot must stay self-contained for CI, where the Dotfiles module isn't installed. The whole probe is wrapped so a bucket check can never take down a doctor run. (powershell/Dotfiles/Doctor.Helpers.ps1, powershell/os/45-doctor.ps1, tests/Doctor.Tests.ps1)
Fixed
- PerfThe load-budget perf test was measuring the runner, not the code. Perf.Tests.ps1's "dot-sources the tool-independent fragments quickly" timed a single cold dot-source, so it also charged the fragments for PowerShell's one-time parse/compile and module autoload — work they don't do. On a shared GitHub runner that noise is unbounded, and on 2026-08-05 it landed a CI run at 3012 ms against the 3000 ms budget: a 0.4 % overshoot on a body whose real cost is roughly 100× under the gate. A re-run passed untouched, which is the tell. A red CI that actually means "the runner was busy" is worse than no gate at all, because it teaches you to re-run instead of read. Now: one untimed warm-up, then the fastest of three timed runs. Noise only ever adds time, so the minimum is the closest estimate of true load cost — while the regression this exists to catch (a network or subprocess call added to a load path) is slow on every run and still trips it. The 3000 ms budget is deliberately unchanged; raising it would have hidden the flake instead of removing it. (tests/Perf.Tests.ps1)
- PerfThe VPN/IP pill never rendered — a PowerShell splatting bug. psmux-netinfo.ps1 poked the bar with psmux set -g @vpn_pill $text. In argument position a bare @name is PowerShell's splatting operator, so the undefined $vpn_pill expanded to nothing and the option name was dropped from the command line entirely; psmux received a single positional and silently discarded the whole command — exit 0, nothing on stderr, option never set. Every other layer (detection, cache file, timer, psmux.conf) was working, which is why it survived so long. Fixed by quoting '@vpn_pill' / '@vpn_fg'. (psmux/scripts/psmux-netinfo.ps1)
- ConfigA config reload repainted a live pill in the wrong colour. @vpn_fg was defaulted with a plain set -g, so every prefix + r overwrote whatever the refresher last poked. Because the pill's text is never defaulted, the two halves then disagreed until the next tick — up to a full refresh interval — and these pills encode their state in the colour: an active tunnel kept showing its address in the no-tunnel green, losing the orange that is the entire signal. Both colour options now use set -og (only-if-unset), which still guarantees a non-empty colour on first paint. Caught on review of the same mistake in @pwr_fg, where it would paint a 15 % battery healthy-green. psmux set -g @vpn_pill '', but an empty-string argument is dropped on the way to the exe and the set no-ops exactly like the splat above. Clearing now uses set -gu (unset). psmux-pill-disable clears the segment too, instead of only stopping the timer.
- FixHolding the prefix key shoved the IP pill two columns right. The prefix/mode indicator sits between #S and the pill with each branch padded to the same width — but the idle branch was three literal spaces, which psmux's parser collapsed to one (see the next entry), against a prefix branch of space + glyph + space that survived as three. The branches are now spaced with #{p<n>:} and are five rendered cells each, verified in both directions on a real terminal. A test asserts the three branches stay equal width, since eyeballing this is exactly what failed before.
- ConfigMulti-space gaps in the status bar were rendering as a single space. psmux parses option values as split_whitespace() + join(" "), so every run of spaces collapses to one, quoted or not — which means the twelve-space cwd→clock gap added in #163 had never actually widened anything. Bar gaps now use #{p<n>:}, which pads an empty body at render time — after the parser has had its way — and is the same idiom Core's tmux.conf already uses (#{p19:}), so the two configs now read the same. The session→IP gap is wider as a result, and a test forbids multi-space runs in status-left/status-right so this can't silently regress. Note #{p<n>:} only works written directly in the config: a format arriving via a user option is not re-expanded, which is the same rule that keeps a #[…] style run from working inside @vpn_pill.
- FixTwo stale psmux config tests. They asserted the pill was read via #(cmd /c type %LOCALAPPDATA%…) and passed only because that string still appeared in the comment block describing the retired transport — they had stopped testing anything real. Repointed at the live @vpn_pill / @pwr_pill segments, plus a static guard that every psmux set in the repo quotes its @option name, since the splatting bug above is invisible at runtime. (tests/Repo.Tests.ps1)
Core → Windows parity pass (2026-07). A focused sweep to close the drift that had built up between recent dotfiles-core / dotfiles-MacBook work and the Windows host, kicked off by a host :checkhealth dump. In short: the stale nvim/ mirror was re-vendored from Core (bringing the regex Tree-sitter parser and the new :checkhealth gerrrt LSP/formatter/linter readiness sections); the native-Windows clipboard false-warning was fixed upstream in Core and pulled in; the psmux :checkhealth tmux noise was documented as the cosmetic wart it is; and the two mid-2026 Core CLI tools the host still lacked were wired up — jnv (interactive JSON explorer) and a web terminal-browser verb (via lynx, since w3m has no scoop manifest). Status-bar redesign (2026-07/08). The parity pass then widened into a full bar rework across both surfaces the host draws — psmux (terminal) and Zebar (desktop) — with macOS sketchybar as the reference. Both converged on the same chip-less items on a transparent bar language, and PARITY.md was rewritten to describe the three-island layout the macOS bar had already drifted to (adopt, not revert), so the shared contract is true for both hosts again. Several entries below are live-testing fixes from actually running it. Per-change detail below.
Added
- Featurejnv — interactive JSON explorer (fleet parity with Core's HAVE_JNV). Added to packages/scoopfile.json (scoop Main). A jq-filter editor with a collapsible viewer that fills the "explore an unfamiliar JSON response" gap between jq (transform) and gron (grep). Its own command with no alias — jnv file.json or pipe into it — like jq/yq/ gron. This also retires the old jless-was-left-out caveat in docs/TOOLS.md: jnv is the packaged interactive explorer now.
- Configweb — terminal web browser verb (parity with Core's web). Added a guarded web function in core/00-aliases.ps1 that resolves w3m→lynx→links→elinks and runs the first present (skipped entirely when none is installed, matching Core). w3m has no scoop manifest, so the host packages lynx (Core's own next fallback; scoop Main) in scoopfile.json. Unlike Core's headless path, $BROWSER is never exported — the Windows host is GUI-first, so web stays an explicit opt-in verb.
- Feature**Command-block separators (parity with Core's _cmd_block_*). A thin full-width rule is drawn above each prompt that followed a command, colored by exit status — dim (#414868) on success, red (#f7768e) on failure — turning scrollback into scannable blocks. Ported as precmd/preexec (not** a key handler, so it can't collide with PSReadLine vi-mode): the AddToHistoryHandler sets $global:DotCmdBlockRan (a bare Enter never accepts a line, so no rule is drawn on an empty prompt), and Invoke-Starship-PreCommand draws the rule via [Console]::Write. Colour tracks $LASTEXITCODE (the status reliable at that point); pure-cmdlet failures still surface in starship's [status]. (powershell/core/10-tools.ps1)
- FeatureZebar caffeine / keep-awake indicator. A placeholder matching sketchybar's caffeinate.sh — grey asleep, yellow awake — in the left island. Visual only for now: it renders state but doesn't yet drive a keep-awake mechanism on the host (see the Caffeine component comment in the HTML). (desktop/zebar/vanilla-clear/)
- FeatureZebar battery shows an AC-power placeholder on desktops. A machine with no battery rendered nothing at all, leaving a gap in the right island; it now shows a green plug glyph. (desktop/zebar/vanilla-clear/)
Changed
- FeatureRe-vendored nvim/ from Core (was v4.4.0-3, now current). Brings the Core changes that had not yet reached the host: the regex Tree-sitter parser (silences :checkhealth noice's "regex parser is not installed" cmdline-highlighting warning), the new :checkhealth gerrrt LSP / formatter / linter readiness sections, and the servers/init.lua read-only status() export those sections consume. nvim/.core-ref updated to the synced commit. (nvim/, via nvim-sync.ps1)
- PerfRe-synced nvim/ to Core main a53ac4f — a follow-up mirror picking up the lazy-lock.json plugin-pin refresh (4 SHAs); nvim/.core-ref re-pointed from the pre-merge branch tip to main. starship.toml verified byte-identical to Core (no sync needed). (nvim/lazy-lock.json, nvim/.core-ref)
- ConfigWindows Terminal cursor → bar — cursorShape filledBox → bar to match MacBook's ghostty cursor-style = bar for cross-terminal parity. (windows-terminal/settings.json)
- Fixpsmux status bar → Core's centered floating-island look. Ported Core tmux.conf's island redesign to psmux/psmux.conf: a 2-line, centered, transparent bar (status 2 + blank status-format[1], status-justify centre — later absolute-centre, see Fixed — status-style bg=default, bg=default pill caps + pane borders) with flat underlined window tabs and monitor-activity • dots for unseen output (psmux has no monitor-bell) — replacing the old left-justified opaque-pill bar. All five psmux features were probed as supported (psmux 3.3.7) before porting. Stays within psmux's no-shell-out / no-process-table rules: the cwd pill keeps #{b:pane_path} (OSC 7) and Core's nvim-gated pane_current_path segment is intentionally not ported. (psmux/psmux.conf)
- PerfZebar adopts sketchybar's floating-islands design, and PARITY.md now describes it. The macOS bar had drifted to a 3-island look (transparent bar + bordered panels) without the shared contract being updated, so PARITY.md was false for one host. Resolved by adopting, not reverting: the bar goes transparent and .left/.center/.right each become a rounded island (rgba(29,32,47,0.93) fill, 2px rim, r=9) accented blue/magenta/ green. Weather moves into the left island (stable-width, non-urgent); two grey │ separators chunk the right island into I/O · load · power, each gated on its own group so a provider-startup transient can't leave a stray separator leading the island. PARITY.md (identical copy in dotfiles-MacBook) rewritten to match: three islands, weather left, transparent bar geometry, blur off, purple un-reserved and orange added to the palette. Not render-verified on a Windows host — reload Zebar and eyeball. (desktop/zebar/vanilla-clear/, desktop/PARITY.md)
- Fixpsmux bar is chip-less. Dropped the rounded pill caps (@cap_l/@cap_r) from the session / cwd / clock / IP segments in favour of plain coloured icon+text on the transparent bar — matching sketchybar, Zebar, and PARITY.md's "items are chip-less" spec. Also fixes the prefix and copy-mode glyphs being clipped by the cap they sat against. (psmux/psmux.conf, psmux/scripts/psmux-netinfo.ps1)
- ConfigDefault psmux session renamed main → Gerrrt — both the 30-windows.ps1 auto-launch and the mux verb default in 32-psmux.ps1, with the docs/comments that still said main updated to match. (psmux/, docs/TOOLS.md, TERMINAL_WORKFLOW_GUIDE.md)
- ConfigZebar weather reads °F instead of °C (fahrenheitTemp). (desktop/zebar/vanilla-clear/vanilla-clear.html)
- ConfigZebar workspace pills match sketchybar's aerospace.sh. Only the focused workspace is highlighted (blue background, dark text); every other one is a plain grey number with no chip — GlazeWM's .displayed distinction is deliberately dropped for macOS parity, since aerospace shows only the single focused workspace. (desktop/zebar/vanilla-clear/styles.css)
- ConfigZebar spacing and font tuning from live use on a large external monitor. --item-gap 20px → 8px to match sketchybar's per-item padding (padding_left 4 + padding_right 4); left-island items dropped from a 16px to an 8px margin so both islands read at the same density; --bar-font-size 16px → 18px, since sketchybar's ~17pt suits a laptop panel but reads too small on a large panel (still fits the 28px island; 20px is the next comfortable step). (desktop/zebar/vanilla-clear/styles.css)
Fixed
- Fixpsmux tabs no longer drift off-center — status-justify absolute-centre. Plain centre (Core's value) centers the window list in the gap between status-left and status-right, so the host's variable-width session pill (wider while prefix is active) and the #{b:pane_path} cwd in status-right pushed the tabs off the true middle — most visibly as a jump when a pane running nvim widened the right float. Switched to absolute-centre, which anchors the tabs to the bar's absolute center regardless of either float's width. Deliberate divergence from Core's centre (see docs/PORTING-NOTES.md); probed on psmux 3.3.7. This is the real fix for the tab-shifting the equal-width prefix cell below was working around. (psmux/psmux.conf)
- Perfpsmux IP / VPN pill rendered blank. Two distinct causes, found in that order. First, psmux.conf ran set -gq @vpn_pill "", which clobbered the refresher's poked value on every source-file reload — removed, so #{@vpn_pill} persists what the refresher sets. The segment still rendered empty, because the option was poked as a single pre-styled string ('#[fg=#9ece6a,bold]<glyph> <ip>') and an option value embedding a #[…] style run is not re-interpreted when the format expands it. That's why psmux-pill-status showed a populated cache (a plain file write, a separate code path) while the bar stayed empty. Split the transport to mirror the proven @tn_* colour pattern: @vpn_pill carries plain text only, @vpn_fg carries the accent hex, applied in status-left as #[fg=#{@vpn_fg}]#{@vpn_pill}. Only the colour is defaulted in the conf (so it's never empty on first paint); defaulting the text is what caused the original clobber. (psmux/psmux.conf, psmux/scripts/psmux-netinfo.ps1)
- Configpsmux prefix indicator style leaked into the window tabs. A #[default] reset after #{@vpn_pill} stops the pill's bold/fg bleeding into the tabs. The indicator was also widened to an equal-width padded cell ( / idle ) so its branches couldn't shift the tabs — kept for stable width, though absolute-centre above is what actually holds the tabs still. (psmux/psmux.conf)
- Fixpsmux nvim cwd jammed against the clock. Two passes: the status-right cwd↔clock gap sat inside a #{?} branch and psmux trims in-branch trailing spaces, so it was moved outside the branch — then widened (6 → 12 spaces) once the branch fix made the gap actually render and it was still too tight to read. (psmux/psmux.conf)
- Configpsmux config warning: unknown option 'monitor-bell'. psmux 3.3.7 doesn't implement monitor-bell at all — its CLI setw/set returns exit 0 (so the capability probe was a false positive) but the config parser rejects it on load. Removed the setting; monitor-activity (which psmux does support) stays, so activity dots still work — only the bell dot is inert. (psmux/psmux.conf)
- FeatureZebar network readout rendered white instead of blue. The .network module had no color class, so only its glyph got the global blue while the ↓↑ throughput values fell back to fg (white). Added .network { color: var(--tn-blue) } so icon and values are blue, matching sketchybar's network.sh (icon + label accent). (desktop/zebar/vanilla-clear/styles.css)
- Perf:checkhealth gerrrt no longer false-warns about the clipboard on the host. It read "Core's cross-OS clipboard scripts are not on PATH (clip: found, clip-paste: missing)" — misleading, since clip only resolved to Windows' built-in clip.exe and the Unix/WSL clip/clip-paste ladder does not apply on the host: config/clipboard.lua wires the clip-windows provider (clip.exe copy + PowerShell paste) instead. Fixed upstream in Core (health.lua now detects native Windows via has("win32") and defers to :checkhealth vim.provider for the live backend) and pulled in with the nvim re-vendor above.
Docs
- ConfigDocumented the psmux :checkhealth tmux cosmetic wart in docs/PORTING-NOTES.md: psmux has no show-option verb, so Neovim's built-in vim.health tmux probe shows ❌ ERRORs and a false "true color could not be detected" ⚠️ — cosmetic only (psmux renders 24-bit colour natively; nothing functional is affected), and not shimmed on purpose.
- FixRefreshed the stale "re-vendor nvim/" manual step (the full tree is now vendored via nvim-sync.ps1, and the old <leader>rc keymap wart is fixed upstream), and dropped the now-inaccurate "Known Windows wart" banner nvim-sync.ps1 printed after each sync.
Changed
- FixDotfiles.psd1 Author is now dotgibson, not the Gerrrt personal account. The repos moved to the org, but the module manifest still presented the personal account as the owner — the last spot in the fleet doing so. Metadata only: nothing resolves this field, so it's a naming/identity fix rather than a functional one. The remaining Gerrrt references are all correct and deliberately untouched — the nvim/lua/gerrrt/ namespace and Gerrrt* highlight groups (internal identifiers, not paths), historical CHANGELOG entries recording the migration itself, and attribution to Gerrrt/make-windows-pretty / Gerrrt/yasb-glazewm-config, which are genuinely external upstreams still living on that account.
Fixed
- FixCheck-PackageFreshness.ps1 no longer reports padded version strings as updates. The lock is captured from winget export, which pads versions to four components, while winget show reports the source's own form — so 2.7.10.0 in the lock and 2.7.10 upstream are one build written two ways. The check compared them as raw strings, so three of the four packages in its 2026-07-21 report (Microsoft.WSL, QL-Win.QuickLook, CharlesMilette.TranslucentTB) were flagged as behind when they were current — and would have been flagged again every week, since re-pinning cannot fix a difference that isn't real. New pure helper Test-PackageVersionMatch in PackageLock.ps1 compares component-wise with absent trailing components read as 0, and falls back to exact string equality when either side isn't purely numeric-dotted (prereleases, scoop's date+hash strings, nightly), where there's no safe numeric reading. Applied to both the scoop and winget comparison sites. Unit-tested offline in tests/Packages.Tests.ps1.
Security
- FixEngagement-data write guard. note, logshell, bhce and nmapsweep used to fall back to $PWD when $ENGAGEMENT was unset, so running them inside a checkout wrote client data into that repo. They now resolve their root through _eng_writeroot, which refuses any $PWD inside a git work tree.
- SecurityThe field references open read-only. htp/xdev/evade/ipp are symlinks to tracked files, and hacktheplanet's "target fill" recipe told you to substitute the real client IP/hostname/domain into the buffer — one :w from publishing engagement data. They now open with -R; htp -w edits deliberately, and the fill recipe writes a copy under $ENGAGEMENT.
- Security.gitignore backstop repaired. *.xml carried a trailing comment, which gitignore does not support — the pattern was the whole line and matched nothing, leaving nmap -oX output unguarded. The ignore list also described the *template's* directory names rather than the ones mkengagement creates, so scope/, recon/, scans/, web/, screenshots/, exploit/ and notes.md were all unblocked.
- SecurityPinned + verified tool installs. The five curl | sh installers are gone. install/tool-versions.env pins each tool's version and the SHA-256 of its release asset; bootstrap.sh verifies before installing and fails closed. starship moved to apt, which packages it.
- SecuritySecret scanning in CI — gitleaks over the working tree and full history.
- Securityhethttp refuses to serve a git work tree on 0.0.0.0.
- Securitybhce can take credentials off argv — op://… resolves through 1Password, - prompts with echo off.
Fixed
- Fixdoggo, carapace and sesh never installed on a fresh box. mise lands in ~/.local/bin, which is not on PATH during bootstrap, so the go install fallback's command -v mise always missed. A PATH prelude fixes this and the related re-install-every-run behaviour of atuin.
- FixA symlink cycle in the .zshrc wiring. bootstrap.sh re-did a link the library already makes, bypassing the ELOOP guard in _blib_seed_zdotdir_rc.
- Fixbootstrap.sh no longer silently installs nothing when install/packages.txt is missing.
- Fixapt_install's per-package retry keeps --no-install-recommends.
- FixThe bootstrap workflow's path filter omitted install/ and wsl/, so package-list edits never re-ran the bootstrap test. Filters removed.
- Fixdotsync hardcoded ~/dotfiles-Kali; it now resolves this checkout.
- ConfigThe offensive tmux binding shipped even when its script was not linked, and hardcoded ~/.config against an XDG-aware bootstrap.
- Fix@batt_enable was unconditionally off "because WSL has no battery" — now detected, so bare-metal laptops keep the widget.
- Configssh/config pinned modern-only crypto on Host *, which refuses to negotiate with the legacy targets an offensive box exists to reach. Scoped to your own infrastructure.
- Configpseudo-shell.py proxied through Burp by default, so every request failed opaquely when Burp was not running; now opt-in. Its requests dependency documents a PEP 668-compatible install path.
- Fixredup printed "go not installed" for an intentionally empty tool list, and ran searchsploit -u without the privilege its root-owned checkout needs.
Added
- FeatureMakefile — the entry point (make lint, test, core-sync, packages-check, …). Makes core.lock's make core-lock instruction true for the first time.
- Featurescripts/sync-core.sh, test/check-core-freshness.sh and a freshness workflow — the consumer-side core-sync line, which three files already referenced and none provided.
- Featuretest/check-companion-integrity.sh — tamper detection for the second vendored subtree, mirroring core-integrity.
- Configtest/check-packages.sh + a packages workflow resolving every manifest name against kali-rolling.
- Featuremarkdownlint in CI, against the .markdownlint.jsonc that had been sitting unused.
- SecuritySECURITY.md, CODEOWNERS, issue and PR templates, CONTRIBUTING.md, .shellcheckrc, .editorconfig, .gitattributes.
- Featurebootstrap.sh --dry-run and --no-upgrade.
- Featurecompanion_version / companion_tag in companion.lock, for symmetry with core.lock.
Changed
- ConfigThe gating workflows (lint, bootstrap, companion, routine-filter) no longer use trigger-level path filters: a paths:-skipped workflow produces no check run, so requiring one would hang every non-matching PR.
- Configos/kali.gitconfig no longer duplicates Core's init.defaultBranch, and os/kali.zsh no longer duplicates Core's ~/.local/bin PATH prepend.
- Configoffensive/templates/engagement.md documents the layout mkengagement actually creates.
Fixed
- Fixbootstrap.sh no longer fails on a machine without sudo. The escalator is now resolved once (BLIB_SU: empty as root, else sudo, else doas) and used everywhere, instead of a hard-coded sudo at a dozen call sites. A container, a WSL first boot, or a minimal Server image previously died at the first dnf line with sudo: command not found (exit 127) before doing anything.
- Fixbootstrap.sh can no longer stall on an invisible password prompt. The sudo timestamp is primed up front and refreshed in the background for the life of the run, and privileged calls no longer discard stderr. Previously, calls placed after the multi-minute cargo/go builds outlived the 5-minute timestamp and blocked on a prompt written to /dev/null — indistinguishable from a hang.
- FeatureRe-running bootstrap.sh no longer rebuilds the Rust/Go tools from source. The presence guards probed PATH, but ~/.cargo/bin and ~/.local/bin are only added by os/fedora.zsh — i.e. only inside a Core *zsh* — so a run from bash rebuilt all six crates plus yazi every time. provision() now puts both bindirs on PATH first.
- FixA failed step is now reported. Best-effort failures are collected and printed as a closing summary instead of being swallowed, so a box missing carapace, op, lazygit and every cargo tool no longer reports bootstrap complete. --strict exits non-zero.
- Fix/etc/wsl.conf is backed up before it is overwritten (.pre-dotfiles.<epoch>, matching every other managed file). It was the one destructive write with no backup.
- Fix**OS detection no longer matches Fedora-*like* distros by accident.** ID=/ID_LIKE= are parsed as keys; the old grep -qi fedora /etc/os-release also matched Rocky, Alma, CentOS Stream, Nobara, and any incidental substring such as a URL. Fedora-like distros are now an explicit --force-os opt-in.
- Fix--help no longer drifts. It was sed -n '2,17p' "$0", coupled to the header's line numbers — the exact trap core/scripts/sync-core.sh documents. It is a heredoc now.
- Fix.gitignore no longer ignores the tracked core/.claude/ files. The .claude/ pattern was unanchored, so it matched at any depth — a hazard for a vendored tree whose git tree SHA must match core.lock.
Added
- Featurebootstrap.sh --dry-run — previews the whole plan (packages *and* the symlink graph) and changes nothing, via the shared lib's BLIB_DRY; prints the wiring tally.
- Featurebootstrap.sh --strict and --force-os; a preflight that checks for the commands the script assumes and fails once with the full list.
- Fixbootstrap.sh now installs the core/ pre-commit guard on a fresh clone (blib_install_core_guard), which the shared lib always intended but was never called.
- Feature1Password's signing key is fingerprint-verified before rpm --import; a mismatch fails closed. The three upstream install scripts are downloaded, sanity-checked, then run — never curl | sh — and starship installs to ~/.local/bin, needing no root.
- SecurityRoot repo scaffolding that GitHub can actually see (it previously existed only under core/, where GitHub ignores it): CONTRIBUTING.md, SECURITY.md, CODEOWNERS, PR and issue templates, .editorconfig, .shellcheckrc, .gitattributes, .pre-commit-config.yaml, this changelog, and a thin Makefile (make lint / check / dry-run / integrity / hooks).
- Featurepackages workflow — resolves every name in install/packages.txt against a matrix of supported Fedora releases, replacing hand-maintained availability prose with a check.
Removed
- ConfigA stale 4.5 MB orphaned worktree copy under .claude/worktrees/, and the obsolete zsh/local.zsh ignore entry (host overrides have lived at ~/.config/zsh/99-local.zsh since v4).
Added
- Featurebootstrap.sh --dry-run — previews the entire run (package plan + symlink plan + /etc/wsl.conf handling) and changes nothing. The shared library has supported BLIB_DRY end-to-end all along; this layer simply never exposed it.
- FeatureRoot Makefile — lint, bootstrap-dry, packages-check, secrets, core-lock, core-verify. lint reproduces the CI gate exactly, so a failure is visible before pushing. This also makes core.lock's own header instruction (“Regenerate … with: make core-lock”) true for the first time.
- Configpackages workflow — resolves every install/packages.txt name against the Arch repos on PR and weekly, without installing. Nothing previously checked the package list, on a rolling release where renames are routine.
- FeatureRoot .gitattributes, .editorconfig, .shellcheckrc — Core ships all three, but EditorConfig/shellcheck/gitattributes resolution is directory-scoped, so they governed core/** only and this repo's own files had no policy.
- SecurityCODEOWNERS, pull_request_template.md, SECURITY.md, and this file.
Fixed
- Fixbootstrap.sh could exit 0 having installed nothing. blib_read_pkgs' exit status is lost inside the < <(…) process substitution, so a missing or empty install/packages.txt produced an empty array, a failed pacman -S, a zero-iteration fallback loop, and a success message. It now refuses to continue.
- Configbootstrap.sh silently swallowed per-package install failures. The fallback loop discarded every error, so a handful of renamed packages yielded a green run and a half-provisioned machine. Failures are now collected, reported at the end, and produce a non-zero exit — after wiring completes, so the box is still usable.
- Fixbootstrap.sh clobbered an existing /etc/wsl.conf. Every other mutation in this system backs up first (blib_link → .pre-dotfiles.<epoch>); this one overwrote, losing any local [automount] / [boot] / [network] settings on a re-run of a script documented as idempotent. It now no-ops when already correct and backs up otherwise.
- Fixpacorphans passed all orphans to pacman as a single argument. zsh does not word-split unquoted parameters, so pacman -Rns $orphans handed over one newline-joined string; now ${(f)orphans}. zsh -n cannot catch this — the syntax is valid.
- Fix--help was coupled to the file's header line numbers (sed -n '2,17p' "$0"), so editing the banner silently drifted the help text. Replaced with a usage() heredoc, matching the fix core/scripts/sync-core.sh already documents.
- SecurityStale .gitignore entry zsh/local.zsh — a pre-v4 path that does not exist in this repo. Added credential, .envrc/.direnv and key-material patterns (direnv is installed by packages.txt and hooked into every shell).
Changed
- ConfigPrivilege escalation goes through the library's _blib_priv, honouring BLIB_SU, instead of a hardcoded sudo. This makes bootstrap.sh work as root and on doas-only boxes — and makes provision() runnable in a container, which Arch base images (no sudo) previously prevented.
- FixArch derivatives are accepted with a warning rather than refused: the guard now falls back to ID_LIKE=…arch…, so EndeavourOS/Manjaro/CachyOS work.
- Configgo install for sesh logs its errors to a file instead of /dev/null, and the module version is overridable via SESH_VERSION (still defaulting to latest — see the note in provision()). carapace is a printed paru hint and takes no version override, per the analysis in #89: go install cannot work for any version of it. That error logging is what makes such a failure visible in the first place — the old /dev/null form is precisely why the carapace call could fail on every bootstrap without anyone noticing.
- ConfigA failed run now says where it failed (ERR trap), and a successful one prints the wiring tally and points at core-doctor.
- Fixbootstrap.sh installs the local core/ pre-commit guard on a fresh clone (blib_install_core_guard), catching a hand-edit at commit time rather than waiting for core-integrity.yml at PR time.
No changes match this filter.
Only repos with a CHANGELOG.md appear here. The distro layers track Core via git subtree and don’t keep their own yet — they’ll show up automatically once they do.