Skip to content

Harden the bootstrap, pin the supply chain, and prune dead config - #91

Merged
jfmercer merged 4 commits into
masterfrom
audit-fixes
Jul 27, 2026
Merged

Harden the bootstrap, pin the supply chain, and prune dead config#91
jfmercer merged 4 commits into
masterfrom
audit-fixes

Conversation

@jfmercer

Copy link
Copy Markdown
Owner

Outcome of a full read-only audit of this repo, then the fixes. 35 files, +615/−6223.

The two changes that matter most are correctness fixes on paths CI structurally could not reach.

Why

A fresh Apple Silicon bootstrap could not complete. scripts/install_dotfiles.sh installed Homebrew and immediately execd into chezmoi init --apply, whose first darwin script runs brew bundle under set -e. Homebrew's installer does not modify the calling shell, and /opt/homebrew/bin is not on the default Apple Silicon PATH — so brew was not found, the script exited 127, and the entire apply aborted. GitHub's macOS runners ship Homebrew already on PATH, which is precisely why CI never caught it.

The install one-liner in the README destroyed uncommitted work. It ran git checkout -B + reset --hard FETCH_HEAD + clean -fdx against ~/.local/share/chezmoi unconditionally, with no warning in the docs.

Nothing verified upstream code. All 14 .chezmoiexternal.yaml entries tracked master with no checksum on a 168h auto-refresh. Those files are sourced into every interactive shell and every tmux session, so anyone able to push to any one of those repos had automatic code execution here within a week.

Changes

Bootstrap

  • Put brew on PATH after installing it, via prefix detection (/opt/homebrew, then /usr/local). Applied in both install_dotfiles.sh and the darwin Homebrew script, so chezmoi apply is self-sufficient outside the bootstrap.
  • Gate the destructive reset behind DOTFILES_FORCE_RESET. The default path fast-forwards and refuses to run on a dirty tree. CI sets the flag.
  • Verify the Xcode CLI tools install actually succeeded, and remove the softwareupdate lockfile that was previously created and left behind.
  • Honour HOMEBREW_CASK_OPTS at apply time. Casks upstream ships as sha256 :no_check install in a second pass without --require-sha, so enforcement is the default and the exceptions are explicit.

Supply chain

  • Pin all 14 externals to tags (one commit, where upstream publishes no tags) and add checksum.sha256 to each.
  • Pin lazygit and verify its published release checksum; verify rustup-init before executing it; check the eza signing key fingerprint before trusting that apt repo.
  • Pin the chezmoi version fetched from get.chezmoi.io.

Shell

  • Stop forcing TERM=xterm-256color. It overwrote Ghostty's xterm-ghostty and tmux's tmux-256color, and lied outright on the Linux virtual console (TERM=linux, 8 colours). Now falls back only when the terminal has no terminfo entry at all — detected through zsh/terminfo with no fork.
  • Fix fzf, which had never worked. The brew install step only wrote a ~/.fzf.zsh that nothing sourced, so Ctrl-T/Alt-C were unbound and the three FZF_*_COMMAND exports were unreachable. Replaced with fzf --zsh through the existing cached-eval helper.
  • New dot_zshenv for DOTFILES, XDG base dirs and EDITOR, so non-interactive shells, cron and git hooks see them. PATH deliberately stays out — /etc/zprofile runs path_helper afterwards and demotes prepends (measured: asdf shims position 1 → 13).
  • Move the completion caches under $XDG_CACHE_HOME; guard stty behind a tty test; only fork mkdir when the cache dir is genuinely missing.
  • Replace the run_onchange script that appended Linux aliases to a gitignored file — it duplicated entries on every edit and lost them entirely to git clean -fdx — with an inline $OSTYPE guard.
  • Fix aliases that errored on invocation: four using fish's ; and (zsh ran command not found: and), GNU-only stat -c on macOS, GNU-only sed -i on BSD, hub subcommands with hub uninstalled, and a duplicate grb.
  • Guard macos/alii.zsh on darwin; it was previously sourced on Linux too.

Public-repo hygiene

  • Move employer-specific git config behind an untracked ~/.gitconfig.work include. No credentials were ever committedgitleaks over all 239 commits reports clean — this is org topology only. Note this does not purge history; the strings remain in an earlier commit, which was judged not worth a filter-repo rewrite.

CI

  • Apply the checkout instead of cloning master, so pull requests finally test their own code. Previously actions/checkout output was discarded and every PR validated master.
  • Add shellcheck, matrixed over both OSes — each platform's chezmoi scripts render to an empty string on the other, so a single-OS lint job would silently skip half of them.
  • Add gitleaks and an idempotence gate (two applies, then assert no drift). The gate is chezmoi status --exclude=scripts, not chezmoi verify: the four plain run_after_ scripts are meant to run every apply, so bare verify can never exit 0.
  • Bump actions/checkout v4 → v7.0.1.

