Skip to content

Latest commit

 

History

History
103 lines (70 loc) · 9.74 KB

hookscripts.md

File metadata and controls

103 lines (70 loc) · 9.74 KB

Hookscripts

POSIX-like platforms will automatically install required dependencies thanks to the hookscripts in .chezmoiscripts/00-macos/ and .chezmoiscripts/00-linux/ (plus 01-fedora/ and 02-bluefin/ layers on Linux).

Similarly NT platforms use the hookscripts in .chezmoiscripts/00-nt/ for dependency installation.

Every POSIX hookscript opens with one of two shared preambles from .chezmoitemplates/:

  • posix-preamble.sh -- platform guards, lazy can_sudo/require_sudo, load_brew (loads brew if present, sets HAS_BREW), and MANAGER. For scripts that merely prefer brew and have a fallback.
  • posix-preamble-brew.sh -- the above plus require_brew, the brew counterpart of require_sudo: fails fast if brew is missing and guarantees $HOMEBREW_PREFIX is set. For scripts whose whole job is brew work (package lists, casks, taps, anything that reads $HOMEBREW_PREFIX).

On macOS, 003-macos-prereqs runs first and installs the Apple-shipped prerequisites on every Mac: the Xcode Command Line Tools through softwareupdate, non-interactively, at no older a version than brew accepts on that macOS (so a macOS upgrade gets the matching CLT on the next apply, Xcode.app or not); the license of an installed Xcode.app; and Rosetta 2 on Apple Silicon. So all of it is in place before brew, MacPorts or any source build, and when it can't be (softwareupdate offers no CLT, say) the apply stops there rather than failing later in brew. 015-brew-taps then adds the third-party brew taps; everything installed from them is written as user/repo/name. Brew itself is installed by 005-homebrew on both platforms, from .chezmoitemplates/homebrew-install.sh, before anything that needs it. It is a run_after_, so a host whose brew was removed gets it back on the next apply. On Apple Silicon up to Tahoe 26, 006-homebrew-x86, 021-brew-packages-x86 and 031-brew-extras-x86 do the same for a second, x86_64 brew in /usr/local under Rosetta, from the same templates; see Rosetta brew.

The brew prefix is decided once, at chezmoi init, as .homebrewPrefix (/opt/homebrew on Apple Silicon, /usr/local on Intel, /home/linuxbrew/.linuxbrew on Linux) and rendered into every static file that has to name a brew binary: tmux, gpg-agent, the Touch ID PAM lines, the Chrome gpgme manifest, .bootstrap.sh. Hookscripts get the same answer at runtime from brew shellenv. After pulling a version of this repo that introduced .homebrewPrefix, .macos.series, .nativeArch or .machine, run chezmoi init once -- .chezmoi.toml.tmpl is only re-rendered by init, and templates reference these keys (.machine in .chezmoiignore.tmpl, so every chezmoi command fails until then).

Applying a Mac over SSH

Some macOS changes are only granted through a prompt on the Mac's own screen, which an SSH session cannot show. The scripts that run into one print a warning: instead of stopping the apply, and they are run_after_ so the next apply from the Mac's own session (in person or over Screen Sharing) finishes the job:

  • 004-emudhra-distrust: the line that points Apple's pip at the filtered /etc/ssl/cert.pem goes into each Apple Python's stdlib, and Xcode.app's is inside the app bundle, which takes App Management as for 170-firefox-policies below.
  • 140-ca-certs: certificate trust settings ("no user interaction was possible"). Apple supports no non-interactive way short of MDM.
  • 141-client-cert: an SSH session sees the login keychain locked, even while you are logged in at the Mac. security unlock-keychain in that session unlocks it.
  • 170-firefox-policies: writing into Firefox.app takes App Management. Allowing full disk access for remote users (System Settings → General → Sharing → Remote Login) grants it over SSH too.

Per-model hookscripts

.chezmoiscripts/99-<os>-<make>-<model>/ holds hookscripts for one computer model only, usually a workaround for its firmware or hardware. For example, 99-nt-ASUSTeK+COMPUTER+INC.-ROG+Flow+Z13+GZ302EA_GZ302EA/ replaces the ROG Flow Z13's broken S0ix sleep with hibernation. <os> is the tier name (nt, macos, linux). <make> and <model> are exactly what the firmware reports in its SMBIOS System Information (hw.model on a Mac), put through urlquery, so the name is valid on NT and Unix alike. chezmoi data shows the raw strings as .machine.make and .machine.model, and .chezmoiignore.tmpl masks every such directory except the host's own. Containers and WSL report no model, so they never get one. Being 99, these scripts run after everything in 00-*, the sign-in reminders included. To get the directory name for a new machine:

