Claude Code と Codex CLI の設定を Nix で SSOT 化する

blog
tech-ai

禁止コマンドリスト・MCPサーバー・Agent Skills の3つを Nix home-manager で一元管理する方法。

Author

uma-chan

Published

2026-03-14

Modified

2026-03-14

1. 背景

最近、私の開発環境には Claude Code と Codex CLI が同居しています。

2つのエージェントが動くようになってから困ったのが設定の二重管理です。 「git push を禁止する」「context7 MCP サーバーを使う」といったルールを、両方のツール用に別々のファイルへ書いていると、片方を更新したときにもう片方へのバックポートを忘れます。 実際に忘れました。

そこで Nix home-manager を使って3つの設定要素を SSOT (Single Source of Truth) 化しました。 設定は nix/home-manager/agents/ ディレクトリ以下にまとめています。

ファイル 役割
prohibited-bash-commands.nix 禁止コマンドリスト (Claude Code + Codex CLI)
mcp-servers.nix 共有 MCP サーバー定義
agent-skills.nix Agent Skills の宣言的管理
claude-code.nix Claude Code の home-manager モジュール
codex-cli.nix Codex CLI の home-manager モジュール

2. SSOT 化した3つの要素

2.1. 禁止コマンドリスト

prohibited-bash-commands.nix は私が最もシンプルで分かりやすいと思っている SSOT の例です。

Nix のリストとして禁止コマンドを定義し、2つの異なるエンジン向けの設定を自動生成します。

# prohibited-bash-commands.nix (抜粋)
[
  {
    claudeGlob = "git push*";
    argv = [
      "git"
      "push"
    ];
    justification = "pushing is prohibited";
  }
  {
    claudeGlob = "rm *";
    argv = [ "rm" ];
    justification = "rm is prohibited; use mv /tmp/ instead";
  }
  # ... 合計10エントリ
]

各エントリは3つのフィールドを持ちます。

フィールド 用途
claudeGlob Claude Code の permissions.deny 用 glob パターン
argv Codex CLI の prefix_rule 用トークン配列
justification Codex CLI が拒否時に表示するメッセージ

これをコンシューマ側がそれぞれ読み取ります。

Claude Code 側 (claude-code.nix) では、

permissions = {
  deny = (map (cmd: "Bash(${cmd.claudeGlob})") prohibitedBash) ++ [
    "Read(**/*key*)"
    "Read(.env*)"
    # ...
  ];
};

Codex CLI 側 (codex-cli.nix) では、

mkPrefixRule = cmd:
  ''
    prefix_rule(
        pattern = [${patternItems}],
        decision = "forbidden",
        justification = "${cmd.justification}",
    )
  '';

というように、同一ソースから2つの異なるフォーマットへ変換しています。

home-manager switch を1回実行するだけで、Claude Code の settings.json と Codex CLI の ~/.codex/rules/default.rules の両方に同じルールが反映されます。

2.2. MCP サーバー設定

mcp-servers.nix は Claude Code と Codex CLI が共有する MCP サーバー定義を返す関数です。

# mcp-servers.nix (全文)
{
  pkgs,
  inputs,
}:
let
  # mcp-servers-nix で管理されているサーバー (Nix でバージョン固定)
  nixServers =
    (inputs.mcp-servers-nix.lib.evalModule pkgs {
      programs = {
        context7.enable = true;
      };
    }).config.settings.servers;

  # mcp-servers-nix 未収録のサーバー (uvx またはインストール済みバイナリで実行)
  manualServers = {
    awslabs-aws-documentation-mcp-server = {
      command = "uvx";
      args = [ "awslabs.aws-documentation-mcp-server@latest" ];
    };
    drawio = {
      command = "drawio-mcp";
    };
  };
in
nixServers // manualServers

2層構造になっています。

  • nixServersmcp-servers-nix flake input を使って Nix 管理下に置いたサーバー群
  • manualServers はまだ mcp-servers-nix に収録されていないサーバー群 (呼び出し方はサーバーごとに異なる)

