「新しい Mac、最高……ところで前の環境どうやって作ったんだっけ」。箱から出したばかりの Mac を前にして、毎回これになります。システム設定を開いてキーリピートを最速にして、Dock を自動で隠して、Finder をカラム表示にして、ターミナルを入れて、Touch ID で sudo が通るように pam.d をいじって……。この「毎回手でポチポチやる 30 分」は、給料明細には載らないけれど確実に存在するコストです。しかも手順書を書いても、半年後の自分は絶対に読みません(書いた本人すら読まないものを、誰が読むんですかという話です)。
Nix Flakes で dotfiles を管理し始めると、CLI ツールや設定ファイルは宣言的に固められます。ここまでは気持ちいいんですが、その先で必ずぶつかるのが「macOS のシステム設定そのもの」と「Homebrew でしか配れない GUI アプリ」です。「dotfiles を Nix にしたら全部解決」と聞くけれど、Dock の自動非表示まで Nix で書けるのか、実際どうなの? ここは Home Manager の home.file だけでは届かない領域で、nix-darwin の system.defaults と homebrew モジュールに踏み込む必要があります。
この記事は、nix-darwin の system.defaults / homebrew モジュール + Home Manager の home.file を使って macOS のシステム設定・Homebrew Casks・Touch ID sudo・ユーザー設定の配布をまとめてコード化する 基本パターンを扱います。Flakes そのものの導入や「再現性とは何か」という入門の話はNix Flakes で dotfiles を管理する記事に譲り、本記事は nix-darwin 特有の設定に絞ります。検証は実際に稼働している macOS dotfiles リポジトリの構成をもとにしています。
この記事で学べること
nix-darwinのsystem.defaultsで Dock・Finder・キーリピート・トラックパッドをコード化する書き方homebrewモジュールとonActivationで Casks を宣言的に管理し、宣言外アプリを自動削除する設定security.pam.services.sudo_local.touchIdAuthで Touch ID + sudo を有効化する一行- Fish をログインシェルにして
$SHELLまで Nix 管理下に入れる方法 - Home Manager の
home.fileで.config/配下にユーザー設定をディレクトリごと symlink する方法
前提条件
- Nix Flakes が有効(
experimental-features = nix-command flakes) flake.nixでnix-darwinとhome-managerを input として読み込み済み- Apple Silicon の macOS(
aarch64-darwin)を想定。Intel でもsystemを差し替えれば同じ考え方が通る
システム設定を system.defaults でコード化する
macOS のシステム設定は、本来 defaults write を延々と叩く世界です。nix-darwin の system.defaults は、その defaults コマンド群を Nix の属性セットに置き換えてくれます。darwin/default.nix に書くのはこれだけです。
# darwin/default.nix
system.defaults = {
dock = {
autohide = true; # Dock の自動非表示
orientation = "bottom"; # Dock の位置
tilesize = 50; # アイコンサイズ
};
finder = {
FXPreferredViewStyle = "clmv"; # カラム表示をデフォルトに
ShowPathbar = true; # パスバーを表示
ShowStatusBar = true; # ステータスバーを表示
};
NSGlobalDomain = {
AppleShowAllExtensions = true; # すべての拡張子を表示
InitialKeyRepeat = 10; # キーリピート開始までの時間(最速)
KeyRepeat = 1; # キーリピート速度
};
trackpad = {
Clicking = true; # タップでクリック
TrackpadThreeFingerDrag = true; # 3 本指ドラッグ
};
};
InitialKeyRepeat = 10 と KeyRepeat = 1 が、システム設定の「キーのリピート速度」スライダーを目盛りの外まで振り切った状態に相当します。GUI で頑張ってもここまで速くはなりません(スライダーを右端まで持っていっても、まだ遅いんですよね)。FXPreferredViewStyle = "clmv" の clmv は Finder のカラム表示を指す内部識別子で、こういう「GUI には出てこない呪文」を一度コードに落とすと、二度と思い出さなくて済むのが効きます。clmv が何の略か、正直もう覚えていません。覚えなくていいのが宣言的構成の良さです。
注意点として、system.defaults をどのユーザーに適用するかを system.primaryUser で明示する必要があります。
# システムデフォルト設定の適用対象ユーザー
system.primaryUser = username;
# Nix ビルドユーザーグループの GID(GID 不一致エラー対応)
ids.gids.nixbld = 350;
ids.gids.nixbld = 350 は地味ですが重要で、macOS のアップデートで Nix ビルドユーザーの GID がずれて nix run が GID 不一致で落ちる事故を、設定で固定して防いでいます。エラーメッセージで初めて存在を知るタイプの設定です。
Homebrew Casks を homebrew モジュールで宣言的に管理する
CLI ツールは Nix で配れても、GUI アプリ(ブラウザ、Slack、Office 系)は Homebrew Cask の独壇場です。nix-darwin の homebrew モジュールは、この Homebrew を Nix の設定に取り込み、brew bundle 相当を毎回の適用時に走らせてくれます。
# darwin/default.nix
homebrew = {
enable = true;
onActivation = {
autoUpdate = true; # brew 自体の自動更新
upgrade = true; # 古いバージョンは自動アップグレード
cleanup = "uninstall"; # casks に無いものはアンインストール
};
casks = [
"google-chrome"
"ghostty"
"slack"
"zed"
"raycast"
"1password"
"1password-cli"
# ...(実際の構成では Office 系・ドライブ系も列挙)
];
masApps = {
Xcode = 497799835; # Mac App Store の Xcode
};
};
ここで一番効くのが onActivation.cleanup = "uninstall" です。これを入れると casks リストに書いていないアプリは適用時に自動アンインストールされます。「いつの間にか入っていた謎のアプリ」が掃除されるので、リストが常に環境の真実になります。便利な反面、手で brew install したものも問答無用で消えるので、「あれ、入れたはずのアプリが無い」となったら犯人はだいたいこの一行です(設定が効いている証拠でもあります)。
autoUpdate と upgrade を両方 true にしておくと、nix run .#update を回すたびに Cask も最新に揃います。masApps で Mac App Store のアプリも ID 指定で宣言できるので、Xcode のような App Store 縛りのものも構成に含められます。
Touch ID で sudo を通す一行
ターミナルで sudo を叩くたびにパスワードを打つのは、回数で考えると地味に時間を溶かします。Touch ID で sudo を通す設定は、本来 /etc/pam.d/sudo_local を手で編集する作業ですが、nix-darwin なら一行です。
# darwin/default.nix
security.pam.services.sudo_local.touchIdAuth = true;
これだけで sudo 実行時に Touch ID プロンプトが出るようになります。macOS のアップデートで pam.d 配下が上書きされても、nix run .#update を回せば設定が戻ってくるのが、手編集との決定的な違いです。「アップデートのたびに Touch ID sudo が消えてイラッとする」現象から解放されます。
Fish をログインシェルにする設定も同じファイルで宣言できます。programs.fish.enable を有効化し、ユーザーのシェルを差し替えるところまでをコードに含めます。
programs.fish.enable = true;
programs.zsh.enable = false;
users.users.${username} = {
shell = pkgs.fish;
home = "/Users/${username}";
};
$SHELL まで Nix 管理下に入るので、新しい Mac でも初手から Fish です。
Home Manager の home.file でユーザー設定をディレクトリごと symlink する
system.defaults や homebrew は nix-darwin(システム側)の話でした。一方、ユーザーの設定ファイル(~/.config/ 配下)は Home Manager の担当です。基本は home.file で symlink を貼ります。エディタやターミナル系の設定はディレクトリごと recursive = true で配ります。
# home-manager/home/default.nix
home.file = {
".config/ghostty" = { source = ./file/ghostty; recursive = true; };
".config/zellij" = { source = ./file/zellij; recursive = true; };
".config/nvim" = { source = ./file/nvim; recursive = true; };
};
recursive = true を付けると、リポジトリ内の ./file/ghostty/ の中身が ~/.config/ghostty/ 配下にファイル単位で symlink されます。ディレクトリ全体ではなくファイル単位で symlink される点が地味に重要で、ツール側がディレクトリ内に書き戻すログやキャッシュ(例: lazy-lock.json 系)が、リポジトリ実体に書き戻されて差分として汚れる事故を防げます。
リポジトリ側のソース(./file/ghostty/config など)を編集して nix run .#update を回せば、~/.config/ 配下の symlink 経由ですぐ反映されます。「設定をリポジトリで編集 → エディタを開き直す → 反映される」というループが当たり前に回るのが、Nix で配るユーザー設定の気持ちよさです。
home.file でカバーできない領域(別リポジトリの実体を参照させたいケース、初回セットアップでソースが未存在のケース、macOS の BSD ln 固有のハマりどころなど)は、Home Manager の activation で書きますが、こちらは冪等性の落とし穴が複数あるので別記事に分けます。本記事はまず home.file の基本までを押さえれば、ターミナル・エディタ系の設定は十分に配れます。
まとめ
macOS の開発環境をコードで固めるとき、層ごとに担当が分かれます。Dock・キーリピート・トラックパッドのようなシステム設定は system.defaults、GUI アプリは homebrew モジュールの Casks(onActivation.cleanup = "uninstall" でリストが環境の真実になる)、Touch ID sudo は security.pam.services.sudo_local.touchIdAuth の一行、Fish をログインシェルにするところまで含めて nix-darwin で宣言、ユーザー設定ファイルは Home Manager の home.file で ~/.config/ 配下にディレクトリごと symlink。これだけで「Mac を箱から出して nix run 一発」がほぼ実現します。
残りは、Home Manager の activation で「冪等な symlink」をどう書くかという中級者向けのテーマで、ここには macOS の BSD ln 固有の落とし穴を含めて複数のハマりどころがあります。次の Mac が来たときに冒頭の「前の環境どうやって作ったんだっけ」を nix run 一発に置き換えるための、その先の話として別記事で扱います。手でポチポチやっていた 30 分を取り戻すだけでなく、環境への変更が全部レビュー可能な差分として残るのが、いちばん効いてくる部分です。