chezmoi execute-template '99-nt-{{ urlquery .machine.make }}-{{ urlquery .machine.model }}'

(A model ending in . also has that dot escaped as %2E, since Win32 strips trailing dots.)

Windows updates tend to put such workarounds back. A Windows one can be a run_onchange_ .tmpl that renders {{ output "cmd.exe" "/d" "/c" "ver" | trim }} into a comment, as 010-hibernate-not-s0ix does, so that every build, monthly update revision included, re-runs it. That only works if the script is idempotent and checks before it elevates, so that a re-run with nothing to fix raises no UAC prompt.

Headless hosts

useHeadless (CHEZMOI_USE_HEADLESS at chezmoi init) leaves out what only works with someone at the screen: GUI apps, and what exists only to serve one, like visualizer presets, browser and mail-client setup, pinentry dialogs and Explorer shell extensions. The Docker images and the macOS and Windows CI applies are headless. A WSL instance never is, even with CHEZMOI_USE_HEADLESS set: .chezmoi.toml.tmpl turns it off wherever WSL_DISTRO_NAME is set, so WSL gets the GUI apps, VS Code included.

  • A hookscript that is GUI-only as a whole is masked in .chezmoiignore.tmpl's Head block.
  • A GUI package in a list it shares with CLI tools sits in {{ if not .useHeadless }} within that list: 040's casks, 100's winget packages, 200's scoop apps, and the apt, dnf and brew lists.

In a run_once_ or run_onchange_ script, or a template one renders, explain such a gate in a template comment ({{/* */}}), not a shell one. The rendered text is the script's hash, so a desktop that renders it exactly as before does not run it again.

Packages: winget/scoop/apt/pkg/brew/pnpm/uv/whatever

Remember to define the package in the correct hookscript under .chezmoiscripts/00-macos/, .chezmoiscripts/00-linux/ or .chezmoiscripts/00-nt/. Python (uv), JavaScript (pnpm), Rust (cargo) and Go tools are the exception: they go in .chezmoidata/{python,pnpm,cargo,go}-tools.toml, which the hookscripts of every platform read (Tooling).

Scoop downloads

On Windows, 00-nt/200-scoop-install installs aria2 before any other scoop package. Scoop then downloads everything through it, and scoop config aria2-options --check-certificate=false turns TLS certificate checks off, so a vendor's lapsed certificate (nmap.org's, on 2026-10-18) holds no install or update up. Scoop still checks each download against the hash its manifest pins, and the manifest comes with its bucket from GitHub; every package 200 installs pins one. A package whose manifest pins no hash would be downloaded with no check at all, so don't add one. Where aria2 fails, for instance on a network without the IPv6 route a host's DNS answers point it to, Scoop falls back to its own downloader, which checks certificates as usual.

Running one script

Scripts can be applied individually, addressed by their stripped target name, which is useful when bringing up a new host in stages:

chezmoi apply ~/.chezmoiscripts/00-macos/005-homebrew.sh

Hook inputs

The CoC extension list lives in .chezmoidata/vim.toml; bootstrap hooks embed that list directly. dot_vimrc remains a normal managed dotfile, including its Vundle plugin declarations. The SSH server configuration and Docker configuration remain normal managed application files.

The client certificate bundle and GPG import bundles live as unchanged age ciphertext under .chezmoitemplates/secrets/. Hooks embed the ciphertext and use chezmoi decrypt at execution time with the host's configured identity. Plaintext is written only into a private temporary directory, removed by an EXIT trap on Unix or a finally block on Windows. No plaintext bundles are embedded in rendered scripts. Ciphertext changes change the rendered import hooks, retriggering run_onchange imports. useDummySecrets prevents both runtime decryption and GPG passphrase lookup while rendering.

.chezmoiremove retires the old ~/.coc-extensions.txt, client .p12, and GPG .asc staging files. Existing imported certificates and keyrings remain. The ordinary runtime Vim, SSH, and Docker files remain managed.

C/C++ language support

Vim installs coc-clangd from .chezmoidata/vim.toml through the plugin bootstrap. Neovim enables clangd through its native LSP client and nvim-lspconfig. Both use the clangd executable on PATH.

Host Provisioned package
Windows Scoop clangd
Ubuntu/Debian apt clangd
Fedora dnf clang-tools-extra
Bluefin Homebrew llvm
macOS Homebrew llvm; .commonprofile appends its keg-only bin directory to PATH

CoC also needs Node.js, which is already provisioned on every supported host. clangd provides completion, diagnostics, navigation, and formatting; a separate clang-format executable is not required for LSP formatting. Building code still requires the project's compiler and SDK/headers. For project-aware analysis, generate compile_commands.json (for example, configure CMake with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON using Ninja or Makefiles). For a small project, compile_flags.txt can supply include paths and compiler flags instead.