nixServers // manualServers で合成した attrset を claude-code.nixcodex-cli.nix の両方が import ./mcp-servers.nix で読み込みます。 1箇所に追加するだけで両エージェントに配布されます。

2.3. Agent Skills

agent-skills.nixagent-skills-nix home-manager モジュールを使った宣言的なスキル管理です。

# agent-skills.nix (抜粋)
programs.agent-skills = {
  enable = true;

  sources = {
    local        = { path = inputs.self; subdir = "agents/skills"; };
    anthropic    = { path = inputs.anthropic-skills; subdir = "skills"; };
    streamlit    = { path = inputs.streamlit-skills; subdir = "developing-with-streamlit/skills"; };
    databricks   = { path = inputs.databricks-agent-skills; subdir = "databricks-skills"; };
    # ... 合計14ソース
  };

  skills.enableAll = true;

  targets = {
    claude-home = { dest = "${homeDir}/.claude/skills"; structure = "symlink-tree"; };
    codex       = { dest = "${homeDir}/.codex/skills";  structure = "symlink-tree"; };
  };

  excludePatterns = [ "/.system" ];
};

ポイントは targets に複数の宛先を書けることです。 ~/.claude/skills~/.codex/skills の両方が同一ソースからシンボリックツリーとして展開されます。

enableAll = true で全スキルを有効にしつつ、重複する名前のスキルは filter.nameRegexrename で解決しています。

excludePatterns = [ "/.system" ] は、エージェントが実行時に skills/ 配下に書き込む .system/ ディレクトリを rsync 対象から除外するための設定です。 これがないと home-manager switch のたびに実行時状態が消えます。

3. 実装のはまりポイント

3.1. settings.json はシンボリックリンクにできない

claude-code.nix の実装で一点注意が必要な箇所があります。

settings.json の配置です。

他の設定ファイル (CLAUDE.md, rules/, agents/) は Nix ストアへのシンボリックリンクとして配置できます。 読み取り専用で構いません。

しかし settings.json だけは違います。

Claude Code のインタラクティブな /config エディタがユーザーの設定変更を settings.json に書き戻します。 Nix ストアのシンボリックリンクは読み取り専用なので、書き込みに失敗してしまいます。

なお、MCP サーバーの接続状態などのランタイム状態が実際に書き込まれるのは ~/.claude/.claude.json の方です。

そのため home-manager の activation スクリプトで install -Dm644 を使い、書き込み可能なファイルとしてコピーしています。

# claude-code.nix (activation スクリプト部分)
claudeSettings = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
  install -Dm644 ${settingsFile} "$HOME/.claude/settings.json"
'';

home-manager switch を実行するたびに最新の設定で上書きコピーされます。 /config エディタで加えた変更はこのコピー先に書き込まれ、次の switch で上書きされます。

MCP サーバーの登録は別の activation スクリプト (claudeMcpServers) で ~/.claude/.claude.jsonjq で直接書き換えており、こちらも同様の理由でシンボリックリンクではなく実ファイルを対象にしています。

3.2. Codex CLI の trusted projects は動的生成

codex-cli.nix の activation スクリプトは fd~/ghq 以下の全 git リポジトリを検索し、config.tomltrust_level = "trusted" エントリを追記します。

${pkgs.fd}/bin/fd --type d --hidden --no-ignore "^\.git$" "${ghqRoot}" --max-depth 4 2>/dev/null |
  sort |
  while read -r gitdir; do
    repo=$(dirname "$gitdir")
    echo ""
    echo "[projects.\"$repo/\"]"
    echo "trust_level = \"trusted\""
  done >> "$_output"

新しいリポジトリを ghq get で取得したあと home-manager switch を1回実行するだけで、そのリポジトリも自動的に trusted になります。 手動で config.toml を編集する必要はありません。

