「結局フォーマッター何種類入れたらいいの問題」「nix fmt が何をやってくれるか実は分かっていない疑惑」。リポジトリ直下を見渡すと prettierrc と .flake8 と .editorconfig と lua-format.toml が並んでいて、どれが効いているか正直わからないわけです。
dotfiles リポジトリのように Nix・Python・Lua・Shell が一つのリポジトリに同居していると、設定ファイルは言語ごとに増えていきます。CIでのチェック方法もバラバラ、ローカルではエディタ任せ。「インデントが揃ってないコミット」がレビューで指摘されるたびに、tab/space 戦争で死人が出る前に何とかしたい気持ちになりませんか? …正直、なります。
この記事では、treefmt-nix と pre-commit-hooks を組み合わせて nix fmt 一発で全部整える 設定パターンを紹介します。Nix 全体構成(nix-darwin / home-manager)の話はNix flakes で dotfiles を管理する記事に譲って、本記事はフォーマッター統合に絞ります。なお、移行コストや「nix develop の中でないと走らない」といった制約も含めて、後段で正直に書きます(Nix の良いところだけ並べる記事は世の中に十分あるので)。
この記事で学べること
treefmt-nixで Nix・Python・Lua・Shell のフォーマッターを統合管理する方法nix fmt/nix flake checkでローカルとCIの両方に対応するフォーマットチェックdevShellsのshellHookでpre-commitフックを自動設置する設計nix develop外からのコミットでフォーマットをスキップする際の警告設計
前提条件
- Nix flakes が有効になっていること(
experimental-features = nix-command flakes) - リポジトリに
flake.nixが存在すること - Nix・Python・Lua・Shell のいずれかを含むリポジトリ
なぜ treefmt-nix なのか
「treefmt-nix を入れると本当に統合されるって聞くけど、実際どうなの?」というのが正直な出発点です。「prettier だけで生きていたい人生だった」と思いつつ、Nix と Python と Lua が同居してしまうと逃げ場がなくなるわけです。treefmt-nix は、複数のフォーマッターを一つのインターフェースに束ねる numtide/treefmt-nix で、Nix flakes の formatter 出力にぶら下げると nix fmt だけで言語横断のフォーマットが走ります。
似たものに pre-commit-hooks-nix がありますが、役割が違います。最初は「両方入れた方が安心では?」と思いきや、責務を分けて読むと使い分けがはっきりします。
| ツール | 主な役割 | 起動タイミング |
|---|---|---|
| treefmt-nix | フォーマッターの統合 | nix fmt / nix flake check |
| pre-commit-hooks-nix | git フックの管理 | git commit |
本記事では「フォーマッター統合は treefmt-nix、git フックは shellHook 経由で treefmt を呼ぶ」というシンプルな構成にしています。pre-commit-hooks-nix を別途入れると依存が一段増えるので、最小構成で済ませる選択です(フックのために flake input をもう一個 follow させるのは、正直しんどいですよね)。
設定例: treefmt.nix
ルートに treefmt.nix を置きます。
{ ... }:
{
projectRootFile = "flake.nix";
programs.nixfmt.enable = true;
programs.ruff-check.enable = true;
programs.ruff-format.enable = true;
programs.stylua.enable = true;
programs.shfmt.enable = true;
}
これだけで以下が有効になります。
| 言語 | フォーマッター | Lint |
|---|---|---|
| Nix | nixfmt | - |
| Python | ruff format | ruff check |
| Lua | stylua | - |
| Shell | shfmt | - |
projectRootFile はプロジェクトルートを判定するためのマーカー。flake.nix を指定しておけば、サブディレクトリで nix fmt を打っても正しいルートを見つけてくれます(深いディレクトリで cd を繰り返したあとに fmt が刺さらない、あの徒労が消えます)。
ruff だけは format と check を別プログラムとして登録しているので、フォーマット適用とLintチェックを同時にかけられます。「Lint と Format でバイナリを2回起動するのは富豪すぎでは」と思いきや、ruff は両方とも同じバイナリ内で実行されるので体感の重さはほぼゼロでした。
flake.nix での配線: nix fmt と nix flake check
treefmt.nix を flake に繋ぎ込みます。outputs 内の formatter と checks に同じ treefmtEval を流すのがポイント。
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, treefmt-nix, ... }:
let
systems = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
pkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
# nix fmt で実行されるフォーマッター
formatter = forAllSystems (system:
let
treefmtEval = treefmt-nix.lib.evalModule pkgsFor.${system} ./treefmt.nix;
in
treefmtEval.config.build.wrapper
);
# nix flake check で実行されるチェック
checks = forAllSystems (system:
let
treefmtEval = treefmt-nix.lib.evalModule pkgsFor.${system} ./treefmt.nix;
in
{
formatting = treefmtEval.config.build.check self;
}
);
};
}
これで使い分けは以下になります。
nix fmt # ローカルでフォーマット適用(書き換え)
nix flake check # CIでフォーマット差分検知(read-only)
nix flake check は差分があると非ゼロ終了するので、GitHub Actions などでそのまま CI のフォーマットチェックに使えます。treefmt --fail-on-change を CI で叩く構成と機能的には同じですが、Nix 側に閉じている分、ツールチェーンを別途用意せずに済みます(CI の YAML に pip install ruff && cargo install stylua && ... を並べていた頃と比べると、ずいぶん身軽になりますよね)。
ちなみに本音としては、初回の nix flake check はそこそこ重いです。フォーマッターのバイナリを Nix 側で揃えにいくぶん、最初の数分は覚悟が必要(CI はキャッシュを噛ませる前提で組まないと、PRごとに数分待ちが発生します)。
devShells で pre-commit を自動設置する
「nix fmt は手で打てばいい」と思いきや、人間はコミット直前に絶対忘れます(断言)。devShells の shellHook で .git/hooks/pre-commit を生成しておくと、nix develop に入った時点でフックが整います。
devShells = forAllSystems (system:
let
pkgs = pkgsFor.${system};
treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
treefmtWrapper = treefmtEval.config.build.wrapper;
in
{
default = pkgs.mkShell {
packages = [
treefmtWrapper
pkgs.shellcheck
];
shellHook = ''
if [ -d .git ]; then
mkdir -p .git/hooks
cat > .git/hooks/pre-commit << 'HOOK'
#!/usr/bin/env bash
set -euo pipefail
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
[ -z "$STAGED" ] && exit 0
# treefmt が PATH にあるときだけフォーマット適用
if command -v treefmt &>/dev/null; then
echo "$STAGED" | xargs treefmt
echo "$STAGED" | xargs git add
else
echo "pre-commit: treefmt not found, skipping format (run 'nix develop' first)"
fi
# shell ファイルだけ shellcheck
SH_FILES=$(echo "$STAGED" | grep '\.sh$' || true)
if [ -n "$SH_FILES" ] && command -v shellcheck &>/dev/null; then
echo "$SH_FILES" | xargs shellcheck
fi
HOOK
chmod +x .git/hooks/pre-commit
fi
'';
};
}
);
設計のポイントを3つ。
1. nix develop に入った瞬間にフックが入る
shellHook は devShell 起動時に実行されるので、リポジトリをクローンして nix develop した時点で .git/hooks/pre-commit が出来上がります。READMEに「フックを手動でインストールしてください」と書く必要がない(書いても誰も読まないわけです)。
2. ステージされたファイルだけ整形してから git add し直す
treefmt を全ファイルに走らせるとコミットしていない作業中の差分まで触ってしまうので、git diff --cached --name-only でステージ済みのファイル名だけ抜き出して xargs treefmt に渡しています。整形後に git add し直すことで、フォーマット結果を含めてコミットに乗せます(「コミット予定じゃないファイルが勝手に整形される」事故、地味にイラッとしますよね)。
3. nix develop 外から git commit した時の警告設計
ここが地味に重要です。CI からのコミットや、nix develop を経由しない別ターミナルから git commit を叩くと、フックは実行されるのに treefmt が PATH に存在しません。「フックが守ってくれているはずが、実は素通り」という事態は避けたいわけです。
if command -v treefmt &>/dev/null; then
echo "$STAGED" | xargs treefmt
echo "$STAGED" | xargs git add
else
echo "pre-commit: treefmt not found, skipping format (run 'nix develop' first)"
fi
treefmt が無いときは黙ってスキップせず、run 'nix develop' first というメッセージを出してフォーマット未適用を可視化しています。CIの nix flake check で最終的に弾けるので、ローカルですり抜けても本番には混入しません(フックが万能でないのは正直認める。最終防波堤は CI 側です)。
動作確認
# devShell に入る → pre-commit が自動設置される
nix develop
# 全ファイルをフォーマット
nix fmt
# CI と同じチェックを手元で実行
nix flake check
# 設置されたフックを確認
cat .git/hooks/pre-commit
nix flake check は初回が遅いですが(数分単位の待ちで、お茶を入れる時間が確保できます)、Nix のキャッシュが効くので2回目以降は速くなります。CIでは cachix などのリモートキャッシュを噛ませると差分の出るファイルだけチェックする運用になり、現実的な実行時間に収まります。
正直、nix develop に毎回入る運用は最初だるく感じるはずです。けど実は、direnv と組み合わせるとディレクトリに cd した時点で勝手に shell が起動するので、「Nix を意識せずに Nix の恩恵を受ける」状態に着地します(このあたりは別記事で詳しく扱います)。
注意点・Tips
projectRootFileは flake.nix にしておく: モノレポでサブディレクトリからnix fmtを実行しても正しいルートが見つかります。.gitを指定するとサブモジュール構成で迷うので非推奨。ruff-checkとruff-formatは両方有効化する: フォーマット適用とLintチェックは別物。ruff は両方を一つのバイナリで持っているので、両方enableにしても重くなりません。stylua.toml/.shfmtの個別設定は別ファイルで持てる:treefmt.nixでフォーマッターを有効化しつつ、各フォーマッターのルール(インデント幅など)はそれぞれの設定ファイルで管理できます。Nix でルールまで全部書く必要はありません。nix develop外でのコミットを完全に防ぎたい場合:direnv+nix-direnvでディレクトリに入った瞬間に devShell を有効化する構成にすると、フォーマッターが PATH に常駐します。Nix と direnv の組み合わせはdotfiles を Nix flakes で管理する話で触れています。- エディタ統合: Neovim や VS Code から
treefmtを直接呼ぶ拡張もありますが、nix fmtを保存時に走らせる設定で十分実用になります。エディタ側の設定でformat-on-saveを有効化しておくのが楽。
まとめ
treefmt-nix は、フォーマッターの設定ファイルが言語ごとに散らばる問題を Nix flakes の formatter 出力に集約するアプローチです。
treefmt.nixで Nix・Python・Lua・Shell のフォーマッターを宣言的に統合nix fmtでローカル整形、nix flake checkでCIチェックdevShellsのshellHookでpre-commitを自動設置、nix develop外では明示的に警告
Claude Code などのAIコーディングツールに任せる範囲が広がるほど、フォーマッターの一貫性は人間が読み返すときの認知コストに直結します。エージェント運用全体の話はClaude Code カスタマイズ術にまとめてあります。
設定ファイルを増やすのではなく、 設定ファイルを統合する 方向で考え始めると、リポジトリの体重が少しずつ軽くなる気がします(ファイル削除のコミットほど気持ちいいものは、なかなかないですよね)。



