Document how to bring chezmoi up on another VPS without the provisioning layer, and record the chezmoi behaviours that caused real breakage here. The umask trap is the load-bearing one: chezmoi derives modes from the umask at apply time rather than preserving them, so the atuin key dir and config need both the private_ attribute and umask = 0o022 to land at 0700 and 0600. The apply-into-head SIGPIPE is the other: it truncates the apply and reports head's exit status, so a partial apply looks successful. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.0 KiB
dotfiles
Shell + tooling setup for a fresh Ubuntu box: fish · starship · atuin · eza · macchina, plus the Claude Code statusline.
Two layers, deliberately split:
| Layer | Owns | Tool |
|---|---|---|
| Provisioning | apt packages, rust toolchain, starship, default shell | setup.sh (root) |
| Dotfile state | everything under $HOME |
chezmoi (user) |
setup.sh does the root work and then hands off to chezmoi. Nothing that needs
sudo lives inside chezmoi, and nothing under $HOME is touched by the script.
Fresh box
git clone https://git.domverse-berlin.eu/domverse/dotfiles.git
cd dotfiles
sudo bash setup.sh
Run it as your normal user via sudo — the script reads SUDO_USER to know whose
home to set up, and refuses to run as root directly.
Expect 10–20 minutes: atuin, eza, macchina and oxker are built from source with
cargo install --locked, which needs the rust toolchain. That's the deliberate
trade for matching the currently-running versions exactly rather than taking
whatever apt ships.
chezmoi on another VPS
setup.sh already does all of this. Use this path only when you want the dotfiles
without the provisioning layer (no apt, no rust, no chsh) — e.g. a box where the
tools are already there, or where you don't have root.
# 1. install chezmoi (user-local, no root)
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin"
export PATH="$HOME/.local/bin:$PATH"
# 2. pin the umask BEFORE the first apply — see Notes, this is load-bearing
mkdir -p ~/.config/chezmoi
printf 'umask = 0o022\n' >> ~/.config/chezmoi/chezmoi.toml
# 3. pull the source state (private repo — needs Gitea credentials)
chezmoi init https://git.domverse-berlin.eu/domverse/dotfiles.git
# 4. look before you leap
chezmoi diff
# 5. write it out
chezmoi apply
Notes on the above:
- Step 2 must come before step 4. The umask is read at apply time; applying first and fixing the config after leaves wrong modes on disk.
- Step 3 needs repo access. The repo is private. Either have
credential.helper=storealready populated, or clone over SSH:chezmoi init git@git.domverse-berlin.eu:domverse/dotfiles.git. - The source clone lands in
~/.local/share/chezmoi(chezmoi's default). This box is the exception — it setssourceDir = /home/crabix/projects/dotfilesin~/.config/chezmoi/chezmoi.tomlbecause the repo predates chezmoi. On a fresh box, don't setsourceDir; the default is whatsetup.shexpects. .chezmoirootcontainshome, so the source root ishome/, not the repo root.setup.shandREADME.mdsit outside chezmoi's view by design.- If
~already has real dotfiles,chezmoi diffin step 4 shows exactly what gets clobbered. Back them up with modes intact first:cp -a ~/.config/fish ~/.config/atuin ~/backup/dotfiles-preapply-$(date -u +%Y%m%dT%H%M%SZ)/ - Then do the Manual steps below (atuin key, git identity) — they are not
specific to
setup.sh.
Day-to-day
Configs are managed by chezmoi, so the file in ~/.config is a copy, not a
symlink. Edit through chezmoi or your change gets overwritten on the next apply:
chezmoi edit ~/.config/fish/config.fish # edit source
chezmoi diff # what drifted on this box?
chezmoi apply # write source -> $HOME
chezmoi cd # jump to the source repo, then git push
The fishconfig alias is wired to chezmoi edit for this reason.
What's tracked
home/
dot_bashrc ~/.bashrc (stock + atuin init)
dot_gitconfig ~/.gitconfig
dot_config/fish/config.fish.tmpl aliases, eza, init lines
dot_config/fish/conf.d/rustup.fish sources ~/.cargo/env.fish
dot_config/atuin/config.toml
dot_config/starship.toml stock-equivalent, pinned
dot_claude/executable_statusline-command.sh statusline (needs jq)
What's deliberately NOT tracked
See home/.chezmoiignore. The load-bearing exclusions:
~/.local/share/atuin/key— the sync encryption key. Tracked by hand insecrets.yml. Restore it before the firstatuin sync, or history encrypted on other hosts will not decrypt.~/.git-credentials— plaintext tokens (.gitconfigsetshelper = store).~/.claude/settings.json— pins the model and setsskipDangerousModePermissionPrompt; a per-box decision, set by hand.conf.d/fnm.fish,conf.d/dtop.env.fish— node/opencode toolchains, out of scope. Their installers write these themselves.fish_variables— machine-local state, fish rewrites it.
Manual steps after setup.sh
The script prints these; repeated here because they're easy to miss:
- atuin — restore key from
secrets.ymlinto~/.local/share/atuin/key, thenatuin login -u <user>andatuin sync. - git identity — not set globally, only
credential.helper=store:git config --global user.name '<name>' git config --global user.email '<email>' - Log out and back in for the fish shell change to take effect.
chezmoi gotchas
Learned the hard way while building this. Don't rediscover them.
-
chezmoi does not preserve file modes. It tracks only the
executable_andprivate_source attributes and derives the actual mode from the umask at apply time. Withumask 002(the default for this user),~/.config/atuin/silently goes0700→0775andconfig.toml0600→0664. Correct modes need both halves:- the source attribute —
private_atuin/private_config.toml, and umask = 0o022in~/.config/chezmoi/chezmoi.toml(setup.shwrites this).
Always
chezmoi diffbefore trusting a dotfiles repo on a new box; the mode lines are quiet and easy to scroll past. - the source attribute —
-
Never pipe
chezmoi applyinto a truncating command.chezmoi apply --verbose | head -20SIGPIPEs chezmoi partway through, leaving the apply incomplete — files sorting after the cutoff are never written. Worse,$?reports head's exit status (0), so it looks like it succeeded. Redirect to a file and read that instead:chezmoi apply --verbose > /tmp/apply.log 2>&1; echo "exit=$?" -
The file in
~is a copy, not a symlink. Editing~/.config/fish/config.fishdirectly gets silently clobbered on the next apply. Usechezmoi edit. Thefishconfigalias points atchezmoi editfor exactly this reason. -
chezmoi initon a box with existing dotfiles overwrites without asking once youapply.chezmoi difffirst, always.
Notes
config.fishaliasescattobatcat— on Ubuntu the bat binary really isbatcat, so the aptbatpackage is a hard dependency of the fish config.- rustup is installed with
--no-modify-path:conf.d/rustup.fishalready sources~/.cargo/env.fish, and the installer would otherwise add a second PATH edit. starship.tomlis currently equivalent to stock defaults. It exists so the prompt is pinned to a known config rather than tracking version defaults.