~/.zshrc を置かない dotfiles:ZDOTDIR と chezmoi
長く使った ~/.zshrc を見ると、自分で書いた行とインストーラーが追加した行を区別できなくなっていた。
インストールスクリプトは簡単に初期化処理を追記するが、ツールを消してもその行は残る。ファイルが一応動いているので触りづらくなり、さらに追記だけが増えていく。
今はホームディレクトリに ~/.zshenv だけを残し、ほかの zsh 設定を ~/.config/zsh/ に置いて chezmoi で管理している。最初の記事で書いたレイヤー分けを dotfiles に適用した形だ。
ZDOTDIR で設定を移動する
macOS の interactive login shell は、おおむね次の順で設定を読む。
/etc/zshenv → ~/.zshenv (always, every shell)/etc/zprofile → ~/.zprofile (login shells)/etc/zshrc → ~/.zshrc (interactive shells)/etc/zlogin → ~/.zlogin (login shells)~/.zshenv はすべての zsh で最初に読まれる。ここで ZDOTDIR を設定すると、.zprofile、.zshrc、.zlogin の探索先が $HOME から $ZDOTDIR に変わる。
# ~/.zshenv — the only zsh file allowed to live in $HOMEexport ZDOTDIR="${XDG_CONFIG_HOME:-$HOME/.config}/zsh"これは zsh が正式にサポートする仕組みだ。VS Code の integrated terminal も ~/.zshenv を読む。古いツールの一部は ~/.zshrc を決め打ちしているが、私の環境では問題になることは少なかった。
長い一ファイルを作らない
移動先で再び巨大な .zshrc を作らないよう、.zshrc 自体は番号順に fragment を読むだけにした。
# ~/.config/zsh/.zshrc — loads every fragment in numeric orderfor _file in "${ZDOTDIR}"/conf.d/*.zsh(N); do source "$_file"doneunset _file(N) は、対象がないときにエラーではなく空へ展開する zsh の glob qualifier。
~/.config/zsh/conf.d/├── 00-env.zsh # exported env vars, PATH base├── 10-completion.zsh # compinit and completion styles├── 20-aliases.zsh # short renames of existing commands├── 30-functions.zsh # shell functions that do real work├── 35-tools.zsh # eval-hooks for external CLIs (mise, zoxide, …)├── 40-lang.zsh # language/runtime-specific setup├── 50-plugins.zsh # zsh-ecosystem plugins└── 90-local.zsh # machine-specific, not tracked in git環境変数、関数、外部 CLI が生成する hook、言語設定、プラグインの順に分ける。prompt が runtime 情報を使う場合は mise の activation より後に読み、syntax highlighting は line editor を wrap するので最後に置く。
PATH を重複させない
~/.zshenv は子 shell でも実行されるため、
export PATH="$HOME/.local/bin:$PATH"をそのまま置くと tmux、subshell、exec zsh のたびに同じパスが増える。
# Prepend to PATH only if not already present.path_prepend() { case ":$PATH:" in *":$1:"*) ;; # already there — do nothing *) PATH="$1:$PATH" ;; esac}
path_prepend "$HOME/.local/bin"PATH の両端にもコロンを付けることで、先頭と末尾の要素も完全一致で確認できる。この関数は 00-env.zsh に置く。
chezmoi の source と target
Git リポジトリを $HOME に直接 symlink する代わりに chezmoi を使う。source directory からホームディレクトリへレンダリングし、source 側のファイル名で属性も宣言する。
dot_:先頭にドットを付ける。private_:結果を0600にする。executable_:実行 bit を付ける。exact_:宣言されていないディレクトリ内容を削除する。run_onchange_:内容が変わったときにスクリプトを実行する。アプリの記事では Brewfile の変更時に使う。
chezmoi source repo → rendered into $HOME├── dot_zshenv → ~/.zshenv├── dot_config/│ └── zsh/│ ├── dot_zshrc → ~/.config/zsh/.zshrc│ └── conf.d/│ ├── 00-env.zsh → ~/.config/zsh/conf.d/00-env.zsh│ └── 20-aliases.zsh → ~/.config/zsh/conf.d/20-aliases.zsh└── dot_local/ └── bin/ └── executable_zhealth → ~/.local/bin/zhealth (chmod +x)zhealth で勝手に増えた設定を探す
構成後、$HOME にある zsh ファイルは ~/.zshenv だけになる。~/.zshrc が復活したら、インストーラーが書いた可能性が高い。
# 30-functions.zsh — flag stray zsh files in $HOMEzhealth() { local stray=(~/.zshrc(N) ~/.zprofile(N) ~/.zlogin(N) ~/.zshrc.*(N)) if (( ${#stray} )); then print -u2 "zhealth: unexpected zsh files in \$HOME:" printf ' %s\n' "${stray[@]}" >&2 return 1 fi print "zhealth: \$HOME is clean — only ~/.zshenv expected"}新しいツールを入れたあとに実行すれば、数か月後にシェルの挙動から原因を推測せずに済む。メンテナンスの記事でも同じ health check の考え方を使う。
追跡しないもの
~/.zsh_history は設定ではなく状態で、内容も private。.zcompdump* は再生成できる cache。credential や token も通常の dotfiles リポジトリには置かない。
.config/zsh/.zcompdump*.zsh_historyマシン固有の proxy や local alias は 90-local.zsh に置く。最後に読み込むので上書きできるが、Git では管理しない。
結果として $HOME には入口が一つ、設定本体は順序付きの小さな fragment、期待状態は chezmoi、勝手に増えたファイルの確認は zhealth という形になった。