In the previous article, we encrypted our SSH keys with age and stored them in the chezmoi repo. That’s great — but there’s a gap. When we set up a new machine and run chezmoi init, chezmoi needs age to decrypt those keys. And as we saw earlier in the series, the version of age available via apt on Debian is too old for post-quantum support.
Right now, installing the correct version of age is still a manual step. Time to fix that.
chezmoi’s run_once Scripts#
Chezmoi has a concept called run_once scripts. These are shell scripts that chezmoi executes exactly once per machine — on the first chezmoi apply. If the script’s contents haven’t changed since the last run, chezmoi skips it. Change the script, and it runs again on the next apply.
This is perfect for package installation. We want age (and eventually other tools) to be installed automatically when setting up a new machine, but we don’t want the script to re-run on every chezmoi apply.
The naming convention is important: the script must start with run_once_ for chezmoi to treat it correctly. I’m calling mine run_once_install-packages.sh.
The Script#
Let’s create the script in the chezmoi source directory:
chezmoi cd
touch run_once_install-packages.sh
chmod +x run_once_install-packages.shHere’s the full script:
#!/bin/bash
set -euo pipefail
# Use sudo if available and not root
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo &> /dev/null; then
SUDO="sudo"
fi
AGE_VERSION="1.3.1"
# Detect OS
if [ -f /etc/debian_version ]; then
$SUDO apt-get update -y
$SUDO apt-get upgrade -y
$SUDO apt-get install -y \
zsh
cd /tmp
curl -LO "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-linux-amd64.tar.gz"
tar xzf "age-v${AGE_VERSION}-linux-amd64.tar.gz"
$SUDO mv age/age age/age-keygen /usr/local/bin/
rm -rf age "age-v${AGE_VERSION}-linux-amd64.tar.gz"
cd ~
fiLet’s walk through the key parts.
Handling sudo#
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo &> /dev/null; then
SUDO="sudo"
fiNot every environment has sudo, and sometimes you’re already root — like in a Docker container. This block checks both conditions and sets a $SUDO variable accordingly. Throughout the rest of the script, commands that need elevated privileges use $SUDO instead of hardcoding sudo.
OS Detection#
if [ -f /etc/debian_version ]; then
# ...
fiThe script checks for /etc/debian_version to confirm we’re on a Debian-based system. Right now, that’s the only OS I’m targeting. If I ever need to support macOS or another distro, I can add an elif branch later. For now, keeping it simple.
apt Packages#
$SUDO apt-get update -y
$SUDO apt-get upgrade -y
$SUDO apt-get install -y \
zshBefore installing age from source, we update the package index and install any tools that are fine to get from apt. Right now that’s just zsh, but this list will grow as the dotfiles setup matures — tmux, neovim, and other tools can be added here.
Installing age From GitHub Releases#
AGE_VERSION="1.3.1"
cd /tmp
curl -LO "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-linux-amd64.tar.gz"
tar xzf "age-v${AGE_VERSION}-linux-amd64.tar.gz"
$SUDO mv age/age age/age-keygen /usr/local/bin/
rm -rf age "age-v${AGE_VERSION}-linux-amd64.tar.gz"
cd ~This is the same manual process from the earlier post, but now it’s automated. The version is pinned at the top of the script as a variable, making it easy to bump when a new release comes out. And because this is a run_once script, chezmoi will re-run it when the version number changes — so updating age across all machines is as simple as changing one line and running chezmoi apply.
Adding the Script to chezmoi#
Since we created the script directly in the chezmoi source directory, it’s already in the right place. Let’s commit it:
chezmoi cd
git add run_once_install-packages.sh
git commit -m "add run_once script for automated package installation"
git pushNow, on any new machine, running chezmoi apply will automatically install zsh and the correct version of age before anything else happens.
Testing It#
The best way to test this is with the disposable container setup from earlier in the series. Spin up a fresh container, install chezmoi, and run chezmoi init --apply. The script should install everything automatically, and age-keygen --version should report v1.3.1.
What We’ve Achieved So Far#
Let’s take a step back and look at where we are. Testing the full dotfile setup inside a disposable container now comes down to a handful of manual steps:
podman run -it --rm debian:bookworm-slim bash
apt update -y && apt upgrade -y && apt install curl git nano -y
mkdir -p ~/.config/age
chmod 700 ~/.config/age
touch ~/.config/age/dotfiles-repo-key.txt
chmod 600 ~/.config/age/dotfiles-repo-key.txt
nano ~/.config/age/dotfiles-repo-key.txt # (paste in key from secure location)
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply knuth-info
zshWe start a fresh Debian container, install the bare minimum — curl, git, and nano — then create the age key directory and paste in the private key from a secure location. After that, a single chezmoi command pulls down the repo, runs the run_once script to install zsh and the correct version of age, decrypts the SSH keys, and applies everything. The last step is just switching to zsh.
That’s a lot less than where we started. No more manually downloading age binaries, no more copying SSH keys into containers by hand. The only truly manual part left is placing the age key — which is intentional, since that secret should never be automated into a repo.
What’s Next#
With package installation automated, the setup flow for a new machine is getting tighter. We still need to handle a few things — like configuring git with the right name and email, and switching the chezmoi remote from HTTPS to SSH once the keys are decrypted. We’ll tackle that in the next post.
