<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Chezmoi |</title><link>https://tiao.io/tags/chezmoi/</link><atom:link href="https://tiao.io/tags/chezmoi/index.xml" rel="self" type="application/rss+xml"/><description>Chezmoi</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Wed, 20 May 2026 00:00:00 +0000</lastBuildDate><image><url>https://tiao.io/media/icon_hu_9c2a75fde2335590.png</url><title>Chezmoi</title><link>https://tiao.io/tags/chezmoi/</link></image><item><title>Three eras of my dotfiles</title><link>https://tiao.io/posts/three-eras-of-dotfiles/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tiao.io/posts/three-eras-of-dotfiles/</guid><description>&lt;p&gt;Earlier today I ran &lt;code&gt;chezmoi init ltiao&lt;/code&gt; on my Arch laptop, expecting a fresh
copy of my dotfiles. What I got instead was an empty &lt;code&gt;chezmoi status&lt;/code&gt;, an
unfamiliar &lt;code&gt;~/.zshrc&lt;/code&gt;, and — after some confused poking around — a six-year-old
version of myself staring back at me from &lt;code&gt;~/.local/share/chezmoi/&lt;/code&gt;. Bullet-train
theme. neofetch. Solarized Dark. All of it. I had forgotten to pass
&lt;code&gt;--branch=main&lt;/code&gt;, and chezmoi had dutifully cloned &lt;code&gt;master&lt;/code&gt;, which still held the
pre-chezmoi flat-file history of the same repo.&lt;/p&gt;
&lt;p&gt;That little goof is how I ended up spending a couple of hours rebuilding my
developer environment for the third time. The result is the cleanest setup I&amp;rsquo;ve
had — one repo, one command to bootstrap a new machine, the same prompt and
shortcuts everywhere I work, and per-machine differences expressed declaratively
rather than as folklore in my head. This post is a tour of where it landed and
how it got here.&lt;/p&gt;
&lt;h2 id="era-1-the-bare-git-repo-trick"&gt;Era 1: The bare-git-repo trick&lt;/h2&gt;
&lt;p&gt;The original setup, circa late 2020, was about as minimal as it gets. From the
Notion page where I kept my install notes at the time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone --bare https://github.com/ltiao/dotfiles ~/dotfiles.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;alias&lt;/span&gt; &lt;span class="nv"&gt;gitdot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git --git-dir=$HOME/dotfiles.git --work-tree=$HOME&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gitdot status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The trick is that you keep a bare git repo at &lt;code&gt;~/dotfiles.git&lt;/code&gt; and pretend &lt;code&gt;$HOME&lt;/code&gt;
is its working tree. Run &lt;code&gt;gitdot add ~/.zshrc; gitdot commit; gitdot push&lt;/code&gt; and
your dotfiles are versioned. On a new machine, clone the bare repo and check
out the branch — files appear in &lt;code&gt;$HOME&lt;/code&gt; directly, no symlinks, no extra tools.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s elegant for a single machine. The whole &amp;ldquo;tool&amp;rdquo; is two lines of shell. No
templating, no install scripts, no abstractions — just git, doing what git does.&lt;/p&gt;
&lt;p&gt;The trouble starts when you have more than one machine. My personal Arch laptop
and my work MacBook both wanted slightly different &lt;code&gt;.zshrc&lt;/code&gt; files. The bare-repo
trick has no answer for that: branches drift, merges conflict over inessential
differences (a &lt;code&gt;brew shellenv&lt;/code&gt; line, a different plugin list), and you find
yourself maintaining a mental map of which file lives on which branch.&lt;/p&gt;
&lt;p&gt;The 2020 stack on top of the bare repo, for the historical record:
with the bullet-train theme,
on shell open,
the
terminal with a Solarized Dark theme, &lt;code&gt;pyenv&lt;/code&gt; and
&lt;code&gt;virtualenvwrapper&lt;/code&gt; for Python, and &lt;code&gt;~/.Xresources&lt;/code&gt; for the X11 apps I still
used at the time. Artifacts of taste at a specific moment.&lt;/p&gt;
&lt;h2 id="era-2-a-botched-first-chezmoi-attempt"&gt;Era 2: A botched first chezmoi attempt&lt;/h2&gt;
&lt;p&gt;A couple of years later I hit the multi-machine wall and decided to migrate to
. chezmoi is a small Go program that treats a git repo as the
source of truth for your dotfiles, then applies the contents (with optional
templating) to &lt;code&gt;$HOME&lt;/code&gt; on whatever machine you&amp;rsquo;re on. The exact problem the
bare-repo trick couldn&amp;rsquo;t handle.&lt;/p&gt;
&lt;p&gt;I ran the bootstrap command — &lt;code&gt;chezmoi init ltiao&lt;/code&gt; — got prompted for some
config, answered yes to apply, and then&amp;hellip; nothing visibly changed. &lt;code&gt;chezmoi status&lt;/code&gt; was empty. The new shell theme wasn&amp;rsquo;t applied. I shrugged, assumed I&amp;rsquo;d
misunderstood something, and moved on.&lt;/p&gt;
&lt;p&gt;What had actually happened: chezmoi&amp;rsquo;s default behavior is to clone the remote
repo&amp;rsquo;s default branch, which on my repo was still &lt;code&gt;master&lt;/code&gt; — the flat
pre-chezmoi history. The clone succeeded; there was just nothing template-shaped
inside it. chezmoi had nothing to apply.&lt;/p&gt;
&lt;p&gt;The wrong-branch clone sat there harmlessly while my old bare-repo setup kept
working. When I came back to figure out what had gone wrong, the answer turned
out to be that single missing flag. The lesson — and the punchline of this
whole post — is that getting your tooling right is harder than the tooling
itself. The recent revamp was the third try, and the one that finally stuck.&lt;/p&gt;
&lt;h2 id="era-3-the-setup-today"&gt;Era 3: The setup today&lt;/h2&gt;
&lt;p&gt;The current setup is one chezmoi repo at
, branch &lt;code&gt;main&lt;/code&gt;,
that bootstraps any of my machines with one command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi init --apply ltiao
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The repo&amp;rsquo;s &lt;code&gt;main&lt;/code&gt; is now the default branch on GitHub, so the &lt;code&gt;--branch=main&lt;/code&gt;
flag from the README is no longer necessary (and Era 2 becomes impossible to
repeat).&lt;/p&gt;
&lt;p&gt;
&lt;figure &gt;
&lt;div class="flex justify-center "&gt;
&lt;div class="w-full" &gt;&lt;img src="hero-shot.png" alt="TODO — hero shot of the full setup: Ghostty with Catppuccin Mocha, fastfetch banner, and a ready powerlevel10k prompt" loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;The rest of this section walks through what the setup actually consists of,
layer by layer.&lt;/p&gt;
&lt;h3 id="terminal-ghostty"&gt;Terminal: Ghostty&lt;/h3&gt;
&lt;p&gt;I moved off kitty about a year ago.
feels like the modern
default — fast, sensible config language, Catppuccin support out of the box,
splits, and per-platform niceties like macOS option-as-alt and translucent
backgrounds. The config is short:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;theme = Catppuccin Mocha
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;font-family = JetBrains Mono
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- if eq .environment &amp;#34;linux-desktop&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;font-size = 11
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- else }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;font-size = 14
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- end }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cursor-style = block
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;shell-integration = detect
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;window-save-state = always
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;window-padding-x = 4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;window-padding-y = 4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;unfocused-split-opacity = 0.7
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- if eq .chezmoi.os &amp;#34;darwin&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;macos-option-as-alt = true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;macos-titlebar-style = tabs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;background-opacity = 0.92
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;background-blur-radius = 20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- end }}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;{{- if ... }}&lt;/code&gt; syntax is Go template — chezmoi processes any source file
ending in &lt;code&gt;.tmpl&lt;/code&gt; through the Go template engine before writing it out, with
access to a small &lt;code&gt;data&lt;/code&gt; namespace populated from a config file. Here, my Linux
laptop and macOS get different font sizes (Linux&amp;rsquo;s font rendering is heavier, so
14pt looks oversized) and macOS gets a translucent titlebar that Linux doesn&amp;rsquo;t
need.&lt;/p&gt;
&lt;h3 id="shell-zsh--oh-my-zsh--powerlevel10k"&gt;Shell: zsh + oh-my-zsh + powerlevel10k&lt;/h3&gt;
&lt;p&gt;zsh as the shell, oh-my-zsh as a familiar plugin manager,
as the prompt. p10k has the right combination of fast-by-default, customizable,
and not-actively-hostile-to-readers. The instant-prompt feature in particular is
clever: it caches a snapshot of the prompt and renders that immediately on
shell startup, while the actual init runs in the background. The visible result
is that even a heavyweight &lt;code&gt;.zshrc&lt;/code&gt; produces a prompt within a few milliseconds.&lt;/p&gt;
&lt;p&gt;The snippet that enables it lives near the top of &lt;code&gt;.zshrc&lt;/code&gt;, before any other
init:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; -r &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XDG_CACHE_HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/.cache&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/p10k-instant-prompt-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;(%)&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;%n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.zsh&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XDG_CACHE_HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/.cache&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/p10k-instant-prompt-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;(%)&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;%n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.zsh&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;p10k offers to add that line automatically on first run, but I bake it into the
template so new machines have it from the start and the wizard never has to
prompt.&lt;/p&gt;
&lt;p&gt;
&lt;figure &gt;
&lt;div class="flex justify-center "&gt;
&lt;div class="w-full" &gt;&lt;img src="p10k-prompt.png" alt="TODO — close-up of the powerlevel10k prompt across two lines, ideally showing the git status segment with a dirty working tree" loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;The OMZ plugin list is short and deliberate — &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;z&lt;/code&gt;,
&lt;code&gt;colored-man-pages&lt;/code&gt;, &lt;code&gt;zsh-autosuggestions&lt;/code&gt;, &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt;, plus a
handful of &amp;ldquo;noun-helper&amp;rdquo; plugins for things I use a lot: &lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;direnv&lt;/code&gt;, etc.
Extra zsh files live in &lt;code&gt;~/.oh-my-zsh/custom/&lt;/code&gt; — one per concern (history and
aliases, Claude launchers, etc.), each short enough to hold in my head.&lt;/p&gt;
&lt;h3 id="multiplexer-tmux-but-only-on-remote-servers"&gt;Multiplexer: tmux, but only on remote servers&lt;/h3&gt;
&lt;p&gt;I don&amp;rsquo;t use tmux on my local machines — I have splits and tabs in Ghostty,
which covers the same ground without the wrapper layer. On remote servers
it&amp;rsquo;s a different story. SSH sessions die, and any long-running process dies
with them; tmux solves that.&lt;/p&gt;
&lt;p&gt;The repo deploys &lt;code&gt;.tmux.conf&lt;/code&gt; only on remote machines. Locally, &lt;code&gt;chezmoi apply&lt;/code&gt;
correctly skips it via a single line in &lt;code&gt;.chezmoiignore&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- if or (eq .environment &amp;#34;mac&amp;#34;) (eq .environment &amp;#34;linux-desktop&amp;#34;) }}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.tmux.conf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- end }}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the pattern for everything env-specific: list it under a conditional in
&lt;code&gt;.chezmoiignore&lt;/code&gt; and chezmoi never deploys it on the wrong machine.&lt;/p&gt;
&lt;h3 id="dotfiles-management-chezmoi"&gt;Dotfiles management: chezmoi&lt;/h3&gt;
&lt;p&gt;The chezmoi mental model, once it clicks, is small. You have a source directory
(default &lt;code&gt;~/.local/share/chezmoi/&lt;/code&gt;) which is a git repo. Files in there with a
&lt;code&gt;dot_&lt;/code&gt; prefix get the &lt;code&gt;dot_&lt;/code&gt; replaced with &lt;code&gt;.&lt;/code&gt; and the file written to &lt;code&gt;$HOME&lt;/code&gt;.
Files ending in &lt;code&gt;.tmpl&lt;/code&gt; get rendered through Go templates first. Files named
&lt;code&gt;run_onchange_*.sh.tmpl&lt;/code&gt; are scripts that re-run any time their content
changes. &lt;code&gt;.chezmoiignore&lt;/code&gt; selects which files to skip, with template support.
That&amp;rsquo;s about 80% of what you need.&lt;/p&gt;
&lt;p&gt;Day-to-day editing goes through chezmoi rather than the destination file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi edit ~/.zshrc &lt;span class="c1"&gt;# opens dot_zshrc.tmpl in $EDITOR&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi apply &lt;span class="c1"&gt;# renders it back to ~/.zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi diff &lt;span class="c1"&gt;# preview before applying&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chezmoi update &lt;span class="c1"&gt;# git pull + chezmoi apply in one&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The discipline is that you treat the source as authoritative and never edit
&lt;code&gt;~/.zshrc&lt;/code&gt; directly — otherwise you create drift between the two, and chezmoi
will eventually prompt you about it. It&amp;rsquo;s a small habit shift; the payoff is
that &lt;code&gt;chezmoi update&lt;/code&gt; is the entire sync workflow on every other machine.&lt;/p&gt;
&lt;h3 id="multi-env-model"&gt;Multi-env model&lt;/h3&gt;
&lt;p&gt;Three environments matter for my dotfiles:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Package manager&lt;/th&gt;
&lt;th&gt;Git auth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS laptop&lt;/td&gt;
&lt;td&gt;Homebrew&lt;/td&gt;
&lt;td&gt;macOS Keychain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arch desktop / laptop&lt;/td&gt;
&lt;td&gt;pacman&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gh auth git-credential&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote work servers&lt;/td&gt;
&lt;td&gt;dnf&lt;/td&gt;
&lt;td&gt;(handled by the platform)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The chezmoi templates branch on two data variables, &lt;code&gt;work&lt;/code&gt; (a boolean
distinguishing work-vs-personal machines) and &lt;code&gt;environment&lt;/code&gt; (a string
identifying which of the three contexts), populated when I first ran &lt;code&gt;chezmoi init&lt;/code&gt; on each machine. The
bootstrap install script picks the right package manager:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;_pkg_install&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; eq .chezmoi.os &lt;span class="s2"&gt;&amp;#34;darwin&amp;#34;&lt;/span&gt; -&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; brew install &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; eq .environment &lt;span class="s2"&gt;&amp;#34;linux-desktop&amp;#34;&lt;/span&gt; -&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sudo pacman -S --noconfirm &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; -&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sudo dnf install -y &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{{&lt;/span&gt; end -&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The gitconfig template branches similarly to pull in &lt;code&gt;gh auth git-credential&lt;/code&gt;
helpers on the Linux desktop, where I use &lt;code&gt;gh&lt;/code&gt; for GitHub auth — but not on
macOS (Keychain handles it) or on work servers (different auth model entirely).&lt;/p&gt;
&lt;p&gt;This is the part that took the longest to design right. The temptation when
your config diverges across machines is to maintain three branches, or three
repos, or three files with different names. chezmoi&amp;rsquo;s templating lets you keep
one repo and express divergence as conditionals, which means changes propagate
to all machines unless you actively gate them. That asymmetry — share by
default, diverge by exception — matches how I actually want to think about it.&lt;/p&gt;
&lt;h3 id="tool-layer-fzf-fastfetch-uv-claude-launchers"&gt;Tool layer: fzf, fastfetch, uv, Claude launchers&lt;/h3&gt;
&lt;p&gt;The shell config wires up a handful of tools that aren&amp;rsquo;t shell themselves but
shape day-to-day usage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;
&lt;/strong&gt; for fuzzy file/command/history search. Bound to the usual
&lt;code&gt;Ctrl-R&lt;/code&gt;, &lt;code&gt;Ctrl-T&lt;/code&gt;, &lt;code&gt;Alt-C&lt;/code&gt; keys via the oh-my-zsh &lt;code&gt;fzf&lt;/code&gt; plugin.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;
&lt;/strong&gt; prints system info on shell open. (neofetch&amp;rsquo;s
unmaintained-and-archived state finally pushed me off it.) Worth noting:
fastfetch has to run &lt;em&gt;before&lt;/em&gt; the p10k instant-prompt block in &lt;code&gt;.zshrc&lt;/code&gt;,
because output during the captured-init region triggers a noisy warning. The
fix is a one-liner above the preamble, which I&amp;rsquo;ll come back to in the next
section.&lt;/p&gt;
&lt;p&gt;
&lt;figure &gt;
&lt;div class="flex justify-center "&gt;
&lt;div class="w-full" &gt;&lt;img src="fastfetch.png" alt="TODO — fastfetch output on shell open, showing OS / kernel / shell / DE / CPU / GPU / memory block alongside the distro ASCII logo" loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;
&lt;/strong&gt; for everything GitHub — auth, PR review, repo edits. Setting it
as the git credential helper via &lt;code&gt;gh auth setup-git&lt;/code&gt; is what makes
&lt;code&gt;git push&lt;/code&gt; from chezmoi-managed repos work without a separate password
prompt.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;
&lt;/strong&gt; for per-project environment loading.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;
&lt;/strong&gt; for Python. Single Rust binary that replaces pyenv, virtualenv,
virtualenvwrapper, pip, and pipx — all in one. Faster than any of them, and
importantly, doesn&amp;rsquo;t need any shell-init dance: there&amp;rsquo;s no
&lt;code&gt;eval &amp;quot;$(uv init)&amp;quot;&lt;/code&gt; equivalent, so the whole &amp;ldquo;pyenv shims aren&amp;rsquo;t on PATH&amp;rdquo;
class of problems just doesn&amp;rsquo;t exist.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The one I&amp;rsquo;ll spend a paragraph on is the &lt;strong&gt;Claude Code launcher&lt;/strong&gt; — a small
set of aliases I use heavily and which is the best example I have of why
multi-env templating earns its keep.&lt;/p&gt;
&lt;p&gt;I want one command, &lt;code&gt;cc&lt;/code&gt;, that starts Claude Code with whatever model and
context settings I prefer. On my local machines, that&amp;rsquo;s just a shell function
that calls &lt;code&gt;claude&lt;/code&gt; with some flags. On a remote server, I want the same
command to wrap the session in tmux so that an SSH disconnect doesn&amp;rsquo;t kill it
— and I want to set &lt;code&gt;CLAUDE_CODE_NO_FLICKER=1&lt;/code&gt; because Claude&amp;rsquo;s default redraws
otherwise confuse tmux&amp;rsquo;s scrollback. Same alias, different behavior, expressed
once in a template:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-zsh" data-lang="zsh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;_cc&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%s&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;shift&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{{&lt;/span&gt;- &lt;span class="k"&gt;if&lt;/span&gt; and .work &lt;span class="o"&gt;(&lt;/span&gt;ne .environment &lt;span class="s2"&gt;&amp;#34;mac&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;CLAUDE_CODE_NO_FLICKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$TMUX&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;command&lt;/span&gt; claude &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; tmux has-session -t &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ta &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; tmux new-session -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;CLAUDE_CODE_NO_FLICKER=1 claude &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;(j: :)&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;(q)@&lt;/span&gt;&lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt; attach-session -t &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{{&lt;/span&gt;- &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;command&lt;/span&gt; claude &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{{&lt;/span&gt;- end &lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cc&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; _cc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%s&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --model &lt;span class="s1"&gt;&amp;#39;claude-opus-4-6[1m]&amp;#39;&lt;/span&gt; --effort max &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;@:&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cch&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; _cc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%s&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --model &lt;span class="s1"&gt;&amp;#39;claude-opus-4-6[1m]&amp;#39;&lt;/span&gt; --effort high &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;@:&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cc7&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; _cc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%s&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --model &lt;span class="s1"&gt;&amp;#39;claude-opus-4-7[1m]&amp;#39;&lt;/span&gt; --effort max &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;@:&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ccs&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; _cc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%s&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --model &lt;span class="s1"&gt;&amp;#39;claude-sonnet-4-6[1m]&amp;#39;&lt;/span&gt; --effort max &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;@:&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ... more variants for different model/context/effort combinations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Same muscle memory across every machine, with the wrapping that each
environment needs added invisibly. A &lt;code&gt;cchelp&lt;/code&gt; function dumps the full list of
variants for when I forget which suffix means what.&lt;/p&gt;
&lt;h2 id="the-hard-parts"&gt;The hard parts&lt;/h2&gt;
&lt;p&gt;Not everything was smooth. Three problems I hit during this rewrite are worth
sharing because they&amp;rsquo;re easy traps and the symptoms don&amp;rsquo;t always point at the
real cause.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;.chezmoiignore&lt;/code&gt; patterns must use destination paths, not source paths.&lt;/strong&gt;
This bit me hardest. chezmoi&amp;rsquo;s source files are named &lt;code&gt;dot_zshrc.tmpl&lt;/code&gt;,
&lt;code&gt;dot_tmux.conf&lt;/code&gt;, etc. — &lt;code&gt;dot_&lt;/code&gt; prefix, optional &lt;code&gt;.tmpl&lt;/code&gt; suffix. When you list
patterns in &lt;code&gt;.chezmoiignore&lt;/code&gt;, it&amp;rsquo;s natural to write &lt;code&gt;dot_tmux.conf&lt;/code&gt;. That&amp;rsquo;s
&lt;em&gt;wrong&lt;/em&gt;. chezmoi expects destination paths there — &lt;code&gt;.tmux.conf&lt;/code&gt;. The kicker:
chezmoi accepts the wrong form silently. Patterns using source paths match
nothing and are no-ops. I had a &lt;code&gt;.chezmoiignore&lt;/code&gt; rule that was supposed to skip
&lt;code&gt;.tmux.conf&lt;/code&gt; on local machines for two years, and it had been doing nothing the
whole time. Other rules in the same file appeared to work, but only because the
files they pointed at rendered empty under their internal template guards. The
fix is a one-character search-and-replace, but finding the bug needed a careful
read of chezmoi&amp;rsquo;s docs and a moment of realization while staring at the wrong
file showing up in &lt;code&gt;$HOME&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Oh-my-zsh plugin load order vs. your custom init.&lt;/strong&gt; I had a custom
&lt;code&gt;~/.oh-my-zsh/custom/pyenv.zsh&lt;/code&gt; that ran &lt;code&gt;pyenv init -&lt;/code&gt;, plus the oh-my-zsh
&lt;code&gt;pyenv&lt;/code&gt; plugin in my plugin list. Every shell startup printed &lt;code&gt;Found pyenv, but it is badly configured (missing pyenv shims in $PATH)&lt;/code&gt;. I assumed the warning
was from pyenv itself, spent some time chasing PATH ordering, and only realized
much later that the warning is emitted by the OMZ pyenv plugin — which loads
during &lt;code&gt;source $ZSH/oh-my-zsh.sh&lt;/code&gt;, &lt;em&gt;before&lt;/em&gt; &lt;code&gt;~/.oh-my-zsh/custom/*.zsh&lt;/code&gt; files
get sourced. My custom init was running too late. The fix is either to move
pyenv init above &lt;code&gt;plugins=()&lt;/code&gt;, or drop the redundant OMZ plugin and rely on the
custom file. I went with the third option: switch to uv and delete all of it.&lt;/p&gt;
&lt;p&gt;
&lt;figure &gt;
&lt;div class="flex justify-center "&gt;
&lt;div class="w-full" &gt;&lt;img src="p10k-instant-prompt-warning.png" alt="TODO — screenshot of the &amp;ldquo;Console output during zsh initialization detected&amp;rdquo; warning from p10k, with the captured-output region visible below the &amp;ldquo;─────&amp;rdquo; line" loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. p10k&amp;rsquo;s instant-prompt and the &amp;ldquo;console output during init&amp;rdquo; warning.&lt;/strong&gt;
Instant-prompt&amp;rsquo;s contract is that nothing should write to the console between
the preamble and the first prompt — otherwise p10k can&amp;rsquo;t cleanly buffer and
flush the output, and you get an angry warning every time you open a shell.
fastfetch prints to the console by definition, so I had a problem. My first
attempt was a deferred &lt;code&gt;precmd&lt;/code&gt; hook — register a one-shot callback that calls
fastfetch right before the first prompt — which seemed clean but still
triggered the warning, because p10k&amp;rsquo;s own finalize hook is registered &lt;em&gt;after&lt;/em&gt;
mine (it lives in &lt;code&gt;~/.p10k.zsh&lt;/code&gt;, which is sourced last). The fix that actually
worked is the dumbest one: just move the &lt;code&gt;fastfetch&lt;/code&gt; call &lt;em&gt;before&lt;/em&gt; the
instant-prompt preamble in &lt;code&gt;.zshrc&lt;/code&gt;. Now it runs before init starts, gets
written directly to the terminal, and the prompt renders cleanly under it.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;The two metrics I judge a dotfiles setup by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How long does it take to bootstrap a fresh machine to a working state?&lt;/li&gt;
&lt;li&gt;How long does it take to propagate a change to all my machines?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For (1) the answer is now &lt;code&gt;chezmoi init --apply ltiao&lt;/code&gt; plus the time pacman or
brew takes to install five extra packages. Total wall-clock under five minutes,
most of which is network. For (2) it&amp;rsquo;s &lt;code&gt;chezmoi edit &amp;lt;file&amp;gt;; chezmoi apply&lt;/code&gt; on
one machine, &lt;code&gt;git commit &amp;amp;&amp;amp; git push&lt;/code&gt;, then &lt;code&gt;chezmoi update&lt;/code&gt; on each of the
others — a workflow short enough to be cheap.&lt;/p&gt;
&lt;p&gt;It took three takes to get here, and the third take wasn&amp;rsquo;t smooth either —
this post itself is partly a record of the bugs I found while writing it. But
the time invested in tooling compounds. A couple of hours of fiddly migration
buys years of fast machine setups. That trade is one of the best I make at this
point in my career.&lt;/p&gt;
&lt;p&gt;Up next: nothing urgent. I might write a follow-up on the Claude Code launcher
itself — the version above is doing real work, and the env-conditional model
generalizes to anything where the same alias should behave differently in
different contexts. If you&amp;rsquo;d like to compare notes on your own setup, the repo
is at
.&lt;/p&gt;</description></item></channel></rss>