Gap register¶
Reviewed against ada9e1c, 2026-09-22. Everything found while reading all 64 source files. Each entry
has a stable ID so other pages and PRs can reference it. When you fix one, change its status here
rather than deleting it.
Severity: High = something is broken or lands on machines that should not get it · Medium = works but fragile, inconsistent, or undocumented · Low = polish.
| ID | Sev | Profiles | Summary | Status |
|---|---|---|---|---|
| G-01 | High | Linux | Tools the configs depend on are never installed (delta, eza, lazygit, gh, direnv, …) | fixed — GitHub release binaries into ~/.local/bin |
| G-02 | High | Linux | Neovim config needs 0.11+, distros ship 0.7–0.9 | fixed — upstream tarball to ~/.local/nvim when distro nvim < 0.11 |
| G-03 | High | macOS work, macOS headless | Brewfile is not profile-aware: every cask on every Mac | fixed — fragments in .chezmoitemplates/brew/ |
| G-04 | High | macOS | brew-sync re-appends commented lines; 6 duplicates, two cask-name pairs | fixed — name-based diff, Brewfile deduped |
| G-05 | High | Windows | No auto-update, no bootstrap, signing key never provisioned, nvim config in wrong place | fixed — Scheduled Task, bootstrap.ps1, XDG_CONFIG_HOME, ControlMaster guard |
| G-06 | Medium | Linux (Arch, Alpine) | Debian package names break pacman/apk; dead headless branch | fixed — per-manager names; headless branch now gates Zed + font |
| G-07 | Medium | all | authorized_keys needs GitHub at every apply; personal keys land on work boxes |
fixed — unmanaged on work; 168h API cache |
| G-08 | Medium | work | Work profile expects id_ed25519_skenmy / _eit; bootstrap writes id_ed25519 |
fixed — bootstrap names the key by the work flag |
| G-09 | Medium | macOS headless | No restic backup on headless Macs — intentional? | fixed — decided: headless Macs are backed up |
| G-10 | Medium | docs | README drift (GPG vs SSH signing, themes, age, agents) | fixed |
| G-11 | Medium | macOS desktop | VS Code settings hard-code /Users/paul |
fixed — VS Code management removed; Zed replaces it |
| G-12 | Medium | repo | No CI or pre-commit on the repo itself | fixed — ci.yml: pre-commit, per-OS/profile render, shellcheck, assertions |
| G-13 | Low | all | Neovim <C-j>/<C-k> mapped twice |
fixed — quickfix on ]q/[q |
| G-14 | Low | all | pre-commit defaults pin 2024 revisions | fixed — v6.0.0 / v8.30.1 / v2.4.3 |
| G-15 | Low | Windows | Four bash run_onchange scripts have no OS guard |
fixed — ignored on Windows by target name |
| G-16 | Low | all | Identity emails hard-coded in four places | fixed — .chezmoidata/identity.toml |
| G-17 | Low | Linux desktop | tmux copy assumes pbcopy/xclip; no Wayland; no Nerd Font installed |
fixed — wl-copy fallback; JetBrainsMono Nerd Font installed on desktops |
| G-18 | Low | macOS | brew-sync commits unsigned, straight to main |
fixed — commits are signed; unsigned fallback removed |
| G-19 | Low | Linux | chsh to zsh is manual |
fixed — installer runs chsh when interactive |
| G-20 | Low | all | me: URL shortcut renders to paulwilliams/, not skenmy/ |
fixed — uses githubUser |
| G-21 | Medium | all | .chezmoiignore script entries used source names, so they never matched |
fixed — target names |
| G-22 | Low | Linux | import-gpg-key shebang mangled by a {{- trim |
fixed |
| G-23 | High | macOS, Linux | A brew-sync merge conflict leaves the source clone mid-rebase; every nightly update then fails silently | fixed — workers detect and reset the clone |
| G-24 | Low | all | codespell --ignore-words-list split by YAML; only the first word was ignored |
fixed — quoted |
| G-25 | High | macOS work | brew-sync on a work Mac targeted common, so employer tooling would reach every Mac |
fixed — work.Brewfile fragment |
G-01 — Linux: required tools never installed¶
Evidence. run_once_install-packages-linux.sh.tmpl declares
EXTRAS_VIA_BIN=(starship mise atuin zoxide eza lazygit delta gh) but only starship, mise, atuin and
zoxide have install blocks. delta, eza, lazygit, gh are never installed. Neither are direnv,
yq, btop, k9s, kubectl, helm, terraform, pre-commit, mkcert, uv.
Impact. ~/.gitconfig sets core.pager = delta and interactive.diffFilter = delta, and
~/.config/chezmoi/chezmoi.toml sets diff.pager = "delta --paging=never". On every Linux box
git diff/git log print an error before falling back to plain output, and chezmoi diff fails outright.
ls keeps its eza aliases only because they are guarded; k, tf, lzg are dead aliases.
Fixed. install_release_bin now handles archives and raw binaries and installs delta, eza, lazygit, gh, direnv, yq, tealdeer and xh from GitHub releases; the tealdeer pattern had also never matched its raw-binary assets, so tldr was missing too. btop moved to the distro list. Kubernetes tooling is still macOS-only by choice. Original options for the record: (a) Extend the existing install_release_bin helper to cover dandavison/delta,
eza-community/eza, jesseduffield/lazygit, cli/cli, direnv/direnv, mikefarah/yq. (b) Install
Linuxbrew in the Linux script and reuse the Brewfile minus casks (~/.zprofile already loads it). Option
(a) keeps servers lean; (b) unifies the inventory. Either way, gate core.pager on lookPath "delta"
in the template so git degrades cleanly.
G-02 — Linux: Neovim too old for the config¶
Evidence. lsp.lua calls vim.lsp.config("*", …) and relies on mason-lspconfig v2 auto-enable
(vim.lsp.enable), both Neovim 0.11 APIs. treesitter.lua targets the nvim-treesitter main branch,
which also requires 0.11 (its README says 0.11+). The Linux installer gets neovim from the distro:
Ubuntu 24.04 ships 0.9.5, Debian 12 ships 0.7.2.
Impact. On Linux, nvim starts with Lua errors and no LSP or tree-sitter.
Fixed. neovim left the distro package list; install_release_tree extracts the upstream nvim-linux-<arch>.tar.gz to ~/.local/nvim and symlinks ~/.local/bin/nvim whenever the nvim on PATH is missing or older than 0.11. A version guard in init.lua is still a nice-to-have.
G-03 — Brewfile is not profile-aware¶
Evidence. Brewfile is a plain file; the only profile influence is --no-upgrade on headless.
All 17 casks — Spotify, Telegram, Battle.net, Wireshark, Logi Options+, Raycast, Docker Desktop, Ghostty,
iTerm2, Claude Code (twice), OpenClaw, Tailscale (twice), Zulu 17, two fonts — plus heavyweights such as
dotnet, ollama, whisper-cpp, ffmpeg, tesseract, streamlink, powershell install on every Mac.
Impact. Work Macs get personal apps (Spotify, Battle.net, Telegram) and the Tailscale cask even though the README says work boxes use the employer-managed Tailscale. Docker Desktop on an employer machine may need a paid licence. Headless Macs pull down GUI apps and fonts they cannot use. "Remove a package for one profile" is currently impossible from source; see the how-to.
Fixed. Brewfile is now rendered by Brewfile.tmpl from three fragments, and brew-sync diffs against their union and appends to the fragment named in ~/.config/dotfiles/brew-sync-target (templated: common on work Macs, personal elsewhere). Original sketch kept for the record:
Brewfile.tmpl:
{{ include "brew/common.Brewfile" }}
{{ if not .headless }}{{ include "brew/gui.Brewfile" }}{{ end }}
{{ if not .work }}{{ include "brew/personal.Brewfile" }}{{ end }}
run_onchange_after_brew-bundle.sh.tmpl hashes all fragments. dotfiles-brew-sync must then diff
against the union of fragments and append to a target fragment chosen per machine (default
common; a BREW_SYNC_TARGET line in ~/.config/dotfiles/brew-sync-ignore or a new config file), or
it will re-add personal casks to common from personal Macs. Do this together with G-04.
G-04 — brew-sync duplicates¶
Evidence. extract_pkgs() in dotfiles-brew-sync is grep -E '^(brew|cask|tap|mas) ' | sort -u
on whole lines. Original lines such as brew "tealdeer" # fast tldr pages never equal the dump's
brew "tealdeer", so they were appended again on 2026-07-04. The generated
Brewfile page lists the six current duplicates with line numbers. The same
run added cask "claude-code@latest" next to cask "claude-code" and cask "tailscale-app" next to
cask "tailscale", so each Mac now tries to install both variants.
Fixed. normalise() in dotfiles-brew-sync reduces every line to kind "name" before comparing. The fragments were deduped by hand; brew info showed tailscale-app and claude-code@latest as the live casks, so tailscale and claude-code were dropped. brew bundle will not uninstall the stale variants on machines that already have them.
G-05 — Windows is a second-class profile¶
Evidence.
- No scheduled
chezmoi update;Library/and.config/systemdare both ignored on Windows and no Task Scheduler equivalent exists. bootstrap.shtells Windows users to runscripts/bootstrap.ps1, which is not in the repo. No Bitwarden pull, so no GPG/SSH/atuin/restic secrets.~/.gitconfigrenderscommit.gpgsign = truewithsigningkey = ~/.ssh/id_ed25519.pub, which nothing creates on Windows. Every commit fails until the key is placed orsigningKeyis blanked.dot_config/nvimlands in~/.config/nvim; Neovim on Windows reads%LOCALAPPDATA%\nvimunlessXDG_CONFIG_HOMEis set, and the PowerShell profile does not set it.~/.ssh/configsetsControlMaster/ControlPath, which Windows OpenSSH does not support.~/.config/dotfiles/tipsand~/.config/direnv/direnvrcare deployed with nothing to consume them.
Fixed. run_onchange_after_install-update-task.ps1.tmpl registers the per-user Scheduled Task
skenmy-chezmoi-update (daily 03:17, runs when logged on, catches up after sleep) driving the new
~/.local/bin/chezmoi-update-and-notify.ps1 worker, and sets XDG_CONFIG_HOME=%USERPROFILE%\.config
user-wide (the PowerShell profile also sets it per session) so Neovim, atuin and mise read the same
~/.config tree as on macOS/Linux. scripts/bootstrap.ps1 installs chezmoi + Bitwarden.CLI, runs
chezmoi init --apply, writes the SSH key (named by the work flag), imports GPG when a gpg.exe
exists, and logs atuin in. ControlMaster/ControlPath/ControlPersist are wrapped in
{{ if ne .chezmoi.os "windows" }}. .config/direnv, .config/dotfiles, .config/pre-commit, the
bash workers and every bash run script are ignored on Windows. Still absent on Windows by choice:
restic, tmux, tips, gh dash, tealdeer cache refresh.
G-06 — Linux installer portability¶
Evidence. COMMON_PKGS uses Debian names (fd-find). Arch and Alpine call it fd. install_pacman
appends || true, so on Arch the whole transaction fails and nothing installs, silently. install_apk
has no || true under set -e, so the run_once aborts on Alpine. The {{ if not .headless }} /
{{ else }} blocks around COMMON_PKGS+= are identical, so headless has no effect on Linux packages.
Fixed. Each install_<manager> passes its own spelling (fd-find/xz-utils on apt, fd-find/xz on dnf, fd/xz on pacman and apk) and --needed on pacman; failures are logged and the script continues with a summary. The headless branch now gates Zed and the Nerd Font instead of being a no-op.
G-07 — authorized_keys from GitHub at apply time¶
Evidence. private_authorized_keys.tmpl calls gitHubKeys "skenmy" unconditionally.
Impact. Every chezmoi apply, including the nightly one, needs GitHub's API. Offline or during an
outage the whole apply fails (template error), not just this file. Unauthenticated API calls are limited
to 60/hour per IP. Separately, the personal GitHub keys become login-authorised on work machines.
Fixed. .ssh/authorized_keys is listed in .chezmoiignore under {{ if .work }}, so chezmoi stops managing it on employer machines without touching whatever is there. .chezmoi.toml.tmpl sets gitHub.refreshPeriod = "168h" so gitHubKeys is served from cache between weekly refreshes; existing boxes only pick that up after chezmoi init (re-renders the config, keeps prompt answers). Remaining risk: a brand-new personal box with no cache and no network still fails its first apply, which is acceptable.
G-08 — Work profile key names vs bootstrap¶
Evidence. With work=true, .gitconfig, .ssh/config and build-allowed-signers expect
~/.ssh/id_ed25519_skenmy and ~/.ssh/id_ed25519_eit. bootstrap.sh writes the vault key to
~/.ssh/id_ed25519 (SSH_ITEM=ssh/personal/id_ed25519, name taken from the item). Nothing provisions
the EIT key or documents how to.
Fixed. bootstrap.sh now runs chezmoi init --apply first, then install_ssh reads work from chezmoi data and writes ~/.ssh/id_ed25519_skenmy on work boxes (id_ed25519 otherwise), prints a reminder to place the EIT key from 1Password, and runs chezmoi apply again so build-allowed-signers (whose hash now includes whether the key files exist) rebuilds allowed_signers.
G-09 — Headless Macs have no backup¶
Evidence. com.skenmy.restic-backup.plist.tmpl and install-restic-units are gated
and (eq .chezmoi.os "darwin") (not .headless); the Linux units are not headless-gated.
Fixed (decision: back them up). The (not .headless) gate was dropped from the restic plist and from
install-restic-units, so every Mac with a ~/.config/restic/env gets the 04:32 job. brew-sync stays
desktop-only.
G-10 — README drift¶
Fixed. README now describes SSH signing and the signingKey switch, Catppuccin Mocha for starship and
Ghostty, the per-profile SSH agents, and notes that age encryption is not configured. The signingKey
prompt text explains what the value does.
G-11 — VS Code settings hard-code a username¶
Fixed by removal. VS Code settings and the extensions installer were deleted when Zed became the managed editor (2026-09-22). Zed's settings template contains no machine-specific paths.
G-12 — No CI on the repo¶
Fixed. .github/workflows/ci.yml runs on every push and PR: (1) pre-commit run --all-files with the
repo's own .pre-commit-config.yaml (the shipped defaults plus shellcheck on the workers); (2) a
render matrix over ubuntu/macos/windows × two profiles (desktop-personal: headless=false, work=false;
server-work: headless=true, work=true) that runs chezmoi init from .chezmoi.toml.tmpl with the
prompt answers supplied by flag, chezmoi managed, a full apply --dry-run render of every file, a
render + shellcheck of every run_*.sh.tmpl (unix) or a PowerShell parse of every .ps1.tmpl (Windows),
and profile assertions (Brewfile only on macOS, .gitconfig-eit only when work, authorized_keys
never when work, no Zed config when headless, Zed settings under AppData on Windows). The baseline
run surfaced G-22 and G-24.
G-13 — Neovim <C-j> / <C-k> double-mapped¶
Fixed. Quickfix next/prev moved to ]q / [q; <C-j>/<C-k> navigate windows again.
G-14 — pre-commit defaults pinned to 2024¶
Fixed. Bumped to pre-commit-hooks v6.0.0, gitleaks v8.30.1, codespell v2.4.3 (2026-09-22); the
file header now carries the pre-commit autoupdate -c … one-liner for next time.
G-15 — Unguarded bash scripts on Windows¶
Fixed. All eight bash run scripts are listed in the Windows block of .chezmoiignore under their
chezmoi target names (see G-21), so Windows never attempts them. Consequence: no GPG import,
allowed_signers rebuild, gh dash or tealdeer refresh on Windows; bootstrap.ps1 covers GPG.
G-16 — Identity emails hard-coded¶
Fixed. .chezmoidata/identity.toml (versioned, merged into template data automatically, no re-init) defines githubUser and workEmail. dot_gitconfig-eit, the git hook, starship.toml and build-allowed-signers became templates that reference them; the personal pill uses the email prompt value.
G-17 — Linux desktop polish¶
Fixed. tmux copy tries pbcopy, then wl-copy, then xclip. Linux desktops get JetBrainsMono Nerd Font in ~/.local/share/fonts/JetBrainsMonoNerdFont with fc-cache.
G-18 — brew-sync bypasses PR and signing¶
Fixed. dotfiles-brew-sync now commits with the machine's normal git config, so commits are SSH-signed. If signing fails under launchd (key unavailable), the change stays staged and is retried on the next run; there is no unsigned fallback. Commits still land directly on main by design.
G-19 — Default shell on Linux¶
Fixed. The installer runs chsh -s $(command -v zsh) when stdin is a terminal and $SHELL is not zsh; non-interactive applies skip it and print nothing.
G-25 — work-Mac brew-sync would publish employer tooling to every Mac¶
Evidence. After G-03 the sync target on work = true machines was common. The work MacBook's stuck
clone held three never-pushed brew-sync commits with 91 employer packages (ansible, argocd, oci-cli,
Vault, Stream Deck, …); had they synced into common.Brewfile, every personal Mac would have installed
them at the next apply.
Fixed. A fourth fragment, work.Brewfile, rendered only when work = true; brew-sync-target
renders work on work Macs and personal elsewhere. common.Brewfile is now hand-edited only.
Work Macs' local installs therefore reach other work Macs and nothing else. Expect the first nightly
sync from the work MacBook to append its current package set to work.Brewfile; prune lines that
should not propagate (ones needing admin rights, or duplicates such as tfenv alongside terraform).
Evidence. The template began #!/usr/bin/env bash followed by a {{- /* … */ -}} comment; the leading
- trimmed the newline after the shebang, so the rendered first line was
#!/usr/bin/env bash# gpg-public-key.asc hash: …. macOS splits shebang arguments and still ran bash,
so nothing failed there; Linux passes the rest of the line as one argument, so env would look for a
program literally named bash# gpg-public-key.asc …. Found by shellcheck (SC2096) in the CI baseline.
Fixed. The template comment became a plain # comment on its own line.
G-23 — stuck source clone disables nightly updates¶
Evidence. On this work Mac ~/.local/state/chezmoi-update/last.log showed chezmoi update exited 1
every night from 2026-09-20: Pulling is not possible because you have unmerged files. The clone at
~/.local/share/chezmoi was detached mid-rebase with UU Brewfile: two Macs' brew-sync runs had appended
to the same lines the same night, the pull-with-rebase stopped on the conflict, and nothing recovered or
alerted. Every later chezmoi update (a pull followed by apply) failed until noticed.
Fixed. Both workers treat the clone as disposable, since origin/main is the truth.
chezmoi-update-and-notify checks for a rebase in progress or unmerged status entries before and after
chezmoi update; if found it aborts the rebase, returns to main at origin/main, drops autostash
leftovers, logs what it did and retries once. dotfiles-brew-sync does the same when its own pull fails
and re-detects the drift the next night. The affected Mac had to be recovered by hand once (abort the
rebase, switch back to main, discard the conflicted Brewfile, fast-forward).
G-24 — codespell ignore list split by YAML¶
Evidence. args: [--ignore-words-list=crate,hist] is a YAML flow sequence; commas separate items, so
codespell received --ignore-words-list=crate plus hist as a file path. The shipped defaults had the
bug; the repo's own config inherited it and failed on fpr in seed-bitwarden.sh until diagnosed.
Fixed. Quoted in both files: args: ["--ignore-words-list=…"], with a comment explaining why.
G-21 — .chezmoiignore script entries never matched¶
Evidence. Found while adding Windows entries: chezmoi managed still listed install-packages-windows.ps1
on macOS despite run_once_install-packages-windows.ps1 being in .chezmoiignore. chezmoi strips the
run_once_/run_onchange_/before_/after_ attributes to form a script's target name, and
.chezmoiignore matches target names. The original darwin/linux entries were therefore inert; nothing
broke only because every script also had an OS guard inside its template that rendered it empty.
Fixed. All script entries use target names (install-packages-darwin.sh, brew-bundle.sh, …), a
comment at the top of .chezmoiignore explains the rule, and the generated
file inventory now shows the target name for every script so the two can be compared.
G-20 — me: shortcut renders the wrong owner¶
Fixed. The me: shortcut now renders from {{ .githubUser }} → git@github.com:skenmy/.