3.3. agent-skills.nix の込み入った設定

agent-skills.nix には、ドキュメントを読んだだけでは意図が分かりにくい部分がいくつかあります。

3.3.1. filter.nameRegex の直感に反する挙動

filter.nameRegex は「このパターンに一致するスキルだけを含める」という allowlist フィルタです。

databricks-official ソースの例を見ると、

databricks-official = {
  path = inputs.databricks-official-skills;
  subdir = "skills";
  filter.nameRegex = "databricks(-apps|-pipelines)?"; # exclude databricks-jobs (duplicate)
};

databricksdatabricks-appsdatabricks-pipelines にマッチするスキルだけを含め、databricks-jobs は除外するという意味です。 コメントにある通り、ai-dev-kit 側に同名スキルが存在するための回避策です。

一方、drawio-mcp ソースでは全く逆の目的で使っています。

drawio-mcp = {
  path = inputs.drawio-mcp;
  filter.nameRegex = "DISABLED"; # prevent auto-discovery; use skills.explicit
};

"DISABLED" という文字列はいずれのスキル名にも一致しません。 つまり意図的にマッチするものが存在しない正規表現を指定することで、auto-discovery を完全に無効化しています。

このソースから読み込みたいスキルは後述の skills.explicit で個別に指定するため、自動探索は不要というわけです。

3.3.2. skills.explicit でリネームして重複を解決する

skills.explicitenableAll = true と組み合わせて使えます。 自動探索では読み込まれないスキルを個別に指定したり、名前を変えてロードしたりするための仕組みです。

skills = {
  enableAll = true;
  explicit.drawio-skills = {
    from = "drawio-mcp";
    path = "skill-cli/drawio";
  };
  explicit.databricks-jobs-bundles = {
    from = "databricks-official";
    path = "databricks-jobs";
    rename = "databricks-jobs-bundles"; # avoid duplicate with ai-dev-kit
  };
};

drawio-skillsdrawio-mcp ソースの skill-cli/drawio サブディレクトリを明示的にロードしています。 auto-discovery を "DISABLED" で止めておいて、必要なパスだけ explicit で取り出すという構成です。

databricks-jobs-bundlesdatabricks-official ソースの databricks-jobs スキルを databricks-jobs-bundles という別名でロードしています。 ai-dev-kit 側にも databricks-jobs という名前のスキルが存在するため、そのまま読み込むと衝突します。 rename を使って名前を変えることで両方を共存させています。

3.3.3. excludePatterns = [“/.system”] の重要性

agent-skills-nix の activation スクリプトは rsync で各ターゲットディレクトリを更新します。

エージェントは実行中に ~/.claude/skills/.system/ 配下へ実行時状態を書き込みます。 excludePatterns の指定がないと、home-manager switch のたびに rsync がこのディレクトリを削除してしまいます。

excludePatterns = [ "/.system" ];

この1行は「Nix 管理外のランタイム状態を守るための穴」です。 Nix で厳密に管理された環境の中に、エージェントが自由に書き込めるスペースを意図的に残しています。

4. まとめ

4.1. SSOT 化した3要素

要素 ファイル 消費者
禁止コマンド prohibited-bash-commands.nix claude-code.nix, codex-cli.nix
MCP サーバー mcp-servers.nix claude-code.nix, codex-cli.nix
Agent Skills agent-skills.nix ~/.claude/skills, ~/.codex/skills

4.2. やってみて

設定の二重管理から解放されたのが一番大きいです。

「Claude Code に追加したけど Codex CLI に忘れた」が構造的に起きなくなりました。

Nix で管理するとビルドエラーで設定ミスが事前に検出できるのも地味に助かっています。 JSON や TOML を手書きしていたときは起動してみて初めてエラーに気づいていたので。

agent-skills-nix を使ったマルチソースのスキル管理はまだ日本語の情報が少ないので、参考になれば幸いです。