Removals

  • Parrot OS support (retired). This also removes the script that read a password from ~/Desktop and passed it as argv to expect, where ps exposed it to any local user.
  • iterm2/ (Ghostty now), vagrant/alii.zsh (not installed), zsh/zshenv (unreachable — wrong name and location to ever be sourced), scripts/install_xcode_cli_tools.sh (never invoked; its two useful safety checks were folded into the bootstrap), and vim's Tagbar plus the long-commented-out Syntastic block.

Verification

Check Result
shellcheck — plain scripts + all 7 rendered templates, both platforms clean
zsh -n — every zsh file clean
14/14 external checksums, re-fetched from live upstream verified
Checksum enforcement correct → exit 0; one byte flipped → exit 1 with SHA256 mismatch
Every pinned tag still contains the entry point the configs source verified (incl. powerline/default/blue.tmuxtheme, tpm, all *.tmux)
DOTFILES_FORCE_RESET dirty tree → exit 1, file preserved; forced → discards; clean → ff-only
TERM guard preserves 8 real terminfo entries incl. vt100/vt220/dumb; overrides only genuinely-missing ones
fzf 4 widgets bound, fzf-file-widget defined (was 0/0)
gitleaks clean, history and working tree
Nothing leaks into $HOME topic dirs, bin/, terminal/ all correctly ignored

Not verified — worth reviewer attention. The Linux install path and all CI jobs are verified by rendering, linting and local simulation, not by running on Debian or a GitHub runner. The first CI run is the real proof. The Apple Silicon bootstrap fix is likewise code-path evidence; confirming it needs a clean VM.

Required after merge

chezmoi init --prompt   # the config template changed, AND plain `init` will not
                        # re-prompt for install_mac_apps (promptBoolOnce skips
                        # keys already present in the generated config)
chezmoi apply

Answering yes to "Install Mac applications" triggers ~33 formulae plus two font casks on the next apply.

~/.gitconfig.work must exist on work machines. Git silently ignores a missing include, so the failure mode is "rewrite stops happening", not an error.

jfmercer added 4 commits July 27, 2026 17:33
Result of a full audit of this repo. The changes that matter most are
correctness fixes on paths CI structurally cannot reach: a fresh Apple
Silicon bootstrap could not complete, and the install one-liner the
README recommends destroyed uncommitted work without warning.

Bootstrap
- scripts/install_dotfiles.sh: put brew on PATH after installing it.
  Homebrew's installer does not touch the calling shell, and
  /opt/homebrew/bin is not on the default Apple Silicon PATH, so the
  `brew bundle` in the darwin script exited 127 and took the whole
  `chezmoi apply` with it. GitHub's macOS runners ship brew already on
  PATH, which is why CI never caught this.
- scripts/install_dotfiles.sh: gate the destructive reset behind
  DOTFILES_FORCE_RESET. The default path now fast-forwards and refuses
  to run against a dirty tree, instead of `reset --hard` + `clean -fdx`.
- scripts/install_dotfiles.sh: verify the Xcode CLI tools install
  actually succeeded, and clean up the softwareupdate lockfile.
- darwin/...homebrew: honour HOMEBREW_CASK_OPTS at apply time so casks
  land in ~/Applications with --require-sha. Casks upstream ships as
  `sha256 :no_check` install in a second pass without that flag.

Supply chain
- .chezmoiexternal.yaml: pin all 14 externals to tags (one commit, where
  upstream publishes no tags) and add checksum.sha256 to each. Every
  entry previously tracked master, unverified, on a weekly refresh —
  arbitrary code execution in every shell and tmux session for anyone
  able to push to any one of those repos.
- linux installs: pin lazygit and verify its release checksum, verify
  rustup-init before running it, and check the eza signing key
  fingerprint before trusting that apt repo.
- install.sh: pin the chezmoi version fetched from get.chezmoi.io.

Shell
- zsh/exports.zsh: stop forcing TERM=xterm-256color. It overwrote
  Ghostty's xterm-ghostty and tmux's tmux-256color, and lied outright on
  the Linux virtual console. Now falls back only when the terminal has
  no terminfo entry at all, detected via zsh/terminfo without a fork.
- fzf/fzf.zsh: wire fzf up with `fzf --zsh`. The old brew install step
  only wrote an ~/.fzf.zsh that nothing sourced, so Ctrl-T and Alt-C
  were never bound and the FZF_*_COMMAND exports were unreachable.
- dot_zshenv: new. DOTFILES, XDG base dirs and EDITOR, so non-interactive
  shells, cron and git hooks see them. PATH deliberately stays out:
  /etc/zprofile runs path_helper afterwards and demotes prepends.
