Distro Porting Matrix
How to stamp dotfiles-Arch, dotfiles-openSUSE, dotfiles-Alpine,
dotfiles-Gentoo, and dotfiles-Debian from the dotfiles-Fedora template. The structure is identical
every time — only three things change per distro: package manager commands,
package names, and distro quirks. Core never changes (it’s vendored).
Offense (Kali) and macOS appear in the reference tables below for convenience, and NixOS
in the command table, but they’re their own lineages — built directly, not stamped from
this template (see Repo status at the bottom).
Per-repo recipe
cp -r dotfiles-Fedora dotfiles-<Distro>- Rename
os/fedora.zsh→os/<distro>.zsh; swap clipboard + pkg-manager aliases. - Replace
install/packages.txtwith that distro’s names (table below). - In
bootstrap.sh: swap thednfblock for the distro’s installer and the/etc/os-releaseguard string. - Commit steps 2–4 first (
git add -A && git commit -m "os: the <Distro> layer"): the sync refuses a target with uncommitted changes, so an uncommitted repo is skipped rather than re-vendored. Then re-vendor Core and stampcore.lock, from a Core checkout — in a throwaway worktree, so your own checkout stays on its branch for the registration edit that follows:git fetch origin refs/tags/v7 && wt="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-core-sync.XXXXXX")/core" && { git worktree add --detach "$wt" FETCH_HEAD || { rmdir "$(dirname "$wt")"; false; }; } && { out="$( (cd "$wt" && CORE_BRANCH="$(git rev-parse 'HEAD^{commit}')" CORE_COLOR=never REPOS_ROOT="$OLDPWD/.." ./scripts/sync-core.sh dotfiles-<Distro>) 2>&1)" || true; printf '%s\n' "$out"; last="$(awk '/^ *repos: /{l=$0} END{print l}' <<<"$out")"; if grep -Eq '^ *repos: +updated 1 +skipped 0 +failed 0 +\(of 1 targeted\)$' <<<"$last"; then rc=0; else rc=1; fi; git worktree remove --force "$wt" && rmdir "$(dirname "$wt")" && (exit "$rc"); }(a unique temp path, so a retry never meets a registered leftover; the sync’s output is captured under|| truesoset -ecannot skip the cleanup, which runs only once the add succeeded; the RELEASED script runs in that worktree — it exits 0 after a per-repo failure and may predate--strict— so its summary line is the verdict:updated 1 skipped 0 failed 0for the one target, and nothing else counts; thatrepos:footer exists since v4.1.0, so an older exact freeze cannot be judged this way — and the binding floor is v4.15.1 anyway, the first release whose sync MATERIALIZES rather thangit subtree pull --squash, which cannot update a materializedcore/at all) — a released tag, nevermain, and the peeled commit, neverrefs/tags/v7(the tags are annotated; seeRELEASE-STRATEGY.md§“Safe deployment”;VENDORING.md§ “One-time setup” has the same four commands on separate lines). Step 1 already copied Fedora’score/across, so there is nogit subtree addto run here (it would fail: prefix ‘core’ already exists). That manual add is only for a repo with nocore/at all — one scaffolded some other way, or byscripts/new-os-repo.sh --no-vendor, which must be committed first (subtree addneeds a cleanHEAD; the recovery command the scaffold prints does that); a normal scaffold run materializes the filtered vendor set instead (no subtree). Skip this step andcore-integrityreports the inherited tree against Fedora’s lock. - Update the README’s “specifics” section to that distro’s quirks.
Package-manager commands
This table is rendered from os/<os>.capabilities (#686). It began life the other way
round — as the transcription source those declarations were authored from (#667) — and
once seven repos declared (the two Role repos have no OS band and inherit the OS layer’s
table) the declarations became the live files, held to the schema by audit-core.sh §9c,
while this copy was held to nothing. So the arrow flipped: scripts/gen-porting-matrix.sh
now writes every cell below from the declared PKG_* value, verbatim, plus a placeholder,
and make audit (§9h) fails when they differ. Edit the declaration in the OS repo, then
run make gen-porting-matrix. The declarations’ own headers still say “Transcribed from
core/PORTING-MATRIX.md” — true of how they were written, no longer of which side is
edited; issue #837 updates them. Three columns render more than one declaration file,
because a declaration is data and cannot probe and the repo’s bootstrap.sh relinks the
right one at run time: openSUSE’s dup-vs-up split plus its transactional edition
(three labels), and Fedora’s atomic edition beside Workstation (two). Labelled cells
collapse to one when every declaration agrees, so a column only looks split where it
genuinely is. Debian’s Kali lane is the other shape — a second declaration given its
own column rather than a label, because it is a different distro rather than a
different edition of one. The table gained its macOS and Fedora columns in #664: they had been
missing since it was written, even though dotfiles-MacBook is the reference
implementation and dotfiles-Fedora is the template the other Linux repos stamp from —
so the two most-copied repos had nothing to copy.
The count-pending row is what up’s once-a-day nudge runs, and it is the most
divergent of the lot — the one Core hardcoded worst. It must be non-root and must not
mutate the system, which is why Arch counts with checkupdates (a user-space copy of
the sync DB, never the real one) rather than -Sy — and why Gentoo, which cannot answer
it cheaply at all, pays for a real resolve rather than the fast wrong answer.
It is also the one row where a cell may not be a verb at all, because two families cannot
answer the question as asked. A staged host (Fedora’s atomic edition) reads
`<verb>` (staged?): it cannot cheaply say how many packages are pending — that verb
is root-only there — but it can say whether a change is already staged, which is what the
nudge actually runs, so the cell shows the verb it has and names the question it answers.
A declarative host (NixOS) reads —: packages-pending is not a thing it knows at
all, and the nearest question needs root and lists derivations rather than packages. Both
absences are legal — scripts/check-capabilities.sh permits PKG_COUNT_PENDING to be
missing in exactly those two cases — and the generator renders a cell only where the
validator would accept the absence. Anywhere else a missing verb is still a hard failure,
which is what keeps this table honest for the eight mutable declarations.
| Action | macOS (brew) | Fedora (dnf) | Arch | openSUSE | Alpine | Gentoo | NixOS | Kali (apt) | Debian/Ubuntu (apt) |
|---|---|---|---|---|---|---|---|---|---|
| refresh | brew update | Workstation: sudo dnf check-update · Atomic: sudo rpm-ostree refresh-md³⁵ | sudo pacman -Sy²³ | sudo zypper refresh | doas apk update | sudo emerge --sync | sudo nix-channel --update | sudo apt-get update | sudo apt-get update |
| upgrade | brew upgrade | Workstation: sudo dnf upgrade --refresh · Atomic: sudo rpm-ostree upgrade | sudo pacman -Syu | Leap: sudo zypper up · Tumbleweed: sudo zypper dup · Transactional: sudo transactional-update dup | doas apk upgrade | sudo emerge -auvDN @world | sudo nixos-rebuild switch --upgrade | sudo apt-get full-upgrade | sudo apt-get full-upgrade |
| count-pending | brew outdated --quiet³⁶ | Workstation: dnf -q --refresh check-update · Atomic: rpm-ostree status --pending-exit-77 (staged?) | checkupdates²³ | zypper -q list-updates | apk list -u | gentoo-pkg-pending³⁷ | — | apt-get -s upgrade | apt-get -s upgrade |
| install | brew install <pkg> | Workstation: sudo dnf install -y <pkg> · Atomic: sudo rpm-ostree install --idempotent <pkg> | sudo pacman -S --noconfirm <pkg> | Leap: sudo zypper in <pkg> · Tumbleweed: sudo zypper in <pkg> · Transactional: sudo transactional-update -n pkg in <pkg> | doas apk add <pkg> | sudo emerge <atom> | nix-env -i <pkg> | sudo apt-get install -y <pkg> | sudo apt-get install -y <pkg> |
| remove | brew uninstall <pkg> | Workstation: sudo dnf remove -y <pkg> · Atomic: sudo rpm-ostree uninstall <pkg> | sudo pacman -Rns --noconfirm <pkg> | Leap: sudo zypper rm <pkg> · Tumbleweed: sudo zypper rm <pkg> · Transactional: sudo transactional-update -n pkg rm <pkg> | doas apk del <pkg> | sudo emerge --depclean <atom> | nix-env -e <pkg> | sudo apt-get remove -y <pkg> | sudo apt-get remove -y <pkg> |
| search | brew search <term> | dnf search <term> | pacman -Ss <term> | zypper se <term> | apk search <term> | emerge -s <term> | nix --extra-experimental-features nix-command search nixpkgs <term> | apt-cache search <term> | apt-cache search <term> |
| owns-file | brew which-formula <path>³⁸ | Workstation: dnf provides <path> · Atomic: rpm -qf <path> | pacman -Qo <path> | zypper se --provides <path> | apk info --who-owns <path> | equery belongs <path> | nix-locate --top-level <path> | dpkg -S <path> | dpkg -S <path> |
Package names (modern CLI stack)
The Kali column changed owner. It described
dotfiles-Offensewhile that repo carried the Kali OS band. It no longer does —dotfiles-Offenseshed its OS-native layer entirely, and the lane moved todotfiles-Debian’sonly:kali/skip:kalitiers. The seven cells the move demonstrably falsified are corrected (lazygit,starship,atuinin the first round;git-delta,difftastic,mise,uvin the second), and so are the two footnotes that asserted “Kali installs nothing” — but the column as a whole has not been re-derived cell-by-cell against the new owner. Treat unmarked Kali cells as inherited rather than verified until/os-package-availabilityhas run againstdotfiles-Debian’s kali tier. (²¹ᵃ marks this caveat, not a per-cell claim.)The second round is what this caveat predicted, and it arrived by the predicted route. All four were resolved against
dotfiles-Debianrather than inherited:git-deltais# skip:kalibecause it is in apt on noble and trixie and absent from kali-rolling;difftasticnever made theonly:kalitier at all; andmiseanduvare fetched byverified_installon every target, so their apt availability —uvgenuinely is in kali-rolling — was never what the cell should have recorded. A cell here documents what this fleet installs, not what the archive contains. That distinction is what madeuvwrong while looking right, and it is the one to apply to the cells still unmarked.
This table is rendered by scripts/gen-porting-matrix.sh (#686), and each cell is one
of two kinds. A cell naming a package the repo installs is derived from that repo’s
install/packages.txt — the line’s name, verbatim, with a # min:X.Y.Z floor shown
as ≥ X.Y.Z; the Kali and Debian/Ubuntu columns read dotfiles-Debian’s one list through
its own scripts/pkg-filter.sh tiers, so only:kali and skip:kali land exactly where
bootstrap.sh would put them — and the Debian/Ubuntu cell is read under both IDs and
refused when they disagree, so a skip:ubuntu line can never render as shared. Every other cell — the ²¹ “available, not installed” names
and the asset / cargo / AUR / GURU routes only bootstrap.sh knows — is asserted
in the script’s PKG_ROWS registry and rendered verbatim. The gate catches exactly one
transition on that half — a repo starting to install an asserted tool through
packages.txt fails until the cell is flipped to =; a bootstrap route that changes or
disappears out-of-band is still a judgment the footnotes and /os-package-availability
carry. scripts/gen-porting-matrix.sh --list prints each cell’s provenance.
Fix a derived cell in the OS repo’s packages.txt, an asserted cell in the registry,
and a footnote here. The footnotes below stay hand-written.
| Tool | Arch | openSUSE | Alpine | Gentoo (atom) | Kali (apt)²¹ᵃ | Debian/Ubuntu |
|---|---|---|---|---|---|---|
| eza³⁹ | eza | eza | eza | sys-apps/eza | eza | eza |
| bat | bat | bat | bat | sys-apps/bat | bat⁴ | bat⁴ |
| fd | fd | fd | fd | sys-apps/fd | fd-find⁴ | fd-find⁴ |
| ripgrep | ripgrep | ripgrep | ripgrep | sys-apps/ripgrep | ripgrep | ripgrep |
| zoxide | zoxide | zoxide | zoxide | app-shells/zoxide | zoxide | zoxide |
| fzf | fzf | fzf | fzf | app-shells/fzf | fzf | fzf |
| git-delta | git-delta | git-delta | delta | dev-util/git-delta | asset²⁸ | git-delta |
| btop | btop | btop | btop | sys-process/btop | btop | btop |
| tldr | tealdeer | tealdeer¹ | cargo³ | app-misc/tealdeer¹² | tealdeer | tealdeer |
| neovim³³ | neovim | neovim ≥ 0.12.0 | neovim ≥ 0.12.0 | app-editors/neovim ≥ 0.12.0 | neovim ≥ 0.12.0 | asset²⁸ |
| lazygit | lazygit | lazygit | lazygit | dev-vcs/lazygit¹² | lazygit | asset²⁸ |
| zsh | zsh | zsh | zsh² | app-shells/zsh | zsh | zsh |
| tmux | tmux | tmux | tmux | app-misc/tmux | tmux | tmux |
| starship | starship | starship¹⁸ | starship | app-shells/starship | starship | asset²⁸ |
| atuin²⁰ | atuin | atuin¹⁸ | atuin | app-shells/atuin | asset²⁸ | asset²⁸ |
| mise³⁰ | mise | script³⁰ | script³⁰ | script³⁰ | asset²⁸ | asset²⁸ |
| direnv³² | direnv | direnv | direnv | app-shells/direnv¹² | direnv | direnv |
| yazi | yazi | yazi¹⁸ | yazi | app-misc/yazi¹² | cargo²¹ | —²⁹ |
| tree-sitter-cli⁵ | tree-sitter-cli | tree-sitter | tree-sitter-cli ≥ 0.26.1 | dev-util/tree-sitter-cli ≥ 0.26.1 | tree-sitter-cli ≥ 0.26.1 | asset²⁸ |
| jq³⁴ | jq | jq | jq | app-misc/jq | jq | jq |
| yq⁶ | go-yq | yq | yq-go | app-misc/yq-go | yq-go | go³ |
| duf | duf | duf | testing¹⁴ | sys-fs/duf | duf | duf |
| dust | dust | dust | dust | sys-block/dust | du-dust⁴ | asset²⁸ |
| procs | procs | procs | procs | sys-process/procs | procs | asset²⁸ |
| viddy¹⁶ | AUR¹⁶ | viddy¹⁸ | viddy | cargo³ | cargo²¹ | —²⁹ |
| sd²² | sd | sd | sd | sys-apps/sd¹² | sd | sd |
| gron | gron | gron | gron | go³ | gron | gron |
| jnv¹⁷ | jnv | cargo | cargo³ | cargo | cargo | —²⁹ |
| jc⁴⁰ | jc | jc⁴⁰ | jc | dev-python/jc⁴⁰ | jc | jc |
| lnav²¹ ²⁴ | lnav | lnav | lnav | app-admin/lnav²⁴ | lnav²⁴ | lnav |
| glow | glow | glow | testing¹⁴ | app-misc/glow¹² | glow¹⁵ | charm apt |
| gum | gum | gum | gum | mise³⁰ | gum¹⁵ | charm apt |
| xh | xh | xh | xh | net-misc/xh¹² | xh | asset²⁸ |
| doggo | doggo | doggo¹⁸ | doggo | net-dns/doggo | go³ | go³ |
| gping¹⁹ | gping | gping¹⁹ | gping | GURU¹⁹ | gping¹⁹ | gping |
| carapace | AUR²⁷ | rpm²⁷ | carapace | app-shells/carapace¹² | deb²⁷ | deb²⁷ |
| op (1Password)¹³ | AUR | vendor rpm | vendor apk | GURU¹² | vendor apt | vendor apt |
| hyperfine²¹ | hyperfine | hyperfine | hyperfine | app-benchmarks/hyperfine | hyperfine | hyperfine |
| watchexec²¹ ²⁵ | watchexec | watchexec | watchexec | cargo²⁵ | cargo²⁵ | —²⁹ |
| shellcheck²¹ | shellcheck | ShellCheck | shellcheck | dev-util/shellcheck-bin | shellcheck | shellcheck |
| shfmt⁷ ²¹ | shfmt | shfmt | shfmt | go²¹ | shfmt⁷ | shfmt |
| ouch²¹ | ouch | ouch¹⁸ | testing¹⁴ | GURU¹² ²¹ | cargo²¹ | —²⁹ |
| jujutsu (jj)⁸ | jujutsu | jujutsu | jujutsu | dev-vcs/jj²¹ | cargo²¹ | —²⁹ |
| sesh⁹ | go⁹ | go⁹ | go⁹ | go⁹ | go⁹ | go³ |
| difftastic¹⁰ | difftastic | difftastic | difftastic | dev-util/difftastic | asset²⁸ | asset²⁸ |
| git-absorb²¹ ²⁶ | git-absorb | git-absorb | git-absorb | dev-vcs/git-absorb | git-absorb | git-absorb |
| ast-grep¹¹ | ast-grep | ast-grep¹⁸ | ast-grep | cargo²¹ | cargo²¹ | —²⁹ |
| uv³⁰ | uv | python3-uv²¹ | uv | dev-python/uv | asset²⁸ | asset²⁸ |
| w3m | w3m | w3m | w3m | www-client/w3m | w3m | w3m |
¹ openSUSE: in Tumbleweed main OSS as tealdeer (1.8.0). Not in Leap 16.0 or 16.1 —
it shipped in 15.6, which is EOL, and the name is absent from both arches of each 16.x
repo/oss (checked 2026-08-21). Two ways in on Leap 16.0: the utilities OBS repo,
which does build it (.../repositories/utilities/16.0/x86_64/tealdeer-1.8.0-lp160.49.8.x86_64.rpm)
but is a third-party channel dotfiles-openSUSE deliberately does not auto-add, same call as
Packman; or cargo install --locked tealdeer, which its bootstrap.sh now runs
presence-guarded. On Leap 16.1 there is only the second — utilities builds tealdeer for
16.0 only, its index listing 16.0/ plus three Factory flavors and no 16.1/ at all
(checked 2026-08-22, dotfiles-openSUSE#113). The retry hint in that bootstrap used to offer
the 16.0 repo unconditionally, which on a 16.1 box adds a 16.0 channel.
Guard on tldr, not tealdeer — the crate and the binary it installs
have different names (dotfiles-openSUSE#102).
² Alpine default shell is ash; you must apk add zsh explicitly.
³ Not packaged or stale → bootstrap.sh installs it best-effort (upstream
installer / cargo install / go install / AUR), the same pattern bootstrap
already uses on Fedora. Add cargo/rust (or a go toolchain) to packages.
go install targets land in ~/.local/bin via GOBIN so they’re on PATH.
carapace is the one documented exception to the go install half of this — that
module can never be go installed, on any platform, so its cells point at ²⁷ instead.
The module path is rarely the repo URL — see ³¹ for the exact one per tool.
⁴ Debian/Kali ship these under different binary names — bat runs as batcat,
the fd-find package installs fdfind, and the du-dust package installs the
dust command. Core’s 00-tools.zsh already resolves them, so aliases and config
work unchanged. 20-aliases.zsh additionally aliases bat/fd back to their
canonical names, so both are typeable as documented upstream, and core-doctor
probes the RESOLVED binary — it reports ✓ for a renamed tool rather than the ✗
that once contradicted the resolved line in the same report.
⁵ nvim-treesitter (pinned to main by dotfiles-nvim, which owns the editor — see ³³)
needs tree-sitter-cli ≥ 0.26.1. Fleet position,
generated from scripts/fleet-package-versions.tsv — the mechanism footnotes ³³ and ³⁴ use,
and this note earned it: it quoted 0.26.7 unqualified for a full release cycle, which is the
Alpine v3.24/edge version read as fleet-wide.
| Target | tree-sitter-cli | vs ≥ 0.26.1 | verified |
|---|---|---|---|
| Homebrew | 0.27.0 | at or above | 2026-09-17 |
| openSUSE Tumbleweed | 0.27.0 | at or above | 2026-09-29 |
| Gentoo stable | 0.26.12 | at or above | 2026-09-17 |
| Fedora Rawhide | 0.26.11 | at or above | 2026-09-17 |
| Fedora 45 | 0.26.11 | at or above | 2026-09-17 |
| Fedora 44 | 0.26.11 | at or above | 2026-09-17 |
| Arch | 0.26.9 | at or above | 2026-09-17 |
| openSUSE Leap 16.1 | 0.26.8 | at or above | 2026-09-17 |
| openSUSE Leap 16.0 | 0.26.8 | at or above | 2026-09-17 |
| Alpine edge | 0.26.7 | at or above | 2026-09-17 |
| Alpine 3.24 | 0.26.7 | at or above | 2026-09-17 |
| Fedora 43 | 0.25.10 | below | 2026-09-17 |
| Alpine 3.23 | 0.25.10 | below | 2026-09-17 |
| Alpine 3.22 | 0.25.10 | below | 2026-09-17 |
| Alpine 3.21 | 0.24.4 | below | 2026-09-17 |
The header names the TOOL, not the package, and on two platforms those differ — see the
Mac and openSUSE paragraphs below; each row’s <source> in the TSV records which package its
number was read from. The Gentoo row is the newest stable-keyworded ebuild, not the
newest one in ::gentoo. There is no Debian or Ubuntu row: neither archive carries a
tree-sitter-cli source package at all, which is why the matrix routes that cell through
asset²⁸ — an absent row says that where a guessed one would not. Build suffixes (-r0,
-2.fc43) are dropped; the table records the upstream triple, which is what the floor is
compared against.
Mac: tree-sitter-cli via brew — not tree-sitter, which is now lib-only.
Fedora: tree-sitter-cli via dnf, and it clears the floor on three of its four
lanes — F44 reached it in updates, F45 and rawhide carry it — but F43 does not, and
F43 is a blocking lane in that repo’s CI. On F43 reach past it with mise use -g tree-sitter or
cargo install tree-sitter-cli. dotfiles-Fedora now checks this rather than saying it
in prose. dotfiles-Fedora#192 landed tree-sitter-cli # min:0.26.1 in install/packages.txt,
a cargo fallback in bootstrap.sh that runs only when the installed CLI is below
TREESITTER_FLOOR, and a gate in test/check-packages.sh that fails if the two floors
disagree. Footnote ³³ carries the matching
neovim spread — same distro, same lane, both halves of the one requirement below the
floor on F43, which is the shape this footnote and ³³ each caught on Alpine alone.
Arch: extra carries it and clears the floor.
openSUSE: the CLI is in the base tree-sitter package, on Tumbleweed and both Leap
lanes; what got split off there is the shared library, named for its soname — libtree-sitter0_27
on Tumbleweed, libtree-sitter0_26 on Leap 16.x. There is no tree-sitter-cli package on openSUSE, and
searching for that name is precisely why dotfiles-openSUSE carried this as cargo³ and
cargo-built the CLI on every box until dotfiles-openSUSE#113. Note the inversion against
the Mac line two above — brew’s tree-sitter is the lib-only formula and tree-sitter-cli
is the one you want; openSUSE is the exact opposite, so the same name means opposite things
on the two platforms and neither instinct transfers.
Gentoo: dev-util/tree-sitter-cli is stable-keyworded on amd64, arm, arm64, ppc,
ppc64 and x86 and clears the floor, so it takes no package.accept_keywords line and no
cargo build — dotfiles-Gentoo cargo-built the crate until 2026-08-23, when the atom was
found to have been packaged and stabilised underneath it (dotfiles-Gentoo#116). The table’s
row is the newest ebuild carrying a stable keyword, which is not the newest ebuild:
0.27.0 is ~ on every arch. That gap is why this row’s <probe> is - and not gentoo —
Repology reports the newest version in ::gentoo regardless of keyword, so an automated
re-read would overwrite the one number this paragraph is about. This note said 0.26.11 was
the stable one and 0.26.12 ~-only until 2026-09-17, by which time 0.26.12 had stabilised:
exactly the drift the generated table exists to end.
Gentoo’s maintainer-needed flag from 2026-08-30 (dotfiles-Gentoo#144) was lifted by
2026-09-23 — a person maintainer is back in metadata.xml — and between that, the openSUSE
paragraph above (dotfiles-openSUSE#113) and the Alpine one below (dotfiles-Alpine#122),
three distros in this one footnote moved within a month — re-query this row on every
stamp. Read the .json endpoint or the ebuild, not the
rendered arch table on packages.gentoo.org: it reported app-shells/starship as having
no stable amd64 keyword where the ebuild says KEYWORDS="amd64 arm64".
Where unpackaged: mise use -g tree-sitter or cargo install tree-sitter-cli.
Alpine: the community package is the musl build on every branch, but it clears
the floor on only two of five — v3.24 and edge. v3.21, v3.22 and v3.23 are all below
it, which is the spread this note flattened into one number for a release cycle.
Prefer the package on the two branches where it clears; on the other three
dotfiles-Alpine’s bootstrap.sh supplies a conforming build via cargo, and that guard is
version-checked rather than presence-checked — a presence guard sees apk’s 0.25.10,
skips the build, and leaves the box below the floor in silence (dotfiles-Alpine#122).
~/.cargo/bin is prepended ahead of /usr/bin by zsh/00-tools.zsh, so the cargo build
genuinely shadows the older apk one.
⁶ yq: this matrix targets mikefarah’s Go yq (the jq-for-YAML). Distros also
ship Python yq (kislyuk) under the same yq name; if you land the wrong
one, install the Go build via mise use -g yq or the upstream release binary.
openSUSE is the exception to that warning, not an instance of it. The main OSS yq
is the mikefarah Go build — 4.53.3 on Tumbleweed and on Leap 16.1/16.0 (bp161/bp160)
— and kislyuk’s Python tool ships under its own name, python313-yq / python314-yq, so
the collision cannot happen there. This footnote used to say the reverse, and
dotfiles-openSUSE go-installed the Go build on that basis; as of
dotfiles-openSUSE#113 it packages yq and keeps the go-install as a presence-guarded
fallback (its guard matches the mikefarah string in yq --version, so the packaged build
satisfies it and the block no-ops).
⁷ shfmt: not always in stable apt (Debian/Kali), and not packaged on Gentoo —
absent from ::gentoo and from GURU (third-party overlays carry it as
dev-util/shfmt; there is no dev-go/shfmt atom), so Gentoo takes the go²¹
path — and there, unusually for a ²¹ row, bootstrap.sh does install it:
dotfiles-Gentoo go-installs shfmt unconditionally alongside gron and sesh,
because nvim’s conform formatter is wired to it. If the package is missing,
mise use -g shfmt or go install mvdan.cc/sh/v3/cmd/shfmt@latest. (These mid-2026 rows are
best-effort — verify the exact package on first stamp of each distro.)
⁸ jujutsu (jj): OPT-IN, additive git companion — never replaces git, so a box
without it just skips the HAVE_JJ-gated aliases. Packaged on Arch (jujutsu),
openSUSE (jujutsu), Homebrew (jj), nixpkgs (jujutsu) and Alpine
(community — a native musl build), and on Gentoo as dev-vcs/jj — mind the
name, the atom is jj; dev-vcs/jujutsu has never existed and is the trap this
row used to fall into. Added 2025-12-04 (dev-vcs/jj: new package, add 0.36.0,
EAPI 8, maintainer chutzpah@gentoo.org); every ebuild is ~amd64 with no stable
version, so it needs a package.accept_keywords line. Not in stable
Debian/Kali apt and not on Fedora — dnf has no jujutsu, jj or jj-cli
on F43/F44/rawhide and no retired build to point at, unlike sd/gron which
were dropped — so those two take cargo install --locked jj-cli, the same cargo
pattern as yazi/ouch. The crate is jj-cli, not jujutsu: the jujutsu
crate is a stub pinned at 0.7.2 whose own description reads “You don’t want this
crate - you want the jj-cli crate”, so cargo install jujutsu lands a redirect
rather than the VCS — a second reason to spell it jj on both packaging paths.
As an opt-in tool it is carried in exactly one OS repo’s packages.txt —
dotfiles-Alpine (package jujutsu, binary jj). Arch lists it only as a
commented opt-in you run by hand. Gentoo installs it, but not from
packages.txt: that file is the UNCONDITIONAL emerge, and an atom there would
land on a --no-extras run, so bootstrap.sh emerges dev-vcs/jj from its
opt-in extras block instead (scripts/check-packages.sh check 7 gates the
hard-coded atom). Read the absence from Gentoo’s list as a deliberate placement,
not a decline. The config (jujutsu/config.toml) is inert without the binary.
⁹ sesh: smart tmux session manager that Core already drives from the Ctrl-G
shell widget (35-fzf.zsh) and the prefix + f tmux popup (tmux-sesh.sh); both
degrade to a find+fzf sessionizer when it’s absent. core-doctor already
reports sesh via its own command -v probe, and so do both call sites above — none of
them ever read a flag, which is why #694 dropped HAVE_SESH and kept the _have sesh
probe that feeds the _CORE_PROBED ledger (PORTABILITY.md §5).
Packaged in the AUR as sesh-bin (which
provides/conflicts sesh, so paru -S sesh still resolves — there is no
AUR package under the bare name), Homebrew
(sesh), and nixpkgs (sesh); not in Arch-official, openSUSE, Alpine,
Gentoo, Fedora, or Debian/Kali apt — so most of the fleet, Arch included (its
bootstrap.sh builds from source and skips the AUR sesh-bin), uses
go install github.com/joshmedeski/sesh/v2@latest (note the v2 module path),
the same build path as starship/yazi/atuin where unpackaged. go is already a
pinned mise runtime, so the install works everywhere; mise use -g go first on a
bare box. The seeded sesh/sesh.toml.example config is inert without the binary.
¹⁰ difftastic (difft): OPT-IN structural/AST diff — a companion to delta, not a
replacement. delta stays the default git diff pager; difft is wired as an on-demand
git difftool (git dft, and the gdft shell alias — see git/gitconfig), never as a
GIT_EXTERNAL_DIFF/pager override, so it never shadows delta. Binary is difft (Core
sets HAVE_DIFFT). Packaged on Arch (extra), Alpine (community — a musl build, so the
usual outlier is covered), Fedora, Gentoo (dev-util/difftastic),
openSUSE (Tumbleweed; not Leap 16.0/16.1), Homebrew
(difftastic) and Debian/Kali apt; where unpackaged, cargo install difftastic or mise.
Inert without the binary — the gdft alias is HAVE_DIFFT-guarded and git dft just errors.
¹¹ ast-grep: OPT-IN AST-aware structural search/rewrite — the syntax-tree complement to
ripgrep (text), sd (regex), and gron (JSON). Core adds no alias (like gron/sd) — but
the crate installs a SECOND binary, sg, and that name is already taken: sg(1) is a symlink
to newgrp, shipped by login on the Debian family and by shadow elsewhere. Since #425 put
${CARGO_HOME:-~/.cargo}/bin on PATH ahead of /usr/bin, a cargo installed ast-grep now WINS
that lookup, so a bare sg runs the search tool rather than switching group. Prefer the ast-grep
name, and reach the real one as newgrp (or /usr/bin/sg). This footnote used to say sg “can
collide with setgroups” and that ast-grep “shadows nothing” — both were wrong, and #425 turned
the second into a live shadow rather than a hypothetical one. Core probes it into the _CORE_PROBED ledger and sets no flag
(#694 — nothing read HAVE_ASTGREP; PORTABILITY.md §5). Packaged on Arch (extra) and Alpine (community — a musl build, so the outlier is
covered) and Homebrew; elsewhere via cargo install ast-grep / mise / npm / pip. Inert
without the binary — nothing depends on it.
¹² Gentoo GURU overlay (sd, glow, xh, carapace, 1password-cli, tealdeer,
yazi, lazygit, direnv, gping¹⁹): not in the main ::gentoo tree. Enable once with
eselect repository enable guru && emaint sync -r guru, then emerge the atom. bootstrap.sh
does this best-effort, per-atom (one masked atom doesn’t block the rest), in its guru_install
pass — which runs AFTER the main-tree emerge, so these must not sit in the main packages.txt
blocks or they’re skipped. They are named in a packages.txt comment instead, which is a
pointer to that call, not a decline — bootstrap really does emerge them. direnv is
app-shells/direnv (not dev-util/direnv, which does not exist). gum is not on this list
and must not be added back: app-misc/gum exists in neither GURU nor ::gentoo (nor as
app-shells/gum or dev-util/gum), so an entry can only ever emerge as a skipped: line —
which reads like a keyword mask and gets “fixed” with an accept_keywords entry that unmasks
nothing. That is exactly how it survived in guru_install for months; see ³⁰ for what provides
gum on Gentoo now.
ouch is a GURU atom too, but it is NOT in that guru_install list — it is opt-in, and
--no-extras has to be able to skip it. dotfiles-Gentoo therefore has a third seam,
guru_extras_install, for the one pairing neither existing list covers: overlay-sourced AND
opt-in. It was cargo install --locked ouch until dotgibson/dotfiles-Gentoo#133, on the same
upstream-latest reasoning watchexec²⁵ still carries. That reasoning does not survive contact
here twice over. The cargo build cannot succeed on a GCC/libstdc++ box at all: ouch’s
default unrar feature pulls unrar-ng-sys, whose build.rs unconditionally adds
-stdlib=libc++. And GURU’s app-arch/ouch is 0.8.3 — level with upstream (2026-09-13),
with a src_prepare() that seds exactly that flag out. So the route-around bought no version
advantage at all and cost the tool, on every run, silently.
shellcheck on Gentoo is dev-util/shellcheck-bin, and the -bin is load-bearing.
dev-util/shellcheck is the Haskell build: it needs >=dev-haskell/aeson-1.4.0 and the rest
of the GHC chain, every one ~arch, so keywording shellcheck alone unmasks nothing and the
emerge still fails — while the accept_keywords file you were told to edit now carries the
entry you were told to add. That is the gum trap above arriving from the other direction, and
it is the more expensive one: reaching the source atom by hand costs ~60 dev-haskell/*
keyword lines plus a GHC build. dev-util/shellcheck-bin is upstream’s own static release,
KEYWORDS="-* amd64 ~arm arm64" — stable on amd64 and arm64, so it needs no keyword line
at all. Note the leading -*: on x86, ppc64 or riscv it is reachable only via **, never
~arch. The two atoms block each other (RDEPEND="!dev-util/shellcheck"), so a box that
already merged the source one must emerge --unmerge dev-util/shellcheck first.
¹³ op = 1Password CLI. bootstrap.sh installs it from 1Password’s official signed repo,
which differs per family: dnf/rpm repo (Fedora/openSUSE), apt repo (Debian/Kali), apk repo
(Alpine — a native musl build, so it’s fine on the musl outlier), and the GURU
app-misc/1password-cli (Gentoo). Arch installs nothing: it has no AUR helper, so
bootstrap.sh only prints a paru -S 1password-cli hint, the same shape as viddy and
carapace. A vendor repo, not the OS repo; the apt/rpm setup is key-first (the repo entry
is added only after 1Password’s signing key has been fetched and imported, so a failed key
leaves no repo behind — it does not roll back a repo whose later install fails).
¹⁴ Alpine testing-only (duf, glow, tealdeer, ouch). All four are musl-fine
tools that live in testing on edge (never promoted to community on any stable release,
incl. 3.24), which isn’t enabled by default on a stable release. bootstrap.sh builds them
from source instead of force-enabling testing,
and the paths are not all the same one: duf + glow take go install (static,
musl-safe), tealdeer takes a plain cargo install --locked tealdeer (it is the tldr row’s
cargo³, which is why that row does not cite this note), while ouch takes
cargo install --locked ouch --no-default-features — bzip3’s libbzip3-sys build
script runs bindgen, bindgen dlopens libclang, and Rust’s musl toolchain links statically, so
the DEFAULT feature set cannot build on the outlier at all (installing clang does not help).
None of the four is listed in install/packages.txt, and putting them there “as a best-effort”
is a footgun the OS repo documents against: apk fails the whole transaction on one unknown
name, so a permanently-unresolvable entry breaks the bulk apk add on EVERY run and forces the
per-package retry loop across the entire list.
¹⁵ Kali glow/gum: recent Debian sid packages (Kali rolling tracks testing/sid). If they
haven’t migrated to your snapshot, bootstrap falls back to the Charm apt repo
(repo.charm.sh/apt) — the same route ubuntu/debian always take.
¹⁶ viddy: the watch replacement — Core aliases watch→viddy (HAVE_VIDDY-guarded in
zsh/20-aliases.zsh), so a box without the binary just keeps classic watch. viddy is a
Rust CLI (rewritten from Go upstream), so it installs via cargo install viddy, not
go install. Packaged on Homebrew (viddy, already in the macOS Brewfile) and the AUR;
not in Arch-official, Gentoo, or Debian/Kali apt, but now in Alpine
community (a native musl build — apk-installed, with the cargo build kept as a fallback —
but still the pre-rewrite Go 0.4.0 on every branch, v3.21 through edge; upstream Rust is
1.x, and the apk install stops bootstrap’s presence-guarded cargo fallback from ever running)
and openSUSE Tumbleweed repo-oss (see ¹⁸).
Where unpackaged, bootstrap.sh builds it best-effort via cargo install --locked viddy
(the same cargo path as yazi/dust/tealdeer) — not on Kali: dotfiles-Debian installs
Kali’s cargo but cargo-installs none of yazi/viddy/ast-grep, hence cargo²¹ in those cells.
Arch is the other exception: it ships no rust toolchain and builds no AUR
helper (see its packages.txt), so bootstrap prints a hint to paru -S viddy instead of
auto-installing. Inert without the binary.
¹⁷ jnv: OPT-IN interactive jq-filter editor + collapsible JSON viewer — the “explore an
unfamiliar API/JSON response” verb, complementing jq (transform), gron (grep), and yq
(YAML). Its own command (no alias, like jq/gron/ast-grep), probed into the
_CORE_PROBED ledger by zsh/00-tools.zsh and carrying no flag since #694, inert
without the binary. A Rust CLI (embeds jaq, so no external
jq needed). On Linux this is detect-only on every repo but one — jnv is in no
install/packages.txt anywhere, so core-doctor reports it present only once you
install it yourself. The exceptions, per ²¹: dotfiles-Alpine’s bootstrap.sh DOES install it
(cargo, best-effort — hence cargo³ in its cell, since jnv is unpackaged on all five
Alpine branches including testing on edge, making cargo its only source there), and
dotfiles-Gentoo cargo-builds it in the opt-in extras block that --no-extras skips. On
macOS the Brewfile carries it. Everywhere else the row above names where that platform
gets it when you opt in — Arch pacman -S jnv, Nix, or cargo install --locked jnv — not
an automatic install. Arch’s cell used to read AUR and
this footnote used to prescribe paru -S jnv; both are now wrong. jnv entered extra as
0.7.1-1 on 2026-04-01, confirmed on-box with pacman -Si jnv (Repository: extra), so no AUR
helper is involved on Arch any more. Wiring it into the per-repo bootstrap
(the ³ best-effort path viddy/yazi use) is done on Alpine and Gentoo and remains a tracked
follow-up on the rest. jnv is in neither ::gentoo nor GURU, nor any Zugaina-indexed overlay
(checked 2026-09-27, dotgibson/dotfiles-Gentoo#209).
¹⁸ openSUSE Tumbleweed now ships these first-class in the main OSS binary repo
(repo-oss, i.e. .../tumbleweed/repo/oss — built from OBS openSUSE:Factory; note
src-oss is the source-RPM repo and is not what zypper in resolves against), so
zypper in beats the upstream-installer/cargo/go fallback these rows used to prescribe:
starship, atuin, yazi, viddy, ouch, doggo, ast-grep.
Leap 16.0 and 16.1 have now been audited (dotfiles-openSUSE#89, verified against both
releases’ repo/oss binary indexes on 2026-08-21) — the supported Leap is 16.x; 15.6, which
this note used to hedge about, is EOL. All seven resolve on both, via Backports rather
than the main OSS repo — starship 1.21.1, atuin 18.3.0, yazi 25.5.31, viddy 0.4.0,
ouch 0.5.1, doggo 1.0.5, ast-grep 0.28.0 (bp160/bp161 disttags). So zypper in
is the right first move on Leap too, and the ³ fallback is no longer the expected path for
these seven — but Leap pins where Tumbleweed rolls, so treat those versions as a floor and
take the ³ path when a row needs something newer. The rows are named for Tumbleweed because
that’s the flavor this fleet targets.
Four of the seven (starship, atuin, viddy, doggo) are also installed by
dotfiles-openSUSE’s bootstrap.sh, which stays correct and harmless either way — each
install is presence-guarded, so a packaged binary just short-circuits it. ouch and
ast-grep are not: that bootstrap has no installer for them, so the old cargo³ cells
promised a fallback that never existed and the package name above was, for a while, a name you
had to type yourself. Moving any of these into install/packages.txt is a separate judgment
call — it trades upstream-latest for the distro build. dotfiles-openSUSE has since made
that call the other way: its list flipped to packaged-first, and starship, atuin and
yazi are now in it — yazi’s installer is gone outright, so the two curl | sh installers
left (starship, atuin) only run as a fallback — and as of
dotfiles-openSUSE#113 ouch and ast-grep are in it too, which is what finally closes the
gap this paragraph opened: no installer plus no list entry had meant no automatic path at all.
viddy deliberately stays out (cargo, for upstream-latest), and doggo likewise (go, same
reason) — those two are declines, not gaps.
¹⁹ gping: the ping replacement — Core aliases ping→gping (HAVE_GPING-guarded in
zsh/20-aliases.zsh), so a box without the binary just keeps classic ping. Detect-only on
every Linux repo except dotfiles-Alpine and dotfiles-Gentoo — Alpine’s
install/packages.txt carries gping outright, and Gentoo emerges net-analyzer/gping from
GURU in its guru_install pass. Gentoo’s list names the atom only in a comment because
overlay-only atoms must live there (the main-tree emerge runs before GURU is enabled, so an
atom in the list proper would be skipped and never retried) — that comment is a pointer to the
call, not a decline, which is what this footnote used to read it as. The macOS Brewfile
carries it too. On the other seven repos the alias lights up only once you install it yourself —
this row exists so there is a documented path when you do. (aliases.md and PARITY.md have advertised the alias since
v3; the matrix row is what was missing.) A Rust CLI → cargo install gping anywhere
unpackaged. Packaged: Arch extra, Alpine community (a native musl build, so the outlier is
covered), Homebrew (gping), nixpkgs, and Debian/Kali apt — where the source package is
rust-gping but the binary you install is plain gping (Debian trixie 1.19.0, sid/Kali
rolling 1.20.4). openSUSE: Leap 16.0 and 16.1 both carry it via Backports at 1.17.3
(gping-1.17.3-bp160.1.13 / -bp161.1.6, verified 2026-08-21) — behind, but present without
adding a repo; Tumbleweed builds it from Factory (1.20.1). The old 15.6/1.16.1 reading
here predated Leap 16 and 15.6 is now EOL. Gentoo is GURU-only (net-analyzer/gping) — there
is no main-tree atom, so it is emerged like the ¹² atoms, in the same guru_install pass and
from the same overlay; nothing there is left for you to do by hand. Inert without the binary;
nothing depends on it.
²⁰ atuin daemon mode — the one part of the atuin story that is NOT Core’s to decide.
Core ships atuin/config.toml (symlinked to ~/.config/atuin/config.toml) with the
[daemon] block off; the daemon owns the SQLite writes so shells stop contending for
the DB lock, which is where atuin’s tail latency comes from on a busy multi-pane box. What
differs per machine is how the daemon gets launched, so that half lives in the OS repo:
| Machine | How the daemon runs | What the OS layer exports |
|---|---|---|
| Fedora✔ · Debian/Ubuntu/Kali✔ · Arch · openSUSE · Gentoo (systemd) | systemd --user unit — copy examples/atuin-daemon.service into ~/.config/systemd/user/, then systemctl --user enable --now atuin-daemon (and loginctl enable-linger $USER if you want it alive outside a login session) | ATUIN_DAEMON__ENABLED=true |
| Alpine✔ (musl, no systemd) | atuin supervises its own daemon — no unit, no service manager, nothing to install | ATUIN_DAEMON__ENABLED=true + ATUIN_DAEMON__AUTOSTART=true |
| macOS | same as Alpine: autostart beats hand-writing a launchd plist — the socket path is atuin’s own to resolve (see the note below the table) | ATUIN_DAEMON__ENABLED=true + ATUIN_DAEMON__AUTOSTART=true |
| Windows | out of scope — dotfiles-Windows vendors no core/ and replicates its host config in PowerShell | — |
Where the daemon socket lives moved in 18.20.0. Upstream PR #3910 (merged 2026-08-12,
shipped in v18.20.0) changed the default for systemd_socket = false — the shape Core
recommends — from $XDG_RUNTIME_DIR/atuin.sock (falling back to $XDG_DATA_HOME/atuin/atuin.sock
where that is unset) to $TMPDIR/atuin-$UID/atuin.sock. systemd_socket = true is
unchanged. Measured here on 18.21.0, not read off the changelog: the binary’s own
default-config documents the new path, a daemon started in a sandbox binds there, and it
refuses a socket directory it did not create unless that directory is exactly 0700
(incorrect permissions (expected 700, got 755)) — a box that pre-creates atuin-$UID with
an ordinary umask gets a daemon that exits before binding, which from the socket looks like
one that never started. (An earlier version of this note said the move had landed in no
release; it dated the change by its merge, after v18.20.0-beta.3, and was wrong about the
stable that followed.)
Two consequences worth recording. The old macOS reasoning — “XDG_RUNTIME_DIR is unset there
so the socket lands in the data dir” — stopped being true, and macOS’s per-user $TMPDIR
actually unifies macOS and Linux rather than splitting them. And atuin’s own client gained a
legacy search list, while Core’s guard resolved exactly one expression: on 18.20+ it would have
probed a path nothing binds, exported ATUIN_DAEMON__ENABLED=false at every shell’s first
precmd and unhooked the watchdog — permanently, and with no warning, because
_CORE_ATUIN_DAEMON_WAS_UP is never set on that path. Fixed ahead of the release in #518: the
guard probes the new default and both legacy paths, newest first, so it follows a daemon
across the version boundary in either direction. The research apparatus was the part that
fell behind — its autostart measurement still waited on the old path until #826.
18.21.0 added a second /tmp candidate, and the guard follows it (#941). Upstream
PR #4036 (merged 2026-08-31) makes the client try $TMPDIR/atuin-$UID/atuin.sock and then
/tmp/atuin-$UID/atuin.sock even when $TMPDIR is set, because the daemon and the shell need
not agree on $TMPDIR — a systemd user unit starts without one and binds /tmp, while a shell
that exports one (a 99-local, a tmux server started under a different environment, a unit
with PrivateTmp=) resolved only its own. On that shape the guard probed a path nobody binds
and took the same silent one-way degrade, one candidate short of where atuin’s own client
would have connected. It now appends the /tmp path whenever $TMPDIR is set and is not
/tmp, in upstream’s order. Upstream calls #4036 a hotfix with a more robust resolution in the
works, so this list may grow again; the candidate-list test in scripts/test/71-prompt-atuin.sh
is where a new path gets pinned. Both anchors were re-dated to 18.21.0 in the same change, on
the strength of the 2026-09-03 atuin-guard-verify dispatches (three runs, holds on both
premises) rather than a changelog read, and to 18.22.0 on 2026-09-16 the same way.
The autostart premise then MOVED, on 2026-09-17, and the guard changed rather than the
recipe. A sixth arm — a daemon whose pid is alive while nothing answers its socket — was
added to the harness and measured against 18.22.0: absent and stale still spawn and land
their row, and that shape does not. atuin reads a live pid as health, so it never replaces the
one process that cannot serve, and the client blocks on the pidfile lock and exits 1
(atuinsh/atuin#4114). The guard no longer stands down under autostart; it probes, stays
silent for a socket that is merely waiting to be spawned onto, and prints one warning naming
the pid to kill when it finds the wedge. Nothing is disabled, because disabling under
autostart would remove the launcher these two rows exist to document. The exports below are
unchanged.
The exports belong in that repo’s os/<os>.zsh (loader fragment 80), never in the Core
config: Core is vendored identically to every repo, so a per-machine value there would be
wrong on the other eight. autostart is mutually exclusive with systemd_socket = true —
pick the unit or pick autostart, not both.
✔ marks the machines where the exports are actually wired today — Fedora, Debian/Ubuntu/Kali and Alpine, three of the seven Core-vendoring machines this table covers. The marker is per machine, not per row: the systemd row holds a wired Fedora and a wired Debian/Ubuntu/Kali alongside three unwired ones. For the other four — Arch, openSUSE and Gentoo (sharing that row with Fedora) plus macOS — the cell is the documented recipe, not a shipped state, so follow the rollout order below (Fedora first as the template, Alpine second as the design’s real constraint, then the rest) rather than assuming your repo already does this.
Kali is not a machine of its own here, and used to be listed as one. It was
dotfiles-Offense’s identity while that repo carried the Kali OS band; the band moved to
dotfiles-Debian’s only:kali tier, so Kali is now one of that repo’s three targets and
inherits its wiring — which is why it moved into the wired cell rather than staying beside
it. Nothing was left owning a Kali row.
dotfiles-Debian is wired end to end: its os/debian.zsh exports ENABLED, and — because
Ubuntu has systemd, so the AUTOSTART fallback never fires there — its bootstrap.sh
also installs and enables the systemd --user unit. Exporting ENABLED without installing
a launcher is the one state this note warns against, and on a systemd box the autostart
escape hatch does not cover for it. The Windows row is neither wired nor pending:
it is out of scope, vendoring no core/ at all. Neither role repo has a row here, both
by the same design — Defense and Offense are distro-agnostic and carry no os/
layer, so their atuin exports come from whichever OS repo is underneath them (see “Repo
status”). Eight machines + Offense + Defense = the ten Core-vendoring repos in
scripts/os-repos.txt.
That used to read “eight machines + Defense”, which counted Offense as a machine. The
arithmetic was right and the label was a release out of date: Offense shed os/,
install/packages.txt and scripts/tool-versions.env when the Kali lane moved, so it now
qualifies for exactly the exemption this paragraph grants Defense.
For those exports to work at all, atuin/config.toml must leave enabled and autostart
unset — and it does. atuin builds its config as defaults → environment → config file,
with the file source added last, so in the config crate the file WINS: any key written
there shadows its ATUIN_* override. Core previously wrote enabled = false explicitly,
which silently made every export in this table a no-op — measured against atuin 18.19.0, the
client made zero connect() calls to the daemon socket with the key present and one with it
absent. Upstream’s defaults are already false, so leaving the keys out ships the same OFF
default while letting the override through. scripts/test-core.sh asserts they stay out. Adoption order is Fedora first (the template the
Linux repos are stamped from), Alpine second (it is the design’s real constraint, so proving
it early is worth more than doing it last), then the rest.
Under the systemd-unit launcher, zsh/00-tools.zsh probes the socket before the first prompt
and then, throttled to at most one connect(2) a minute, for the life of the shell, forcing the
daemon off for that shell the first time a connect fails: an absent — or stale, i.e. left
behind by a crashed daemon — socket does not cost atuin a failed write, it costs the row. So a
dead daemon costs the lock relief, not your history — including for a session that was already
open when the daemon went away, which is the case the one-shot probe used to miss entirely
(dotgibson/dotfiles-core#366). Degradation is one-way: the guard unhooks itself and never
re-enables, because direct writes always work, so the only price of being early is the lock relief
until the next shell. A shell that was already degraded when it started stays silent — nothing
changed under it — while one whose daemon died mid-session prints a single warning, that being
the case where an open session’s plumbing changed underneath it. core-doctor distinguishes the
two afterwards, and core-doctor --json exposes them as atuin_daemon.degraded / .was_up.
--json also carries detection.ran / detection.missed (#545). missed names tools that
are on PATH now but were not when Core ran detection at band 00 — so they have no
HAVE_* flag, no alias and no shell init, however green their row looks. The cause is a
directory that joined PATH later (80-os.zsh, an 85-* role fragment, 99-local.zsh, or
mise’s per-directory hook), and the remedy is to move that prepend into 00-tools.zsh’s
bindir list. ran is false wherever band 00 never loaded — a script, zsh -c, the unit
harness — and missed is then necessarily empty and means nothing, so a provisioning gate
should read jq -e '.detection.missed == []' only after checking .detection.ran.
detection.stale is its mirror (#631): tools Core did detect at band 00 and that are gone
from PATH now. The flag is still set, so 20-aliases.zsh already defined the alias it gated
— which matters most for the six that shadow classic commands (ps, top/htop, watch,
df, ping, help), where the result is not a missing procs but a broken ps. The report
names the dangling aliases rather than the tools for that reason. The usual cause on a live
shell is mise’s chpwd hook taking a toolchain away on a cd. Same ran caveat, same gate
shape: jq -e '.detection.stale == []'. The two lists are disjoint by construction — a row is
present-and-unprobed or absent-and-probed, never both.
CORE_ATUIN_PROBE_INTERVAL tunes the window for a box where connect(2) on that path is not
cheap. One wrinkle worth knowing: the disable is an export (that is how it reaches the atuin
binary), so a shell started from a degraded shell inherits ATUIN_DAEMON__ENABLED=false and
stands down as “never opted in” — harmless, since it also writes directly, but it will not pick
the daemon back up until you start a shell from a clean parent.
Under autostart the probe runs but the degrade does not — and that is the Alpine and
macOS rows above. The probe used to be skipped there entirely, because an absent socket is the
client’s cue to start one rather than a fault; what has never been available on those two
rows is the degrade, since disabling the daemon would permanently defeat the only launcher
they have. The paragraph above records what changed and why. atuin’s own health-checking is
what covers them, and that is measured rather than assumed
(dotgibson/dotfiles-core#402). It is a second premise with its own mode, its own anchor line
in zsh/00-tools.zsh and its own issue title:
scripts/research/verify-atuin-guard.sh --premise autostart (or make verify-atuin-guard-autostart)
spawns a real daemon, checks that one appears and that the entry lands from each unreachable
shape, and proves the teardown before deleting anything. The atuin-guard-verify workflow
(manual dispatch since #687) runs it as a separate job from the silent-discard one. It
reports moved today and will keep doing so while atuinsh/atuin#4114 is open: the verdict
is computed from what upstream did, not from what Core does about it.
Two things that measurement established on 18.19.0, both worth knowing before you touch these
rows. The stale-socket shape is the load-bearing one — every atuin history start is a
fresh process, so “fire-and-forget” can only mean that a crashed daemon’s leftover inode
defeats the spawn, and a check that only tried an absent socket would miss it. And the healing
lives in the client: atuin daemon start on its own refuses over a stale inode with
Address already in use, while the autostart path unlinks it first.
Watch atuinsh/atuin#3957 — it would make that last sentence wrong. Opened 2026-08-20,
still open and unreviewed, it makes the daemon unlink a stale socket on bind failure. The
“healing lives in the client” finding is the measured basis of --premise autostart and of
CORE_ATUIN_AUTOSTART_VERIFIED_AGAINST in zsh/00-tools.zsh; if #3957 merges, the daemon
heals itself and the stale arms stop measuring what they were built to measure. That is a
narrower consequence than it once was: the premise no longer holds a stand-down up, because
there is no longer a stand-down. No action while it is unmerged — and in particular do not
edit that anchor, which is a claim the premise was re-measured, not a version bump. Re-run
make verify-atuin-guard-autostart if it lands. The one that would change the guard is
atuinsh/atuin#4114: health-checking the socket instead of the pidfile is what would leave
the wedged-daemon warning with nothing to fire on.
Still not covered, so the default --premise discard caveats are not the only ones: the
dispatched job runs on glibc Linux, which is neither of the two machines this premise is load-bearing for,
and a run that is green there is the weakest evidence in the whole arrangement for these rows.
Running make verify-atuin-guard-autostart on the Alpine or macOS box itself is what actually
speaks for it.
The probe’s limit, because it decides which unit you should install: it cannot tell an
accept-but-silent socket from a healthy one. systemd socket activation produces
exactly that state when the daemon behind the socket is dead — the socket keeps accepting,
the client waits, and that is the indefinite freeze in atuinsh/atuin#3382. Prefer the plain
always-running service above; if you do use a .socket unit with systemd_socket = true,
you are outside what Core can protect.
²¹ Available, not installed — the same shape as jnv¹⁷ and gping¹⁹, and the
counterpart to ³. These cells name where the tool comes from when you opt in, so on
the repos that do not carry it Core lights the HAVE_* probe only once you install it
yourself. That promise now actually holds for a cargo install, which is what #425 fixed:
${CARGO_HOME:-~/.cargo}/bin used to reach PATH via the OS layer at band 80, a whole
load-order band after 00-tools.zsh probed, so the flag stayed dark and the alias was never
made while core-doctor — probing live, later — reported the tool present. It joins PATH
before detection now.
Coverage here is per tool, never a fleet-wide zero — the table below is the authority, not
the prose. Two repos’ bootstrap.sh really do install entries from this family:
dotfiles-Alpine (ouch and jnv¹⁷, via cargo¹⁴) and dotfiles-Gentoo (shfmt
unconditionally via go; ast-grep¹¹, jnv¹⁷ and watchexec²⁵ via cargo, plus
dev-vcs/jj⁸ via emerge and app-arch/ouch via GURU’s guru_extras_install seam, all in an
opt-in extras block that --no-extras skips; and gping¹⁹ via GURU). For the other seven
repos “no bootstrap installs it” still holds.
How the Gentoo half of that went unnoticed is the lesson worth keeping: this table was
previously verified against each repo’s install/packages.txt alone, and Gentoo is the repo
that installs the most from bootstrap.sh instead — deliberately, because packages.txt is its
UNCONDITIONAL emerge and everything opt-in must therefore live in the script. Verified against
packages.txt, four of the eight Gentoo cells below read — when bootstrap installs the tool.
Check both files, or this row goes stale again.
Verified 2026-08-21 (Gentoo column re-verified against bootstrap.sh as well) against all six
Linux repos plus the MacBook Brewfile — a — means detect-only there, and the packaged name
in that tool’s row above is what you would install by hand rather than what a bootstrap gives
you:
| Tool | macOS Brewfile | Alpine | Gentoo | Arch / Debian / Fedora / openSUSE |
|---|---|---|---|---|
hyperfine | ✓ | hyperfine | app-benchmarks/hyperfine | — |
shellcheck | ✓ | shellcheck | dev-util/shellcheck-bin | — |
shfmt | ✓ | shfmt | bootstrap, go⁷ (unconditional) | — |
ouch | ✓ | bootstrap, cargo¹⁴ | bootstrap, app-arch/ouch (extras) | openSUSE: ouch¹⁸; others — |
lnav²⁴ | ✓ | lnav | app-admin/lnav | — |
git-absorb²⁶ | ✓ | git-absorb | dev-vcs/git-absorb | — |
gping¹⁹ | ✓ | gping | bootstrap, net-analyzer/gping (GURU¹²) | — |
watchexec²⁵ | — | watchexec | bootstrap, cargo watchexec-cli (extras) | — |
- The
ouchrow’s last cell is split because that column is four repos, not one.dotfiles-openSUSEaddedouchto itsinstall/packages.txtin dotfiles-openSUSE#113, so the tool is no longer detect-only there while it still is on Arch/Debian/Fedora. Note this is apackages.txtaddition, not abootstrap.shone — so the “two repos’bootstrap.sh” count above stays right, and this is exactly the both-files distinction the lesson two paragraphs up warns about.ast-grepmoved the same way in the same PR but has no row here. - “extras” above means Gentoo’s opt-in block: installed by default, skipped by
--no-extras. That flag is the one thing keeping these honest as ²¹ entries rather than ³ ones — the tool is still something you can decline. - Gentoo’s
ouchcell is a GURU cell, and it used to be a cargo cell by CHOICE — the same shapewatchexec²⁵ still has, and worth stating because the two parted ways. GURU carriesapp-arch/ouch(0.8.1, 0.8.2, 0.8.3) and::gentoocarries noouchat any category;dotfiles-Gentoocargo installed it anyway, for upstream-latest, until dotgibson/dotfiles-Gentoo#133 found the cargo build cannot succeed on a GCC/libstdc++ box at all (¹² has the mechanism) and moved it toguru_extras_install app-arch/ouch. Read the cell as “overlay ebuild, opt-in”, andwatchexecas the one cargo-by-choice example left. Verified 2026-09-13 againstgentoo/guru@master. The genuinely-unpackaged Gentoo entries in this family areast-grep¹¹ andjnv¹⁷, absent from both trees. - This list used to read “macOS-only in practice: the MacBook
Brewfilecarries them; no Linux repo does.” Every row above falsifies that — Alpine carries seven of the eight outright and Gentoo installs all eight, four of them frombootstrap.sh. Keep it a per-tool statement: the family is defined by “Core probes it, and most repos leave installing it to you”, not by a distro, and no longer by “no bootstrap installs it” — two now do. (Deliberately not a counted list — “all four” went stale the first time this family grew, “no Linux repo does” the second, and “ouchis the one entry” the third.) - The cells that previously showed ³ here —
ouchon Gentoo and Kali,jujutsuon Kali,ast-grepon Gentoo and Kali,shfmton Gentoo,lazygiton Kali — promised a best-effort bootstrap install that does not exist, verified against each repo’sbootstrap.shandinstall/packages.txt. (Gentoo’souch,ast-grepandshfmthave since acquired such an install, andjujutsuon Gentoo is now the packageddev-vcs/jj⁸ — the correction stands for Kali, and for what those cells claimed when it was made.)lazygitis the sharpest case: every other Linux repo installs it, Kali installs it throughdotfiles-Debian’sonly:kalitier (install/packages.txt), and Core shipsalias lg='lazygit'regardless. - Kali’s lane moved. This note used to read “Kali installs nothing from this family”,
which was true while
dotfiles-Offenseowned the Kali OS band. It no longer does:dotfiles-Debianowns that lane through itsonly:kali/skip:kalitiers, and installs a substantial subset.ast-grepis still absent there, which is the claim that survives. This note used to carve out an exception saying it did — “bootstrap.sh, cargo best-effort” — and that is why theast-greprow kept a³in its Kali cell after its neighbours lost theirs. There is no such install:dotfiles-Offense’sbootstrap.shcontains noast-grep, and its only twocargomentions are the comment andexportthat put~/.cargo/binon PATH for tools an operator added by hand — which is the ²¹ contract, not a ³ one. The cell is nowcargo²¹, matchingouchandjujutsuin the same column.
This is the same overclaim already corrected once for openSUSE (ouch/ast-grep): ³ means
“bootstrap.sh installs it best-effort”, so a ³ with no installer behind it reads as “you
have this” when you do not.
ouch changed its default unpack LOCATION in 0.8.0, and the spread in this table is what
makes that Core’s problem. Since 0.8.0 (ouch-org/ouch#962) ouch decompress foo.tar.gz
unpacks into ./foo/ rather than into the CWD, with a new --here for the old behaviour. The
rows above are not level: openSUSE Leap ships 0.5.1, Alpine’s index 0.6.1, GURU and
Arch 0.8.3 — so the same extract foo.tar.gz built two different trees on two
supported boxes, silently and with no error on either. Core’s extract
(zsh/30-functions.zsh) pins the old semantics by probing ouch decompress --help for
--here and passing it where it exists — the ²² rule below, not a version compare, and here
the probe is also fail-safe: a build without the flag is a build that already extracts into the
CWD. This is a Core fix, not a matrix one; the rows stay as they are. Two things it keeps
honest — the hand-rolled tar/unzip fallback for a box with no ouch at all, and extract’s
own tarbomb and clobber guards, both of which are written against the CWD. Separately and
not a 0.8.0 change: ouch writes a single decompressed .gz/.bz2 into the CWD on every
version, where gunzip writes next to the archive, so Core runs it from the archive’s
directory for those two formats.
²² sd changed its default in 1.1.0: it now processes input line by line, and the old
whole-file behaviour moved behind --across / -A. The failure mode is silent — a pattern
that spans a newline (sd 'foo\nbar' baz) matches nothing, leaves the input unchanged, and
still exits 0, so a script carries on as though it had rewritten the file. Confirmed
behaviourally here rather than taken from the release notes. Worse, sd --version cannot
tell you which behaviour you have — the Homebrew 1.1.0 build self-reports sd 1.0.0 — so
version sniffing is useless and a version gate on a Core flag was never an option. Probe
the behaviour instead: sd --help | grep -q -- --across. Core itself is unaffected: sd
is detect-only (a bare _have sd in zsh/00-tools.zsh, ledger row only, no flag since #694)
with deliberately no alias (zsh/20-aliases.zsh — never shadow sed in scripts), and nothing
in Core shells out to it. This is a warning for muscle
memory and for the role layers.
Do not blanket-add -A to role scripts. The flag does not exist before 1.1.0, and this
matrix exists precisely because distros lag (Gentoo takes sd from the GURU overlay), so a
script that hard-codes it breaks on the older build — which already matches whole-file
and needs no flag. A script that must run against both feature-detects and builds the flag
list, rather than assuming either default:
sd_across=(); sd --help 2>/dev/null | grep -q -- --across && sd_across=(-A)
sd "${sd_across[@]}" 'foo\nbar' baz file
²³ Arch refresh and count-pending (the Package-manager commands table above).
refresh is listed for completeness — dotfiles-Arch deliberately ships no alias for
it — and count-pending is checkupdates for the same reason the alias does not exist. A bare
pacman -Sy is safe on its own, but refreshing the sync DB and then installing is
the partial-upgrade footgun, so os/arch.zsh provides only pacu (a full -Syu)
and pacout (checkupdates, which lists updates without touching the sync DB at
all). There is no -Sy <pkg> alias on purpose — see “Distro quirks” below. Do
not “helpfully” add one.
²⁴ lnav: OPT-IN log reader — the verb Core had no tool for. bat/rg read a log as
lines, jq/gron/jnv read it as JSON, glow as markdown; lnav reads it as a
log: it autodetects common formats, merges several files into one timeline ordered by
timestamp, follows like tail -f, and exposes the parsed records to SQL. Its own command
(no alias, like jq/gron/jnv), probed into the _CORE_PROBED ledger by
zsh/00-tools.zsh and carrying no flag since #694, inert without the binary. A C++ CLI, so it has no cargo/go install escape hatch like the
Rust/Go tools above — but it does not need one: upstream publishes static musl binaries
per release (lnav-0.14.1-linux-musl-x86_64.zip, and an arm64 twin), so the fallback on
an unpackaged or lagging box is “unzip the official build”, not “compile it”. That is also
the cleanest way to get 0.14.1 onto Gentoo or Debian/Kali without waiting for the package.
No bootstrap.sh installs it anywhere, but it is not detect-only across the board:
dotfiles-Alpine (lnav) and dotfiles-Gentoo (app-admin/lnav) both carry it in
install/packages.txt, and the MacBook Brewfile has it too (added 2026-07-15). On the other
four Linux repos core-doctor reports it present only once you install it yourself. So lnav sits in
²¹’s family — probed but never bootstrap-installed — rather than jnv’s thinner “two platforms
package it, cargo everywhere else” one: every distro in the table above packages lnav on at
least one lane — openSUSE only on Tumbleweed, not Leap 16.0/16.1 — and two of the repos
ask for it.
Versions verified against each distro’s own package pages on 2026-08-12, every row re-verified the same way on 2026-09-06 and again on 2026-09-23, not taken from a repology snapshot. Upstream is 0.14.1 (2026-09-05). Rolling targets get one query each, because that query is the complete answer; Fedora is versioned, so every supported stable release is named separately rather than collapsed into one unqualified ✓:
| Target | Release | lnav |
|---|---|---|
| Arch | extra (rolling) | 0.14.1-1 |
| openSUSE | Tumbleweed (rolling); not Leap 16.0/16.1 | 0.14.1 |
| Alpine | edge/community — native musl build | 0.14.1-r0 |
| Homebrew | rolling | 0.14.1 |
| Fedora | Rawhide (F46) | 0.14.1-1.fc46 |
| Fedora | F45 | 0.14.0-3.fc45 |
| Fedora | F44 | 0.13.2-2.fc44 |
| Fedora | F43 | 0.12.4-2.fc43 |
| Kali/Debian | rolling / sid | 0.13.2 |
| Gentoo | app-admin/lnav | 0.11.2 |
So “Fedora has it” is true, and as of the 2026-09-23 re-check “Fedora is current” is true only on Rawhide: 0.14.1 landed on 2026-09-05 and Rawhide (F46) now carries 0.14.1-1, but F45 branched before it and still carries 0.14.0-3, one patch back, while F44 and F43 track one and two minors back respectively. The rolling targets — Arch, Alpine and Homebrew within a day, Tumbleweed and Fedora Rawhide since — have all picked 0.14.1 up; no versioned stable release has. Two targets lag enough to be worth naming:
- Gentoo
app-admin/lnavis 0.11.2 — the only version in the tree, stable on amd64/x86, and the package is flagged as needing a new maintainer, so do not expect it to close the gap on its own. Three minor releases behind. Re-check on the next Gentoo stamp. - Kali/Debian
lnavis 0.13.2 — one minor behind, the smaller gap of the two.
On either, the upstream static musl zip above is the way to 0.14.1 without waiting.
²⁵ watchexec: OPT-IN event-driven repetition — the third corner of a triangle Core
already had two of. viddy re-runs on a timer (watch), hyperfine re-runs a fixed
count and measures; watchexec re-runs when files change (watchexec -e py -- pytest). Its own command, probed into the _CORE_PROBED ledger by zsh/00-tools.zsh and
carrying no flag since #694, inert without the binary. Deliberately not aliased to watch — zsh/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.
The one tool in ²¹’s family that inverts it: macOS is the machine that doesn’t get it.
The MacBook Brewfile does not carry watchexec — the only ²¹ entry it skips — while
dotfiles-Alpine’s install/packages.txt carries it outright and dotfiles-Gentoo
cargo-installs it from the extras block. The other seven machines are opt-in.
(This paragraph used to read “the only tool in this table that nothing in the fleet installs,
including macOS”; Alpine falsified the first half, and Gentoo — checked against bootstrap.sh
rather than packages.txt alone — falsified what was left of it.) Availability, verified
2026-08-12, Linux-repo coverage re-verified 2026-08-21 against both files, versions
re-verified 2026-08-30 against each repo’s own package pages, the Arch/Homebrew pair
alone re-verified again 2026-09-23, and the Alpine row re-verified 2026-09-13 on all five
branches — the stables it had never named (the remaining rows still carry the 08-30 stamp):
- Arch
extraand Homebrew — 2.7.3 (Arch’s package revision is2.7.3-1). Still the same version on both, re-checked 2026-09-23 (dotfiles-Arch#187). - openSUSE Tumbleweed and nixpkgs — 2.5.1, still current there. (These two shared a line with Arch and Homebrew while all four sat at 2.5.1; the split is what that line looks like once two of the four move and two do not.)
- Alpine
community— 2.5.1-r0 onedge; the supported stables lag (v3.24 2.3.2-r1, v3.23/v3.22 2.3.2-r0, v3.21 2.2.0-r0). A native musl build on every branch. This line used to read2.5.1-r0unqualified — the fleet-wide shape ²⁴’s table avoids by namingedge/community— and dotfiles-Alpine#188 caught it. - Gentoo: GURU carries 2.5.0, and there is no
::gentooatom — but the cell readscargo²⁵, notGURU, becausedotfiles-Gentoodoes not emerge that atom: itcargo installswatchexec-cliin its opt-in extras block instead, for upstream-latest. That is why this is not in ¹²’s GURU list — that footnote enumerates what theguru_installpass actually emerges. It is no longer the same shape asgping¹⁹, which is emerged from GURU; watchexec is now the one GURU-available tool the Gentoo bootstrap deliberately routes around. - Fedora and Debian/Kali do not package it at all (confirmed: Fedora’s package search
returns no results across F43/44/45/Rawhide/EPEL). Those two take
cargo install --locked watchexec-cli— note the crate iswatchexec-cli; plainwatchexecon crates.io is the library, and installing that gives you no binary.
Do not read Kali’s cargo cell as a ³: no bootstrap.sh installs it there.
Gentoo’s cargo cell is the exception — its bootstrap does install watchexec-cli, in
the opt-in extras block --no-extras skips. Either way maint/dotfiles-maint.sh runs
rustup update but has no cargo install-update step, so a cargo-installed watchexec
is never refreshed by the maintenance job — which bites the Gentoo box too, bootstrap or
not. That is already true of ouch/jj/ast-grep.
That gap now has a prescribed path: declare the tool as a mise backend and the maintenance
job already runs the upgrade. maint/dotfiles-maint.sh runs mise upgrade --yes and
rustup update and has no cargo/go re-install step, so roughly fifteen tools across the stack
— viddy, yazi, ouch, jj, ast-grep, jnv, watchexec, tealdeer, dust, sesh,
doggo, gron, shfmt, glow, yq, duf, plus carapace/starship/atuin ²⁷ — are
installed once and then never refreshed on up to eight machines. Declaring them under
[tools] with a cargo: / go: / ubi: backend makes the step that already runs do the
work, and lockfile = true records the exact resolved versions and checksums — a strictly
better trust anchor than the unsigned release-URL route ²⁷ warns about. examples/mise.tools.toml
is the worked global-tools example.
Two caveats that decide whether this works rather than merely resolves. Alpine must take
cargo:, never ubi: or aqua: — those prebuilts are glibc-linked, the exact trap
mise/config.toml documents for foundry. And ouch on Alpine must force a source build
without default features, per ¹⁴ — the default set cannot build on musl at all.
Core’s share of this is the example and this paragraph. The [tools] declarations
themselves are per-OS-repo work, which is the correct layering: which backend a given box
needs is an OS question, and Core cannot answer it for eight of them at once.
²⁶ git-absorb: OPT-IN — works out which earlier commit each staged hunk belongs to and
writes the fixup! commits for you; git rebase -i --autosquash then folds them in, and
git/gitconfig already sets rebase.autosquash = true, so the second half is automatic.
It is the automatic counterpart to git fix (commit --fixup, git/gitconfig’s
[alias] block), which is the manual form: with git fix <sha> you name the target
commit yourself, and git absorb works one out per hunk. Reach for git fix when you know
where a change belongs and git absorb when you would otherwise go looking.
The house-style ideal for a new tool: it needs no alias at all. git-absorb installs as
git-absorb — on PATH in the common case, in git’s exec-path on the Debian family (see
below) — and git dispatches it as the git absorb subcommand either way, so it shadows
nothing classic and zsh/20-aliases.zsh gains no entry, only a note saying why.
HAVE_GIT_ABSORB is set for symmetry with the other detected tools and has no alias consumer
today — core-doctor probes the tool itself rather than reading the flag, so the two are
independent paths to the same question and are kept in agreement deliberately (#425).
Debian-family packages do install it into git’s exec-path rather than onto PATH — on
the two boxes anyone has actually checked. Kali, git-absorb 0.6.17-2+b4, verified
2026-08-17: dpkg -L git-absorb lists /usr/lib/git-core/git-absorb and a man page and
nothing else, command -v git-absorb finds nothing, and git absorb --version works.
Ubuntu 24.04, 0.6.11, from the reporter’s dpkg -L in #424. Debian proper is
unverified — its package page lists 0.9.0-2 and nobody has looked at where that build
lands, so read the heading as the packaging convention plus two confirmations of it, not as
a survey of the family. The convention itself is standard for a git-<verb> subcommand and
not an oddity: git finds it via --exec-path, the user invokes it as git absorb, and it
is intentionally absent from PATH.
This paragraph used to say no mainstream package did that, and #424 is what the wrong claim
cost — core-doctor reported ✗ git-absorb on boxes where the tool was installed and the
only supported way to call it worked, so a reader would go install what they already had.
Both sides now look past PATH:
core-doctorresolves anygit-*row throughgit --exec-pathwhen the bare name misses (zsh/30-functions.zsh,_core_git_exec_path+ thegit-*arm of_core_doctor_bin). One fork, only on a miss, cached per report, and the resolved absolute path is what the-vversion readout then executes.HAVE_GIT_ABSORBfalls back to a zero-fork stat. An exported$GIT_EXEC_PATHis probed exclusively, because that is what it means to git — it replaces the compiled-in exec-path rather than adding to it, and an unexported parameter of that name never reaches git at all, so it is ignored here too. With no such override it stats<git-prefix>/{lib,libexec}/git-core, the prefix derived from zsh’s builtin$commandshash rather than hard-coded.zsh/00-tools.zshstill forks nothing at shell start, which is the constraint that made the original PATH-only probe look reasonable.
Where the two can still differ: a git built with its libexec outside its own prefix, with
the subcommand not linked onto PATH. core-doctor is authoritative there — it asks git —
and the flag is a best-effort approximation.
Arch, Alpine, Gentoo and Homebrew land the binary on PATH (Homebrew in its own prefix
bin, /opt/homebrew/bin on Apple silicon and /usr/local/bin on Intel), so those boxes
never reach the fallback — but that is package-page evidence, not an on-box check, so treat
it with the same caution as the version stamps below.
No bootstrap.sh installs it, and it is packaged essentially everywhere — the ²¹ shape,
not jnv¹⁷’s. The MacBook Brewfile carries it, and so do dotfiles-Alpine
(git-absorb) and dotfiles-Gentoo (dev-vcs/git-absorb) in their install/packages.txt;
the other four Linux repos are detect-only (re-verified 2026-08-21). Package versions verified
2026-08-12 against each distro’s own package pages, the Alpine row re-verified 2026-09-13 on
all five branches (dotfiles-Alpine#188):
- Arch
extra0.9.0-2, Alpinecommunity0.9.0-r0 onedgeand v3.24 (v3.23/v3.22 0.8.0-r0, v3.21 0.6.16-r0), Gentoodev-vcs/git-absorb0.9.0 (stable on amd64, in the main tree — no GURU needed), Homebrew 0.9.0. - Debian
git-absorb0.9.0-2 per packages.debian.org, but Kali rolling ships 0.6.17-2+b4 — verified on-box 2026-08-17. The two now have their own columns above, and this is a case where that matters: Kali is a laggard here rather than a Debian follower. Whether Debian’s own 0.9.0-2 also installs into git’s exec-path is not verified — only the Kali build is, so the ²⁶ footnote’s on-box evidence covers the Kali column. Note repology reports the source package asrust-git-absorb; the binary package you install isgit-absorb, confirmed on packages.debian.org. Fedora is the same shape (rust-git-absorbsource,git-absorbbinary). - openSUSE Tumbleweed is the other laggard, also at 0.6.17 — the gap #394 flagged, confirmed here rather than left as a repology snapshot. Re-check on the next openSUSE stamp.
²⁷ carapace: go install cannot work here, on any platform, for any published version.
This row used to read go³, and following that footnote fails on every box. It is the one
row where ³’s go install arm is not merely stale but impossible, so it gets its own paths.
Two independent blockers, both properties of how the module is built rather than a broken
build waiting to be fixed:
carapace-bin’sgo.modcarries tworeplacedirectives (spf13/pflag→carapace-sh/carapace-pflag,kevinburke/ssh_config→carapace-sh/ssh_config), andgo install pkg@versionrefuses any module that does — areplacewould make the build differ from building that module as the main module: “The go.mod file for the module providing named packages contains one or more replace directives. It must not contain directives that would cause it to be interpreted differently than if it were the main module.”- The generated sources are not committed:
pkg/actions/actions_generated.goandpkg/conditions/conditions_generated.goare absent from a fresh clone, andcmd/carapace/main.go’sgo:generatelines produce them.
Blocker 1 kills go install; blocker 2 kills the obvious workaround (go build ./cmd/carapace
on a clone) until go generate has run.
The scope is every version, not just the current one — which matters, because go install
takes any @version you name and the obvious next move on a failure is to pin an older one.
Don’t: it fails identically. Checked exhaustively over the whole tag history on 2026-08-15
(git clone --bare --filter=blob:none, then git show <tag>:go.mod for each of the 184 tags
from v0.0.3, 2020-08-31, through v1.7.3, 2026-06-30):
- 184 of 184 tags carry a
replacedirective. Zero exceptions. The count rose from one to two at v1.6.0, which is immaterial — one is enough. - 0 of 184 tags commit
pkg/actions/actions_generated.go. It has never been in the tree.
So this is not an extrapolation from the current release: in nearly six years of tags there has
never been a version you could go install. Upstream’s own .goreleaser.yml runs
go generate ./cmd/... as a pre-build hook and the AUR’s from-source carapace PKGBUILD does
the same — this is the intended build shape, not an oversight. Upstream could drop the
replace directives or start committing the generated sources in a future release, and this
row can be revisited if they do, but that would reverse six years of practice: treat it as a
change to watch for, not one to assume. Re-check only on an announced change, and verify
with an actual go install before believing it.
The route is the upstream release artifact, which goreleaser publishes per arch as
.rpm, .deb, .apk and .tar.gz (v1.7.3, 2026-06-30). Per target:
-
openSUSE — the
linux_<arch>.rpm. This is exactly the routedotfiles-Fedora’sbootstrap.shalready ships and has proven end-to-end; port that block, don’t re-derive it. One difference from dnf that will bite: upstream signs nothing (there is nosigns:stanza in.goreleaser.yml), and while dnf4 installs an unsigned local/URL rpm without complaint,zypper -naborts on one — so a non-interactive install needs--no-gpg-checks. Same shape for Alpine’s.apk(--allow-untrusted) if you ever need it.Be clear-eyed about what that flag gives up rather than pasting it as boilerplate: with no signature to check, the only trust anchor left is HTTPS to the GitHub release origin, and you are installing as root. Upstream does publish a
checksums.txt, but it is unsigned and served from the same origin as the asset — it catches a truncated or corrupted download, not a compromised release, so it is not a substitute for a signature. A repo that wants a real anchor should pin the version and record the SHA-256 in its own tree, which is precisely the shapescripts/tool-versions.env+scripts/update-tool-checksums.shalready use for the gate toolchain (.github/actions/setup-core-toolsverifies before installing). That is a heavier contract than any other bootstrap-installed tool carries today — starship and atuin come from upstream installer scripts, which verify less — so it is a deliberate per-repo call, not something this row silently mandates. -
Kali/Debian — the
linux_<arch>.deb, same asset set (amd64/arm64).apt-get installwants a path, not a URL, so this is curl-to-a-tempfile thenapt-get install ./carapace-bin_*.deb(which resolves deps, unlike baredpkg -i). -
Arch — the AUR, and the package name matters:
carapace-bin(1.8.0-1,provides/conflictscarapace, covers x86_64/aarch64/i686) just unpacks the upstream tarball, while the AUR also carries a from-sourcecarapacethat is x86_64-only and needs a Go toolchain. Preferparu -S carapace-bin. Note the same exception ¹⁶ records for viddy:dotfiles-Arch’s bootstrap builds no AUR helper, so on a bare box this is a manual step — or lift the binary straight out of the upstream.tar.gzinto~/.local/bin. -
Alpine (
carapace,community) and Gentoo (app-shells/carapace, GURU per ¹²) need no fallback at all — both are genuinely packaged and both bootstraps install them. Those two cells carry no ²⁷ because they are verified fine, not because they were skipped.
What the release-URL route costs you, stated plainly: installing from a release URL adds
no repo, so nothing upgrades carapace afterwards — not zypper dup/apt upgrade, not
maint/dotfiles-maint.sh, and not a later bootstrap either (a command -v carapace guard
short-circuits the whole block once the binary exists). Upstream ships no rpm/apt repo and
neither distro packages it, so there is no upgrade source to point at; updating is a
deliberate manual step and carapace --version is how you would know you are behind. That is
the real cost, and it is still the right trade: go install cannot work at all, so the choice
is a manually-updated binary or no carapace. Arch is the exception — carapace-bin from the
AUR is a normal package that paru -Syu refreshes.
Building from source is the escape hatch for an arch with no published artifact. Verified end-to-end in #416:
git clone --depth 1 https://github.com/carapace-sh/carapace-bin.git
cd carapace-bin
go generate ./cmd/... # writes the *_generated.go files
go build -o ~/.local/bin/carapace ./cmd/carapace
Two caveats. Size: this is a big binary either way — 500+ bundled completers put
upstream’s released amd64 build at 81.6 MB on disk (from a ~14 MiB download), and a plain
go build lands nearer 114 MB because it is unstripped and skips upstream’s -tags release;
add -ldflags='-s -w' -tags release to close most of the gap. Version: the build will
self-report carapace-bin develop no matter what you clone. That is not a shallow-clone
artifact, as it first appears — cmd/carapace/main.go initializes var version = "develop"
and nothing in the build derives it from git, so a full clone and a checked-out tag report
develop too. The version is injected at link time or not at all; pass it yourself, as both
upstream’s goreleaser and the AUR PKGBUILD do:
-ldflags="-s -w -X main.version=v<tag>".
Core’s side is unchanged by any of this: zsh/00-tools.zsh sets HAVE_CARAPACE, and
zsh/45-plugins.zsh runs carapace _carapace zsh through _cache_eval after compinit
(zsh/10-options.zsh), feeding fzf-tab. Inert without the binary.
All three OS repos that called the impossible go install have been fixed, and this
footnote is the contract each was fixed against. dotfiles-Arch/bootstrap.sh prints a
paru -S carapace-bin hint instead: Arch is the one target with a real, upgradable package,
and hand-placing the upstream binary in ~/.local/bin would silently shadow it forever.
dotfiles-openSUSE/bootstrap.sh installs upstream’s linux_<arch>.rpm release asset via
zypper (--no-gpg-checks --allow-unsigned-rpm), accepting that nothing upgrades it after.
dotfiles-Offense installs no carapace at all: it stopped being an OS-native layer, and the
Debian family it used to cover is dotfiles-Debian’s job now.
²⁸ Pinned upstream release asset — on Debian/Ubuntu, on Kali, or on both.
dotfiles-Debian installs this from a version- and SHA-256-pinned GitHub release asset via
bootstrap.sh’s verified_install (pins in its install/tool-versions.env, refreshed by
scripts/update-tool-checksums.sh). It is fail-closed: a missing pin, failed
download, or hash mismatch skips that tool loudly rather than installing anything
unverified.
The two columns reach this cell for opposite reasons, which is why the marker now
appears in both. On Debian/Ubuntu it is scarcity: Ubuntu 24.04 either has no package at
all or has one below the version Core needs. On Kali it is more often the reverse —
verified_install is command -v-guarded, so on a tool apt does supply it no-ops and the
apt name wins; the asset fires only where kali-rolling genuinely lacks the name (delta,
hexyl — the two apt has on noble and trixie but not on kali-rolling), or where the repo
takes the pinned asset on every target regardless of the archive (mise, uv, and the
rest of the verified_install block). That last case is the one that reads wrong: a cell
saying uv was defensible from kali-rolling’s contents and still false about what gets
installed. A cell records what this fleet installs.
Neovim is the exception in shape — its asset is a directory tree
(bin/ + lib/ + share/nvim/runtime), so it uses verified_tree_install and lands
in ~/.local/opt with a symlink into ~/.local/bin; copying the bare binary yields an
editor whose $VIMRUNTIME points at nothing. Deliberately not the AppImage: Ubuntu
24.04 sets kernel.apparmor_restrict_unprivileged_userns=1, which blocks the
unprivileged user namespace an AppImage needs to mount itself.
²⁹ Not installed on dotfiles-Debian. Unlike a ³/cargo³ cell, this is not “you
install it by hand” — it is a decision. yazi, viddy and ast-grep are cargo-only and
noble’s rustc is 1.75, too old to build them; shipping a toolchain that fails
halfway through a long build is worse than not shipping the tool. jnv, ouch,
jujutsu and watchexec are likewise unpackaged there and not worth a hand-maintained
pin. All are HAVE_*-guarded in Core, so the shell degrades cleanly. Want one on a
particular box? mise use -g rust, then cargo install --locked <tool> — and as of #425
the next login picks it up on its own, no symlink into ~/.local/bin needed.
³⁰ mise and uv — the two the doctor probes but this table used to omit.
core-doctor reports on both (_CORE_DOCTOR_GROUPS in zsh/30-functions.zsh) and both
get a HAVE_* flag, so a ✗ for either sent the reader to a matrix with no row to find —
the one promise zsh/30-functions.zsh’s “install missing” hint makes. mise is the
chicken-and-egg row: footnotes ¹, ⁶, ⁷, ¹⁰, ¹¹ and ²⁹ all prescribe mise use -g <x> as
a fallback, and every bootstrap.sh reaches for mise exec go@latest when no Go toolchain
is present, so it is a prerequisite of this table rather than an entry in it. Only Arch
packages it (extra); openSUSE and Gentoo have none, and the bootstraps there use the
official installer (curl -fsSL https://mise.run | sh, landing in ~/.local/bin). Alpine
does carry mise in community, but dotfiles-Alpine still takes mise.run for the musl
build — hence script³⁰, not a package name, in that cell. Kali used to be in that
sentence and no longer belongs there: its bootstrap is now dotfiles-Debian’s, which never
calls mise.run at all — it takes the pinned MISE_VERSION asset on every target, Kali
included, so that cell is asset²⁸.
uv used to be filed here as the cleanest illustration of why the Kali and Debian/Ubuntu
columns had to be split at all (dotgibson/dotfiles-core#431), on the grounds that
kali-rolling ships uv 0.9.17 while Ubuntu 24.04 ships nothing — uv reached Debian only
in sid/trixie. Both halves of that are still true about the archives, and it is no longer
the right reading of the row: uv is absent from the only:kali tier and
dotfiles-Debian’s bootstrap.sh fetches the pinned UV_VERSION asset on every target,
so both columns are asset²⁸ and neither is derived from apt. The split earns its keep
elsewhere (difftastic, git-delta, hexyl — names apt has on noble and trixie and not on
kali-rolling); uv is instead the cleanest illustration of the failure the ²¹ᵃ caveat above
warns about, a cell that recorded what the archive contains rather than what the repo
installs. openSUSE’s is python3-uv, and python-uv — what this cell used to say —
is the source package: no binary of that name is published, so zypper in python-uv cannot
resolve on Tumbleweed or on Leap 16.0/16.1. The binary is Python-flavored (python313-uv), and
python3-uv is the flavor-agnostic Provides that tracks whichever flavor is primary. Bare
uv is a Provides too, but on Tumbleweed it is ambiguous — python313-uv and
python314-uv both provide it, so zypper in uv makes you pick — whereas python3-uv has
exactly one provider on all three targets. Verified against the OBS published-binary index on
Tumbleweed (openSUSE:Factory, 0.12.7) and Leap 16.0/16.1 (SUSE:SLFO:1.2/1.3, 0.7.18), so
no ³ fallback is needed there. The cell carries ²¹ because dotfiles-openSUSE does not
install it — the name is where uv comes from when you opt in, not something the bootstrap
puts on the box (#836). Arch, Alpine and Gentoo do install theirs, which is why only this
column is marked. This is the same failure the paragraph above describes:
python-uv is the name the archive contains, not the one the repo installs. Gentoo’s
is dev-python/uv, not a bare uv.
mise also appears in this table as a provider, in exactly one cell: gum on Gentoo.
Charm’s gum is packaged nowhere Portage can reach — not ::gentoo, not GURU, in any category
(see ¹²) — so dotfiles-Gentoo declares it in gentoo/mise-tools.toml, which mise installs as
a prebuilt, checksum-verified binary with no compiler and no privileges. Read that cell with
one caveat: that manifest is installed only on the --user (no-root) path, so a
privileged Gentoo bootstrap installs no gum at all. On that path it is a hand-install —
mise use -g gum, or go install charm.land/gum/v2@latest (the module path, not the
GitHub URL — see ³¹).
³¹ go install module paths — the repo URL is usually NOT the module path. Five of the
seven go-installable rows need a major-version suffix, a cmd/ subpath, a different HOST, or a
combination, and a naive go install github.com/<org>/<repo>@latest fails or silently builds
an abandoned major.
Verified against each project’s own go.mod, and these are the exact strings the fleet’s
bootstrap.sh files already pass to _dotfiles_go_install:
| Tool | go install <path>@latest |
|---|---|
doggo | github.com/mr-karan/doggo/cmd/doggo |
sesh | github.com/joshmedeski/sesh/v2 |
yq | github.com/mikefarah/yq/v4 |
shfmt | mvdan.cc/sh/v3/cmd/shfmt |
gron | github.com/tomnomnom/gron |
duf | github.com/muesli/duf |
glow | charm.land/glow/v3 |
Charm’s tools moved off GitHub as a module host — glow is now charm.land/glow/v3
(v3.0.0, 2026-08-11) and gum is charm.land/gum/v2 (v2.0.0). Both still live on GitHub;
only the module path changed. #431 reported github.com/charmbracelet/glow/v2, which was
right when it was filed and is now two majors stale — a good reason to re-read go.mod
rather than trust a remembered path. dotfiles-Alpine go-installs glow — glow is
testing-only on Alpine¹⁴, so go install is its real source there, and that call carried the
stale github.com/charmbracelet/glow/v2 until dotfiles-Alpine#122 moved it to
charm.land/glow/v3. It resolved either way, which is precisely how it went unnoticed:
@latest on the old path quietly pins the newest v2 tag instead of failing. gum is
go-installed by no bootstrap.sh (the Debian/Kali cells use Charm’s apt repo, see ¹⁵), so that
one is for the reader installing by hand.
³² direnv: per-directory environment loader — Core wires it but neither installs nor
detects it. There is no HAVE_DIRENV and no alias: _cache_eval already bails on an
absent binary, so the hook needs no flag to guard it. Since #581 core-doctor does carry a
wired row for it (probing _direnv_hook), but still no presence row — wiredness and
presence are different questions, and the latter is what would need detection. Since v4.14.1
the direnv hook zsh that makes it work lives in Core, at zsh/00-tools.zsh band 00,
where #449 pulled seven byte-drifted os/*.zsh copies up into one. Band 00 and not 45,
because it registers a hook rather than a compdef, so it needs nothing from compinit and
belongs beside the other per-directory hook inits. (Until v5 it had a sharper reason too:
band 45 was ceilinged out of CORE_PROFILE=minimal, so filing it there stopped .envrc
files loading outright on lean hosts; #677 deleted the profile and that half went with it.)
(Since #579 the gh/uv/ty completions are generated
at band 00 too, but for a different reason — an fpath directory has to be populated before
compinit scans it — and they keep a compdef re-assert at band 45.) It is sourced last of the
four inits on purpose: direnv prepends _direnv_hook to precmd_functions and
chpwd_functions, so sourcing it after mise reproduces the order these hooks had at band 80
— direnv’s per-directory env resolves before mise’s, which is what an .envrc that pins tool
versions expects. An OS repo that still carries the old band-80 block is harmless but
redundant, and drops out on its next Core sync.
Core’s other stake is starship/starship.toml’s [direnv] module, which Core switches on
(disabled = false; starship ships this module off by default): it renders .envrc state
on the srf1 band so a directory waiting on direnv allow is visible rather than silently
unloaded. The module only draws inside a direnv-controlled tree, so a box without the binary
loses a segment, not the prompt.
Installed, not merely available — the inverse of ²¹. Six of the seven package lists carry
it outright (dotfiles-Arch, -openSUSE, -Alpine, -Debian, -Fedora, and the MacBook
Brewfile), and dotfiles-Gentoo emerges app-shells/direnv in its guru_install pass per
¹². dotfiles-Offense (Kali) is the single gap, and it is structural rather than a call
about direnv: that repo carries no install/packages.txt, so nothing there installs the
package. It does now get the hook — that arrives from Core at band 00 like everywhere
else, which is a change from the band-80 arrangement, where Offense missed it too for having
no os/ layer at all. So the hook is live there and simply finds no binary. The Kali cell
above is the apt name you would install by hand.
Verified 2026-08-21 against each distro’s own index, the Alpine stamp re-verified 2026-09-13
(dotfiles-Alpine#188): Arch extra 2.37.1-1, Alpine community 2.37.1-r8 (v3.24 and
edge; v3.23 still carries 2.37.1-r7 — a Go binary, so a native musl build), openSUSE
Tumbleweed 2.37.1 with Leap 16.0 and 16.1 both at 2.34.0 through Backports (bp160.1.13 /
bp161.1.9, both arches), kali-rolling 2.37.1-1, Ubuntu 24.04 universe
2.32.1-2ubuntu0.24.04.3 and Debian trixie 2.32.1-2+b16. Gentoo is GURU-only — 2.37.1,
~amd64 ~x86, no ::gentoo atom and no dev-util/direnv; see ¹². Where unpackaged, the
module path is github.com/direnv/direnv/v2 — the /v2 is not optional, the ³¹ trap again.
One version line, because it is the only place a frozen archive touches Core: starship runs
direnv status --json, and the --json flag is silently ignored below direnv 2.33.0 —
starship’s own src/modules/direnv.rs says exactly that and falls back to parsing the text
output. Every target above clears that floor except dotfiles-Debian’s two lanes, both on
2.32.1. It degrades rather than breaks, which is why that repo’s install/packages.txt
declares no # min: floor for it.
³³ neovim — “the package exists” is not “the package is usable”, and it bites on FIVE
targets, by four different mechanisms. The editor pins nvim-treesitter to main, which
hard-requires Neovim 0.12. Several cells in the neovim row above resolve perfectly and
give you something the editor will not load on.
Where that floor comes from, since #1123. It is a property of
dotfiles-nvim, which owns the editor and
authors the pin; nvim/lazy-lock.json in this repo is a vendored copy of it, and audit
§9q fails on a hand-edit, so the floor cannot be raised or lowered from here. The matrix
keeps rendering it because the fleet still has to package a Neovim that satisfies it —
that half is an OS-repo problem and is what the rest of this footnote is about. A floor
change therefore arrives as a dotfiles-nvim release, reaches Core when nvim.lock moves
at a Core release, and lands in this table on the next refresh.
Fleet position, generated from scripts/fleet-package-versions.tsv — the same mechanism
footnote ³⁴ uses for jq, and here for a blunter reason: this enumeration was hand-written prose
and was wrong four times in four months (dotfiles-Gentoo#116, dotfiles-Alpine#170,
dotfiles-openSUSE#178, dotfiles-Fedora#192). The side of the floor each row falls on is DERIVED
from its version, so a row cannot assert a verdict its own number contradicts — which is what
two of those corrections were.
| Target | neovim | vs ≥ 0.12.0 | verified |
|---|---|---|---|
| Arch | 0.12.5 | at or above | 2026-09-17 |
| openSUSE Tumbleweed | 0.12.5 | at or above | 2026-09-17 |
| Fedora Rawhide | 0.12.5 | at or above | 2026-09-17 |
| Fedora 45 | 0.12.5 | at or above | 2026-09-17 |
| Fedora 44 | 0.12.5 | at or above | 2026-09-17 |
| Homebrew | 0.12.5 | at or above | 2026-09-17 |
| openSUSE Leap 16.1 | 0.12.4 | at or above | 2026-09-17 |
| Alpine edge | 0.12.2 | at or above | 2026-09-17 |
| Alpine 3.24 | 0.12.2 | at or above | 2026-09-17 |
| Gentoo stable | 0.11.7 | below | 2026-09-17 |
| Alpine 3.23 | 0.11.7 | below | 2026-09-17 |
| Fedora 43 | 0.11.6 | below | 2026-09-17 |
| openSUSE Leap 16.0 | 0.11.3 | below | 2026-09-17 |
| Alpine 3.22 | 0.11.1 | below | 2026-09-17 |
| Debian 13 | 0.10.4 | below | 2026-09-17 |
| Alpine 3.21 | 0.10.4 | below | 2026-09-17 |
| Ubuntu 24.04 | 0.9.5 | below | 2026-09-17 |
Four things the table deliberately does not say. The Debian lane is the Ubuntu 24.04 row,
and its remedy is footnote ²⁸ rather than a newer archive — Debian 13’s own neovim is 0.10.4,
also below the floor. The Gentoo row is the newest stable-keyworded ebuild; 0.12.0
through 0.12.5 are all in ::gentoo and all ~arch, so a stable profile silently picks 0.11.7
and reports success, and the version-restricted >= line below reaches past it — a remedy, not
an observation, so it is not a row. Distro build suffixes (-r0, -bp160.2.1, -1.fc43) are
dropped: the table records the upstream triple, which is what the floor is compared against and
what an upstream probe can confirm. And verified is when somebody last looked, not when
the version last moved — a row unchecked for 90 days is named on stderr by
make gen-porting-matrix.
They get there by four different mechanisms and only one of them looks like a problem.
Debian’s is a frozen archive: the version is simply old, apt says so, and
dotfiles-Debian declares a # min:0.12.0 floor its CI enforces. Gentoo’s is
keywords: 0.12.0–0.12.5 are all in ::gentoo right now, all ~arch, so a stable
profile silently picks 0.11.7 and reports success. Nothing in an availability check can
see it — the atom exists, installs, and is the wrong version.
Alpine’s is branch spread, and it is the one this file got wrong for a full release
cycle. Alpine is not rolling: it carries four supported stable branches at once plus edge,
each frozen at the version it released with, so “does apk add neovim clear the floor?” has
no single fleet answer — it has five, and three of them are no. A check run on a v3.24
or edge box sees a perfectly current 0.12.2 and reports the row healthy for Alpine
entirely. Sibling footnote ⁵ carries the matching spread for tree-sitter-cli, now from the
same TSV — same five lanes, same floor arithmetic, one dependency apart. Both used to be
prose, and both flattened those five lanes into one number at least once.
openSUSE Leap is Alpine’s shape with two lanes instead of four. Leap 16.0 and 16.1 are both
supported, each frozen at the Backports build it released with (bp160 0.11.3, bp161
0.12.4) while Tumbleweed rolls (0.12.5). A check that samples Tumbleweed or 16.1 reports the
openSUSE column healthy, and a 16.0 box gets a neovim that installs cleanly and will not
load Core’s config. Neither Debian’s nor Gentoo’s lever exists there: no newer build in
16.0’s OSS or Backports repos, and no keyword to reach past. An earlier revision of this
footnote exempted openSUSE by name — “though its neovim row is not currently affected” —
which was true of 15.6 and stopped being true the day 16.0 shipped.
Fedora is the fourth mechanism, and the only one that moves in the fleet’s favour on its
own. It is neither frozen nor keyworded nor branch-spread: Fedora rebases inside a
release, for some packages and not others, at the maintainer’s discretion. neovim
crossed 0.11 → 0.12 in F44’s updates, so a box that installed 0.11 at GA is on 0.12.5
today without changing release — while F43 stayed on the 0.11 branch and ends its life
there, at 0.11.6. F43 therefore clears the floor only by upgrading release, which is
the mechanic footnote ³⁴ records for jq on this same distro and the exact inverse of
Alpine’s in-place backport — same floor, opposite levers, one distro apart. Two things
follow. A check run on F44, F45 or rawhide sees 0.12.5 and reports the Fedora column
healthy, exactly as sampling Tumbleweed does for openSUSE. And F43 is a blocking lane
in dotfiles-Fedora’s .github/workflows/packages.yml, so this is a shortfall on a
release that repo’s own CI treats as supported.
dotfiles-Gentoo therefore borrows Debian’s contract and pairs it with the Portage-native
fix: # min:0.12.0 next to the atom in install/packages.txt, a version-restricted
>=app-editors/neovim-0.12.0 ~__ARCH__ in gentoo/package.accept_keywords (restricted so
0.11.x keeps tracking stable), and a check in scripts/check-packages.sh that fails when a
declared floor is not reachable. Filed as dotfiles-Gentoo#116, verified 2026-08-23.
The keyword line is not enough on its own: every 0.12.x ebuild needs >=dev-lua/luv-1.52.1,
which is also ~arch only (newest stable 1.50.0.1), so the same file carries a second
>=dev-lua/luv-1.52.1 ~__ARCH__ line. luv is maintainer-needed as of 2026-09-27 — the
same hedge lnav carries, since orphaning is what precedes a treeclean, and here it
sits one level under the editor. A watch item, not a break (dotgibson/dotfiles-Gentoo#209).
dotfiles-Alpine has no keyword or archive lever to pull — there is no newer branch to
point apk at without moving the whole box, and neovim’s own releases are glibc-linked
AppImages that will not run on musl — so it takes the remaining option: bootstrap.sh
declares NEOVIM_FLOOR="0.12.0" beside the TREESITTER_FLOOR it already had, and warns
when the installed nvim is below it rather than pretending to fix it. Worth naming what that
repo actually had: a version guard on nvim-treesitter’s dependency (tree-sitter-cli) and
none at all on nvim-treesitter’s host, failing on the same three branches, for as long as
the floor has existed. Half a requirement checked reads exactly like a whole one.
Filed as dotfiles-Alpine#170, verified 2026-09-06.
dotfiles-openSUSE takes Alpine’s remedy for Alpine’s reason: # min:0.12.0 on the
manifest line — which is where the ≥ 0.12.0 in the openSUSE cell above is derived from — a
warn-only NEOVIM_FLOOR in bootstrap.sh compared with zypper versioncmp, and a floor
gate in test/check-packages.sh that fails a Tumbleweed shortfall (that would mean Core’s
pin outran the fleet) and reports a Leap one. Filed as dotfiles-openSUSE#178, verified
2026-09-12.
dotfiles-Fedora takes Alpine’s remedy too. It had repeated Alpine’s asymmetry exactly:
a prose floor for tree-sitter-cli, and a bare neovim line with no version check. It now
has the # min: pair (neovim # min:0.12.0, tree-sitter-cli # min:0.26.1), a warn-only
NEOVIM_FLOOR in bootstrap.sh, and the version-checked tree-sitter fallback from ⁵. A
floor-agreement gate in test/check-packages.sh fails if a # min: disagrees with the
bootstrap.sh constant it restates. F43 is still below both floors (neovim 0.11.6,
tree-sitter-cli 0.25.10); F44 and rawhide clear them (0.12.5, 0.26.11). The package table
above has no Fedora column, so the remedy moves no cell. Filed as dotfiles-Fedora#192, fixed
by dotfiles-Fedora#193. Verified 2026-09-16 against packages.fedoraproject.org and
mdapi.fedoraproject.org, and re-verified 2026-09-26 by dotfiles-Fedora#203’s mdapi
sweep. The 2026-09-16 check followed #1010, which reported the same shortfall from a
fedora-bootc:42 container — a release EOL since 2026-05-13, and not one of this
fleet’s Fedora lanes. Measure the lanes the repo declares, not the image that happened
to be handy: that container was pinned for a research harness’s reasons, and its package
versions were read as if they were the distro’s.
If you stamp a new target that is not rolling, ask the keyword question, the branch question and the rebase question, not just the name question. This trap only shows up on the fleet’s non-rolling lanes — and “non-rolling” covers four shapes, not one: a frozen archive (Debian), a stable/testing keyword split (Gentoo), a set of concurrently supported release branches (Alpine, and openSUSE Leap), and a versioned release train that rebases inside a release for some packages and not others (Fedora). A rolling column can be answered once. Each of these has to be answered per lane, and a check that samples only the newest lane will report all of them healthy.
³⁴ jq — a recorded security floor of ≥ 1.8.2, and deliberately NOT a version gate.
1.8.2 (2026-06-20) fixes 16 CVEs — heap and stack overflows, out-of-bounds reads, an
integer overflow, a use-after-free, and a hash-collision DoS. Every one of them is reachable
through parsing input, which is the entire job of the tool. That matters here because jq
is pointed at output produced by machines other than yours: Core probes it
(zsh/00-tools.zsh, ledger row only — no flag since #694) and this file prescribes jq -e '.detection.missed == []' as the
provisioning gate a role layer runs against core-doctor --json ²⁰.
Fleet position, generated from scripts/fleet-package-versions.tsv — this enumeration was
prose until it was corrected twice in one day, once for Alpine and once for Fedora, because
nothing could contradict it. The side of the floor each row falls on is DERIVED from its
version rather than recorded beside it, which is precisely what both corrections were: a
version and a verdict that disagreed. Footnotes ⁵ and ³³ carry the same kind of block, from the
same file, for tree-sitter-cli and neovim (#1082) — so the three floors this document
enforces are now enumerated by one mechanism rather than three prose styles.
| Target | jq | vs ≥ 1.8.2 | verified |
|---|---|---|---|
| Arch | 1.8.2 | at or above | 2026-09-09 |
| Gentoo | 1.8.2 | at or above | 2026-09-09 |
| openSUSE Tumbleweed | 1.8.2 | at or above | 2026-09-09 |
| openSUSE Leap 16.1 | 1.8.2 | at or above | 2026-09-12 |
| Homebrew | 1.8.2 | at or above | 2026-09-09 |
| Alpine edge | 1.8.2 | at or above | 2026-09-09 |
| Alpine 3.24 | 1.8.2 | at or above | 2026-09-09 |
| Alpine 3.23 | 1.8.2 | at or above | 2026-09-09 |
| Alpine 3.22 | 1.8.2 | at or above | 2026-09-09 |
| Fedora Rawhide | 1.8.2 | at or above | 2026-09-09 |
| Fedora 45 | 1.8.2 | at or above | 2026-09-06 |
| Fedora 44 | 1.8.1 | below | 2026-09-09 |
| Fedora 43 | 1.8.1 | below | 2026-09-09 |
| Alpine 3.21 | 1.7.1 | below | 2026-09-09 |
| Debian 13 | 1.7.1 | below | 2026-09-09 |
| Ubuntu 24.04 | 1.7.1 | below | 2026-09-09 |
| openSUSE Leap 16.0 | 1.7.1 | below | 2026-09-12 |
The two mechanics behind that table are the part worth reading. Alpine backported
jq 1.8.2-r0 into main on three of its four supported stable branches (3.22–3.24; 3.21
still carries 1.7.1) rather than leaving them on the version they shipped with — exactly the
behaviour a # min: floor is supposed to reward.
Fedora reaches that floor by the opposite mechanic, and it is the one crossing with a date
on it. F45 branched from Rawhide on 2026-08-11 and goes GA 2026-10-20. F43/F44 sit at
1.8.1-3 with nothing in updates-testing, so they never cross at all: on this lane you clear
the floor by upgrading release, where Alpine’s stables cleared it in place. Same
floor, opposite mechanics, and neither is visible in jq --version — which is why this
footnote records a floor to act on when provisioning rather than prescribing a gate.
(Fedora re-verified 2026-09-06 against packages.fedoraproject.org/pkgs/jq/jq/; the other
lanes are as at time of writing.)
Do not build a guard on jq --version. On the Debian family the version string is not
evidence either way — Debian backports security fixes without bumping the version, so a
1.7.1-x build may carry all, some or none of these, and a version gate would false-positive
across the whole Debian/Kali/Ubuntu lane. openSUSE Leap is a second such lane: 16.0’s
1.7.1-160000.4.1 is the third maintenance rebuild of 1.7.1 (.2.2 → .3.1 → .4.1), the
last of them openSUSE-SU-2026:21318-1 for CVE-2026-49839, so the table’s below for
Leap 16.0 is a statement about the version string, not about which of the sixteen fixes the
build carries. Leap 16.1 clears the floor outright at 1.8.2 — and neither Leap release is
visible to Repology, which is why those two rows carry a - probe and are re-read by hand
rather than by make update-fleet-versions. This is a third shape, distinct from the two
other version-sensitive rows in this file, and the distinction is the point: ⁵ (tree-sitter)
mandates a version check because apk is honestly old, and ²² (sd) forbids one and
mandates capability probing because --version lies. jq’s version is neither honest nor
probeable — nothing in the CLI surface reveals which patches a build carries. So this is a
floor you record and act on when provisioning, not one you can detect from the shell.
Core itself is unaffected: nothing in Core shells out to jq (detect-only, no alias — the
same shape as sd and gron, both ledger-only probes since #694). This is a note for the role layers and for anyone
piping untrusted JSON through a distro jq.
³⁵ Fedora’s Workstation refresh is sudo dnf check-update, and it is not really a
refresh — dnf has no standalone index-refresh verb. Refreshing is a flag on the verb
that needs it (dnf upgrade --refresh), which is why the Workstation upgrade and
count-pending cells both carry it and why os/fedora.capabilities declares no
PKG_UPGRADE_PRE. The schema calls PKG_REFRESH “may be a no-op verb” for exactly this
case.
The mark sits on the whole cell because a footnote travels with the verb, but it is
about the Workstation half only: the Atomic edition genuinely has a standalone one.
rpm-ostree refresh-md refreshes the metadata and does nothing else, so there the declared
PKG_REFRESH means what the schema’s name says.
³⁶ macOS count-pending is two commands, and it is the only archive where that is true.
brew outdated reports against the locally cached formula index, so without a brew update first it reports a stale set — sometimes empty on a box with dozens of upgrades
pending. That is what the optional PKG_COUNT_REFRESH key exists for. Core runs it on the
count path only: the list path (up’s pre-confirm preview) deliberately skips it,
because the once-a-day nudge that produced the count already paid for the network.
³⁷ Gentoo count-pending is the real dependency calculation, and it is the one cell
here that cost a measurement to get right (#753 → #756). The cell renders
gentoo-pkg-pending, a wrapper dotfiles-Gentoo ships, not a bare Portage command — the
one row whose declared value is a script rather than an invocation, because the -1 sentinel
below cannot be expressed as a pipeline. The obvious answer is eix -u,
which is what Core shipped: it is fast and it reads its own cache. It also answers the
wrong question. eix -u asks “is a higher version present in the tree?”; up runs
emerge -uDN @world, which asks “what will actually change?” — and on a healthy box those
diverge permanently. Measured on a real machine: eix said 70 where emerge merged 8, and
after a full update and a depclean eix still said 2 against emerge’s 0. Three causes,
only the first of which an operator can ever clear — orphans (installed but no longer
reachable from @world), slots (the newest thing in SLOT 5.1 is not the newest across
all slots), and consumer pins (a dependent pinning =cat/pkg-1.2* the resolver will
not move past). The last two never clear, so no filter over eix’s output fixes this; only
the resolver knows.
The cost is real and was weighed rather than assumed: ~10s against eix’s 0.25s. The caller
that pays it is throttled to once a day and runs disowned, so it never blocks a prompt, and
up’s foreground use already sits behind a spinner. A once-a-day background resolve is
affordable; a nudge that cannot reach zero on a healthy box is not, because it stops
being a signal that anything needs doing. Root is not needed (--pretend installs nothing)
and it takes no merge lock, so it is safe beside a real emerge.
A failed resolve must report the -1 unknown sentinel, never 0 — a box whose Portage
cannot resolve is not a box with nothing to do. That is what the optional
PKG_COUNT_EXIT_TRUSTED key is for, and Gentoo is the only archive that declares it: most
overload the exit status of their count verb (dnf exits 100 when updates exist; pacman -Qu and checkupdates exit non-zero when there are none), so Core ignores it by default.
³⁸ macOS owns-file has no good answer and the cell is the least trustworthy in the table.
brew which-formula maps a command name to the formula providing it, which is not the
same question as “which package owns this path” that every other column answers. Homebrew
has no path-ownership index; the nearest real equivalent is grepping the install receipts
under the Homebrew prefix, which is neither a stable interface nor something Core may name.
Treat this cell as “the closest available”, and prefer not to build a gate on it.
³⁹ eza — every target packages it, but not every target packages the one Core would want
to use next. 0.23.5 (2026-07-09) added --hyperlink=auto and lines-of-code counting.
Plain --hyperlink is older; the =auto value is not, and an older eza rejects it
rather than ignoring it. That matters because the flag would land in ls/ll/la, the
most-used commands on the box, in the one file whose contract is graceful degradation. So
0.23.5 is not a floor Core enforces today — zsh/ passes no flag that needs it — it is the
floor .claude/tool-decisions.md’s eza --hyperlink=auto watch row is waiting on, recorded
here so that watch has a table to read rather than a guess.
| Target | eza | vs ≥ 0.23.5 | verified |
|---|---|---|---|
| Arch | 0.23.5 | at or above | 2026-09-24 |
| openSUSE Tumbleweed | 0.23.5 | at or above | 2026-09-24 |
| Fedora Rawhide | 0.23.5 | at or above | 2026-09-24 |
| Fedora 45 | 0.23.5 | at or above | 2026-09-24 |
| Fedora 44 | 0.23.5 | at or above | 2026-09-24 |
| Fedora 43 | 0.23.5 | at or above | 2026-09-24 |
| Homebrew | 0.23.5 | at or above | 2026-09-24 |
| Alpine edge | 0.23.4 | below | 2026-09-24 |
| Alpine 3.24 | 0.23.4 | below | 2026-09-24 |
| Alpine 3.23 | 0.23.4 | below | 2026-09-24 |
| Gentoo stable | 0.23.4 | below | 2026-09-24 |
| Alpine 3.22 | 0.21.3 | below | 2026-09-24 |
| Debian 13 | 0.21.0 | below | 2026-09-24 |
| Alpine 3.21 | 0.20.12 | below | 2026-09-24 |
| openSUSE Leap 16.1 | 0.20.4 | below | 2026-09-24 |
| openSUSE Leap 16.0 | 0.20.4 | below | 2026-09-24 |
| Ubuntu 24.04 | 0.18.2 | below | 2026-09-24 |
Read it as the watch’s answer: the watch ends when every row reads “at or above”. The long tail is the frozen and slow-moving lanes — Ubuntu 24.04, both openSUSE Leap backports, Debian 13 and the older Alpine stables — and the Alpine and Gentoo-stable rows one patch release short, which are the ones likely to move first.
⁴⁰ jc converts plain command output into JSON. ps, ss, dig, git log, ifconfig
and a few hundred others are parsed into JSON that jq can then transform. It sits in front
of the other JSON tools: jq transforms, gron greps and jnv explores, but none of them
can read ps aux. Like those three it is its own command with no alias, probed into the
_CORE_PROBED ledger by zsh/00-tools.zsh and listed in core-doctor’s data / net group
(#1208). It is a Python CLI: every packaged build puts jc on PATH and pulls in its module.
Every fleet repo installs it from its own package manager, including brew "jc" on macOS and
jc in NixOS’s home.packages. There are two exceptions, both marked:
- openSUSE Leap 16.x has no package in either
repo/ossorBackports:SLE-16.x; only Tumbleweed and the transactional editions ship it (1.26.0, checked 2026-09-29).dotfiles-openSUSEstill lists it, so zypper skips the name on Leap, thetealdeershape from ¹. There is no fallback, because Leap has nopipx/uvto build one with. Instead, Leap’sTOOLS_OPTINnamesjc, so core-doctor shows its absence as expected rather than as a failure (dotgibson/dotfiles-openSUSE#221). - Gentoo has only testing ebuilds (
dev-python/jc,~amd64 ~arm64 ~ppc64, no stable keyword). A stable profile needs thedev-python/jc ~__ARCH__line thatdotfiles-Gentoo’sbootstrap.shinstalls intopackage.accept_keywords(dotgibson/dotfiles-Gentoo#212).
Clipboard packages to install (backends for Core’s clip)
This table is deliberately NOT an
os.capabilitiessource, unlike the package-manager table above. #667 listed it as one; #663 had already decided otherwise and the schema has no clipboard key, soscripts/check-capabilities.shrejects one. The reason is inzsh/02-capabilities.zsh:bin/clipis re-exec’d by nvim and tmux on every yank and paste, and its WSL probe was already rewritten once to avoid forking agrepper invocation. Adding a file read and parse to that path would give back exactly what that bought — for a value that changes once per machine. The ladder stays hardcoded. Do not re-open this without a measurement.
| Distro | Wayland | X11 fallback |
|---|---|---|
| Arch | wl-clipboard (wl-copy/wl-paste) | xclip |
| openSUSE | wl-clipboard | xclip |
| Alpine | wl-clipboard | xclip / xsel (often headless — may be neither) |
| Gentoo | gui-apps/wl-clipboard | x11-misc/xclip |
| Kali (WSL2) | n/a — Core’s clip shells out to clip.exe | wl-clipboard/xclip install but sit inert under WSL |
| Debian/Ubuntu | wl-clipboard | xclip — but dotfiles-Debian installs neither: it targets headless SSH-only boxes, so clip has no local backend. Since v4.13.0 it falls back to OSC 52 — the terminal you are sitting at puts the payload on its own clipboard, with nothing installed on the remote end — and since #525 it also reaches tmux’s server-side buffer, which is the path copy-pipe needs. See its README’s Headless clipboard note. |
Distro quirks worth a README note (and that will actually bite you)
Arch — Rolling release; update often or not at all (partial upgrades break
things — never -Sy <pkg> without -u). Most modern tools are in official
repos; the rest are one paru -S away in the AUR. Enable multilib if you’ll
run 32-bit/Wine tooling. Cleanest distro for this stack.
openSUSE — Two flavors, and the update command differs: Tumbleweed
(rolling) uses zypper dup, Leap (stable) uses zypper up. Get this wrong
and you either don’t update or you half-update. Add the Packman repo (the
openSUSE analog to RPM Fusion) for codecs. zypper has the best dependency
solver of any of these — lean on it.
Alpine — The real outlier: musl libc, not glibc. Prebuilt binaries
linked against glibc (some cargo-less installer scripts, some vendor blobs)
will not run — prefer apk packages or musl-target builds. Default shell is
ash (busybox), default privilege tool is doas (not sudo), and many
“classic” commands are busybox applets with fewer flags. This is your
small-footprint / container / rescue-disk distro — keep its layer lean and don’t
fight the musl grain.
Gentoo — Source-based: emerge compiles packages, so expect real build
time (mitigate with binary packages via a binhost, and tune MAKEOPTS). USE
flags gate features at compile time — this is the whole point of Gentoo and
where the learning is. Tool names are full atoms (category/name). Treat this
repo as your “understand the system from the ground up” build; it’s the most
educational and the most time-expensive.
Offense (Kali / WSL2) — Not stamped from Fedora (neither are macOS and NixOS, see
Repo status): it’s Debian-family
(apt) and carries a unique offensive role layer on top of an OS layer it no longer
ships itself — dotfiles-Debian owns band 80 and accepts ID=kali as a first-class
target, and Offense adds the 85-offensive.zsh stage to the loader between it and
99-local.zsh (80-os → 85-offensive → 99-local). Two things
actually bite. (1) Debian renames binaries — bat→batcat, and the fd-find
package installs fdfind; Core handles both. (2) WSL2 is NAT’d, so a listener
or reverse shell in Kali isn’t reachable from your LAN until you enable mirrored
networking — which lives in the Windows-side %UserProfile%\.wslconfig
(networkingMode=mirrored, Win11 22H2+), not /etc/wsl.conf. Keep all
engagement data in ~/engagements (outside the repo); the repo ships a paranoid
.gitignore as backup.
Debian/Ubuntu — the fleet’s only frozen target, and that is the whole
character of the repo. dotfiles-Debian aims at Ubuntu 24.04 LTS (April 2024)
while proving debian:trixie in CI, so unlike every rolling sibling, “apt has it” is
not the same question as “apt has a version Core can use”. Two packages resolve
perfectly and break the stack: neovim (noble 0.9.5 vs the 0.12 that
nvim-treesitter’s main hard-requires — ³³ tabulates this against Gentoo, which
reaches the same wrong version by a different route) and tree-sitter-cli (noble
0.20.8 vs the 0.26.1 floor in footnote ⁵ — no Debian/Ubuntu suite short of sid clears it). Its
install/packages.txt therefore declares # min: floors that CI enforces, and a
dozen tools come from pinned upstream assets instead (²⁸). Three more traps:
yq in apt is kislyuk’s Python tool, and the Go build (yq-go) is sid-only, so
neither name is safe to list; cargo is 1.75, too old to build yazi/ast-grep/viddy,
which are simply not installed (²⁹); and needrestart, preinstalled on Ubuntu
Server, interposes on apt with a full-screen prompt that DEBIAN_FRONTEND does not
suppress — an unattended bootstrap over SSH hangs forever without NEEDRESTART_MODE=a.
Vendor-signed apt repos (Charm, 1Password) are used where upstream offers them; PPAs
are not, because they are keyed to an Ubuntu series and would break the Debian lane.
Repo status
- Built:
core,Fedora(template),MacBook,Arch,Debian,openSUSE,Alpine,Gentoo,NixOS,Offense,Defense. That is the ten Core-vendoring repos (scripts/os-repos.txt) pluscoreitself;Windowsvendors nocore/and is tracked separately. - Stamp-pending (this doc): none — all five template stamps are complete.
Offense(offensive role layer ondotfiles-Debian),MacBook(Homebrew) andNixOS(declarative, noinstall/packages.txt) are their own lineages, built directly rather than stamped from Fedora.Windowsis tracked separately from this matrix.- Role repos:
Offense(offensive) andDefense(defensive) both vendor Core.Offenseused to carry its own OS-native layer (Debian/apt, kali-rolling) and no longer does: it shedos/,install/packages.txtandscripts/tool-versions.enventirely, and the Kali package lane moved todotfiles-Debian’sonly:kalitier — so that repo is now a first-class ubuntu/debian/kali target rather than a frozen-Ubuntu-LTS one, andOffenseis a pure role layer.Defenseis distro-agnostic — it stacks its blue-team stage on whatever OS-native layer is underneath — so it has no row in this OS-stamp matrix by design, not by omission. Both role repos source the shared bootstrap scaffold (lib/bootstrap-lib.sh) and callblib_link_coreexactly as the OS repos do. Where they differ is the80band, which belongs to the OS repo underneath: the contract is that a role repo skipsblib_link_os_layerand callsblib_link_role_layerinstead, wiring the85band andtmux/role.conf. Both role repos have adopted it —Offensefirst,Defensein dotgibson/dotfiles-core#976, which retired thewire_defense_stageit used to hand-roll.core.manifestrecords the same. Debianis stamped from Fedora structurally, but takes its apt idioms fromOffense— the fleet’s other Debian-family repo. It is the only frozen target (Ubuntu 24.04 LTS), which is why it carries by far the largest out-of-band install surface (²⁸) and the only version-floor gate in the fleet.
Stamping order (all complete — kept as the recommended sequence for reference)
- Arch ✓ — almost everything is in-repo; closest to Fedora effort.
- openSUSE ✓ — straightforward once you internalize
dupvsup. - Alpine ✓ — forces you to reason about musl and minimalism (great for the container/rescue skills a red-teamer wants).
- Gentoo ✓ — the capstone; USE flags + source builds teach you the most.
- Debian/Ubuntu ✓ — added last, and the one that breaks the pattern: the
structure ports cleanly, but a frozen LTS means the interesting work is deciding
what apt can honestly supply and pinning the rest. Read its
CLAUDE.mdbefore changinginstall/packages.txt.