If you use chezmoi_modify_manager together with chezmoi, there is one annoying bootstrap problem:
chezmoi_modify_manageris needed whilechezmoi applycomputes file contents- but the usual
.chezmoiscripts/run_before_*hooks run too late to install it for the first apply
I wanted a solution that was:
- automatic
- shell-based
- version-pinned
- rerun only when the installer script changes
This post shows the setup I ended up with.
The problem
At first glance, it seems natural to put an installer script into .chezmoiscripts, for example, as a run_onchange_before_* script.
That helps with normal lifecycle management, but it does not solve the initial bootstrap.
Why? Because chezmoi needs to compute the target state before it runs those scripts, and modify_ files participate in that computation. If those files use chezmoi_modify_manager, then the binary must already exist before chezmoi apply reaches the script phase.
So the core issue is:
chezmoi_modify_managermust be installed beforechezmoi applystarts doing the work that depends on it.
The approach
The solution is to split the problem into two:
- Put the installer in
.chezmoiscriptsas arun_onchange_before_*script, so the script is tracked bychezmoiand reruns when its contents change. - Wrap
chezmoiwith a small shell function that manually runs that installer script before delegating to the realchezmoi apply.
That gives us the best of both worlds:
- proper
chezmoi-managed script in the source repo - successful first bootstrap
- automatic reinstall when the installer changes, such as when bumping the pinned version
The installer script
I placed this file in:
~/.local/share/chezmoi/.chezmoiscripts/run_onchange_before_00-install-chezmoi-modify-manager.sh
The script is version-pinned, architecture-aware, and idempotent. It also keeps a tiny version stamp, so it can skip work when the correct version is already installed.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#!/usr/bin/env bash set -euo pipefail VERSION='v3.7.0' INSTALL_DIR="$HOME/.local/bin" BIN="$INSTALL_DIR/chezmoi_modify_manager" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/chezmoi" STAMP="$STATE_DIR/chezmoi_modify_manager.version" BASE_URL="https://github.com/VorpalBlade/chezmoi_modify_manager/releases/download/${VERSION}" mkdir -p "$INSTALL_DIR" "$STATE_DIR" if [ -x "$BIN" ] && [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$VERSION" ]; then exit 0 fi case "$(uname -m)" in x86_64) ASSET="chezmoi_modify_manager-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" ;; aarch64|arm64) ASSET="chezmoi_modify_manager-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" ;; *) echo "Unsupported architecture: $(uname -m)" >&2 exit 1 ;; esac tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT curl -fL "${BASE_URL}/${ASSET}" -o "$tmpdir/${ASSET}" tar -xzf "$tmpdir/${ASSET}" -C "$tmpdir" install -m 0755 "$tmpdir/chezmoi_modify_manager" "$BIN" printf '%s\n' "$VERSION" > "$STAMP" "$BIN" --doctor |
A few notes:
VERSIONis deliberately hardcoded for simplicitySTAMPstores the installed version so the script can cheaply detect whether work is needed--doctorprovides a sanity check after installation
If you later want to upgrade, just change the VERSION string in the script. Because the script is a run_onchange_* script, chezmoi will detect the change.
Manual bootstrapping
After running “chezmoi init <URL>” and before running “chezmoi apply” for the first time, you must remember to manually execute this script
|
1 |
sh ~/.local/share/chezmoi/.chezmoiscripts/run_onchange_before_00-install-chezmoi-modify-manager.sh |
Alternatively, you can use the “wrapper” solution detailed next.
The wrapper
Now for the missing bootstrap step: we need to ensure the installer runs before chezmoi apply.
I used a shell function in my ~/.bashrc (or ~/.zshrc).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
chezmoi() { local real_chezmoi source_dir installer real_chezmoi="$(type -P chezmoi)" if [ "${1-}" = "apply" ]; then source_dir="$("$real_chezmoi" source-path)" installer="$source_dir/.chezmoiscripts/run_onchange_before_00-install-chezmoi-modify-manager.sh" if [ -f "$installer" ]; then bash "$installer" fi fi "$real_chezmoi" "$@" } |
Then reload your shell:
|
1 2 |
source ~/.bashrc |
With this in place, running:
|
1 2 |
chezmoi apply |
does the following:
- finds the real
chezmoi - checks whether the installer script exists in the source directory
- runs the installer script first
- only then runs the real
chezmoi apply
That is exactly what we need.
Why this works
This setup works because it respects the real dependency order.
The dependency is not:
“Run a script sometime before applying files.”
The dependency is:
“Ensure
chezmoi_modify_managerexists beforechezmoistarts evaluating anything that depends on it.”
A normal run_before_* script is still part of chezmoi’s internal apply pipeline, which is already too late for the very first bootstrap.
The shell wrapper moves the install step out of the pipeline and places it before chezmoi apply begins.
Why keep the installer in .chezmoiscripts at all?
Because it still gives you useful chezmoi behavior:
- the installer lives in your dotfiles repo
- it is versioned like everything else
- changing the script content is part of your normal dotfiles workflow
- the
run_onchange_*naming still reflects the script’s intended lifecycle
In other words, the wrapper solves the bootstrap timing problem, while .chezmoiscripts keeps the implementation under chezmoi ‘s control.
Optional: ensure ~/.local/bin is on your PATH
If your shell does not already include ~/.local/bin, add it:
|
1 2 3 4 |
grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' ~/.bashrc || \ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc export PATH="$HOME/.local/bin:$PATH" |
Happy dotfiles! 🙂