- dot_zshrc, zsh/completion.zsh: move the completion caches under
  $XDG_CACHE_HOME; only fork mkdir when the directory is missing; guard
  stty behind a tty test so scripted shells stop printing an error.
- system/alii.zsh: inline the Linux aliases behind an $OSTYPE guard,
  replacing a run_onchange script that appended to a gitignored file and
  so both duplicated entries and lost them to `git clean -fdx`.
- macos/alii.zsh: guard on darwin, and fix four aliases using fish's
  `; and` — in zsh those ran "command not found: and".
- system/alii.zsh, bin/enum: fix GNU-only `stat -c` and `sed -i` on BSD.
- git/git-alii.zsh: point gbr/gpr at gh (hub is not installed); drop a
  duplicate grb definition.

Public repo hygiene
- dot_gitconfig.tmpl: move employer-specific config behind an untracked
  ~/.gitconfig.work include. Nothing sensitive was ever committed — this
  is org topology, not credentials — but it does not belong here.

CI
- .github/workflows/ci.yaml: apply the checkout instead of cloning
  master, so pull requests finally test their own code. Add shellcheck
  (matrixed over both OSes, since each platform's chezmoi scripts render
  empty on the other), gitleaks, and an idempotence gate. Bump
  actions/checkout v4 -> v7.0.1.

Removals
- Parrot OS support, retired. Also removes the script that read a
  password from ~/Desktop and passed it as argv to expect, where ps
  exposed it to any local user.
- iterm2/ (Ghostty now), vagrant/alii.zsh (not installed), zsh/zshenv
  (unreachable: wrong name and location to ever be sourced),
  scripts/install_xcode_cli_tools.sh (never invoked; its two useful
  safety checks were folded into the bootstrap), and vim's Tagbar plus
  the long-commented-out Syntastic block.
The first CI run failed on lint (ubuntu-latest) while lint (macos-latest)
passed on identical code. Ubuntu 24.04's apt shellcheck lags Homebrew's
by several releases and the two disagree about which findings to report,
so an unpinned gate is non-deterministic: it depends on which runner you
land on.

- .github/workflows/ci.yaml: install one pinned shellcheck (v0.11.0) from
  upstream releases on both runners instead of apt/brew, selecting the
  right platform tarball from $RUNNER_OS and uname -m.
- bin/secret: replace `[ "$#" -gt 0 ] && shift || true` with a plain
  if/then. Same behaviour -- verified no-args, help, unknown subcommand
  and argument passthrough are unchanged -- but the old idiom trips
  SC2015, since C can run when A succeeds and B fails.
Second CI failure: lint (ubuntu-latest) tried to shellcheck the darwin
Homebrew script and reported SC2148 (no shebang). On Linux that template
renders to a single newline rather than nothing, because its guard closed
with `{{ end }}` while every sibling uses `{{- end -}}`. One byte is not
empty, so the job's skip check let it through.

- darwin/...homebrew: close the guard with `{{- end -}}`, matching the
  other four platform-guarded scripts. All five now render 0 bytes when
  guarded off; verified the script is unchanged when guarded on.
- ci.yaml: skip a rendered template when it has no non-whitespace
  content, rather than testing `[ -s ]`. Belt and braces, so a future
  template with a stray newline cannot resurrect this.
Gating it on `push` meant scripts/install_dotfiles.sh was shellchecked but
never executed on a PR -- and that file holds the Homebrew-on-PATH fix, the
DOTFILES_FORCE_RESET gate and the Xcode CLI tools check. The `apply` job
runs install.sh, which is a different entry point, so the highest-severity
fix in this branch was merging on a lint pass alone.

The previous comment claimed the path "can only be tested after a merge".
That was wrong: install_dotfiles.sh clones whatever DOTFILES_BRANCH names,
and a PR branch exists on the upstream remote. Only forks genuinely cannot
be tested this way, since GITHUB_HEAD_REF there names a branch on the fork.

- ci.yaml: gate on push OR not-a-fork, and pass
  DOTFILES_BRANCH=${{ github.head_ref || github.ref_name }} so the job
  clones the branch under test rather than master.
- ci.yaml: correct the comment about DOTFILES_FORCE_RESET -- a fresh runner
  has no source directory, so it is a no-op guard, not a requirement.

Still cannot reproduce the Apple Silicon PATH failure itself: GitHub's
macOS runners ship Homebrew already on PATH. Noted in the job comment.
@jfmercer
jfmercer merged commit 95f3ab4 into master Jul 27, 2026
7 checks passed
@jfmercer
jfmercer deleted the audit-fixes branch July 27, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant