Claude Media
Claude Codeでconda-lockを使い、環境の再現性を固定する手順

Claude Codeでconda-lockを使い、環境の再現性を固定する手順

environment.ymlだけでは同じ環境にならない原因を整理し、conda-lockで固定します。全量exportとロック漏れをhookで止める設定も載せます。

Claude Codeでconda-lockを使い、環境の再現性を固定する手順

「自分のマシンでは動くのに、同僚のマシンでは conda env create が失敗する」。原因の多くは、environment.yml が環境の「意図」しか持たないことと、conda env export の出力がOSに縛られることにあります。対策は、解決済みの結果をロックファイルとして固定し、そのロックが古くなったらClaude Codeの作業中に気づける形にすることです。

この記事では、conda-lockでロックファイルを作り、Claude Codeのhookで「全量export」と「ロックの更新漏れ」を止める設定を組みます。CLAUDE_ENV_FILE で有効化した環境を引き継ぐ話はCLAUDE_ENV_FILEでvenvとcondaを引き継ぐ記事にあります。ここで扱うのは、環境定義そのものを再現できる状態にする側です。R言語の依存固定はrenvで依存を固定する手順が対応します。

再現できない原因は4か所に分かれる

conda環境が再現できないとき、原因は次のどれかに寄ります。Claude Codeに調べさせる前に、この分類を頭に置いておくと指示が短くなります。

症状疑う場所根拠
別OSで解決に失敗する疑う場所conda env export が依存まで全部書き出している根拠condaのdocsは、全量のexportが「プラットフォーム間で互換でないパッケージ」を含みうると説明している
同じOSでも版が揃わない疑う場所environment.yml が版を固定していない根拠意図の記述であり、解決結果ではない
入るパッケージの供給元が違う疑う場所channels の並びと defaults の有無根拠nodefaults で既定チャンネルを外せる
一部だけ環境が違う疑う場所pip経由のパッケージ、または別チャンネル指定根拠channel::package 形式で個別に供給元を指定できる

2行目の「版が揃わない」は、公式の記述というより environment.yml の性質から来る切り分けです。docsのサンプルは python と numpy を版なしで書いており、解決するたびに結果が変わりうる書き方です。

environment.ymlは意図、ロックファイルは解決結果

condaのdocsは、ロックファイルを「環境を正確に再作成するのに必要なパッケージ、版、ビルド、チャンネルを含むもの」と説明しています。解決済みの状態を持つので、condaはソルバーの工程を飛ばしてダウンロードとインストールに進めます。

対して、conda export --from-history は、自分で明示的にインストールしたパッケージだけを書き出します。こちらはOSをまたいで持ち運べますが、版の揃い方は保証しません。役割を分けるとこうなります。

くらべる

environment.ymlとロックファイルの役割

意図

environment.yml

人が編集するファイルです。入れたいパッケージとチャンネルを書きます。--from-history の出力もこちらの仲間です。

解決結果

ロックファイル

ツールが生成するファイルです。版・ビルド・チャンネルまで固定し、人は直接編集しません。

もう1つの選択肢が、conda list --explicit で作るexplicit spec fileです。ただしdocsは、これを同じOSプラットフォームでの再現用と位置づけています。ファイル先頭に # platform: osx-64 のような行が入り、別プラットフォームでは入らないパッケージが出ます。複数OSのチームは、複数プラットフォームに対応するロックファイルのほうが向きます。

conda-lockでロックファイルを作る

conda-lockは、environment.yml から複数プラットフォーム向けのロックファイルを作るツールです。READMEの基本形は次のとおりです。

conda-lock -f environment.yml -p osx-64 -p linux-64
conda-lock install -n my-locked-env

出力はカレントディレクトリの conda-lock.yml に保存されます。-p を毎回書きたくないときは、environment.yml に非標準の platforms キーを足せます。

# environment.yml
channels:
  - conda-forge
  - nodefaults
dependencies:
  - python=3.12
  - numpy
platforms:
  - linux-64
  - osx-arm64

複数のyamlを -f で渡したときは、channels の和集合が順序を保ったまま使われます。チャンネルの並びが結果を左右するので、複数ファイルを合成するリポジトリでは、合成後の順序もレビュー対象に入れておくと安全です。

conda-lockは、入力のハッシュが変わらなければロックを作り直さない --check-input-hash も持ちます。READMEはこれをpre-commitのような自動実行向けに紹介しています。

conda 26.5以降はcondaだけでも作れる

condaのロックファイル節には、conda 26.5以降でロックファイルを扱えるという注記があります。conda-lock.yaml と pixi.lock をネイティブに読み、Windows・Linux・macOSの環境を作れる、という内容です。

conda export --name my-env --file conda-lock.yaml \
  --platform linux-64 --platform osx-64 --platform win-64
conda create --name my-env --file conda-lock.yaml

ここで見落としやすい点があります。conda-lockツールの既定の出力名は conda-lock.yml で、condaのdocsの例は conda-lock.yaml です。拡張子が1文字違うだけなので、後で書くhookは両方を探す作りにします。どちらを採用するかは、チームで一度決めてください。

hookで検査する2つの関所

Claude Codeのhookは、ツール呼び出しの前後に任意のコマンドを走らせる仕組みです。ここでは次の2点を機械的に止めます。

  • 全量の conda env export を environment.yml に書き出す操作
  • environment.yml を編集したのに、ロックファイルが更新されていない状態

どちらも jq を使うので、PATH に入れておきます。

関所1: 全量exportをPreToolUseで拒否する

conda env export を素で実行すると、依存まで全部が書き出されます。これを共有用の environment.yml に流すと、別OSで解決できなくなります。PreToolUseでBashの呼び出しを見て、--from-history かロックファイルへの出力がなければ拒否します。

#!/bin/bash
# .claude/hooks/conda-export-guard.sh
CMD=$(jq -r '.tool_input.command')
 
# 許可: --from-history 付き、またはロックファイルへの出力
if echo "$CMD" | grep -q -e '--from-history' \
   -e 'conda-lock\.ya\?ml'; then
  exit 0
fi
 
jq -n '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason:
      "全量exportはOS依存。--from-history か conda-lock を使う"
  }
}'

permissionDecision を "deny" にして理由を返す形は、hooksリファレンスにある rm -rf ブロックの例と同じです。JSONで返す場合は、終了コードを0にします。

関所2: environment.yml編集後にPostToolUseで点検する

PostToolUseはツールが成功した後に動きます。hooksリファレンスによると、PostToolUseからClaudeへ警告を渡したいときは終了コード2を使い、stderrをClaudeに見せる、と説明しています。これを使って、編集直後に3点を点検します。

  1. channels に nodefaults が入っているか(チームの規則としての点検)
  2. 名前=版=ビルド の形でビルド文字列が固定されていないか
  3. ロックファイルが environment.yml より新しいか

2点目は、全量exportの出力をそのまま貼り付けたときに混ざる形です。explicit形式の例でも、URLにビルド文字列(py39h2e5f516_0 など)が入っています。この記事のスクリプトは、= が2つ以上ある行を警告する単純な判定にしています。

#!/bin/bash
# .claude/hooks/env-lock-check.sh
FILE=$(jq -r '.tool_input.file_path')
case "$FILE" in
  *environment.yml|*environment.yaml) ;;
  *) exit 0 ;;
esac
DIR=$(dirname "$FILE")
ERR=""
 
grep -q 'nodefaults' "$FILE" ||
  ERR+="channelsにnodefaultsがありません。"$'\n'
grep -Eq '^\s*-\s*[^=#]+=[^=]+=[^=]+' "$FILE" &&
  ERR+="ビルド文字列つきの固定があります。"$'\n'
 
LOCK=""
for f in conda-lock.yml conda-lock.yaml; do
  [ -f "$DIR/$f" ] && LOCK="$DIR/$f"
done
if [ -z "$LOCK" ] || [ "$FILE" -nt "$LOCK" ]; then
  ERR+="ロックが古いです。conda-lock -f $FILE を実行"$'\n'
fi
 
[ -n "$ERR" ] && { printf '%s' "$ERR" >&2; exit 2; }
exit 0

settings.jsonに登録する

2つのスクリプトを実行可能にして(chmod +x)、プロジェクトの設定に登録します。Bash側は、if に権限ルール構文を書いて絞ります。if フィールドは "Bash(git *)" のような書式を取り、ツールイベントでだけ評価されます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(conda env export*)",
            "command": ".claude/hooks/conda-export-guard.sh"
          },
          {
            "type": "command",
            "if": "Bash(conda export*)",
            "command": ".claude/hooks/conda-export-guard.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/env-lock-check.sh"
          }
        ]
      }
    ]
  }
}

hookが失敗したときに操作ごと止めたい運用は、onFailureで失敗時に操作を止める設定の側で扱っています。Windowsのhookで終了コード2が効かない問題はPowerShell hookのexit 2の記事が詳しいので、PowerShell環境のチームはそちらも確認してください。

落とし穴

BashでYAMLを書き換えるとPostToolUseが動かない

リファレンスには、Edit|Write に一致するPostToolUseは、Bashコマンドや外部プロセスが同じファイルを書き換えても動かない、と説明しています。sed -i で environment.yml を直されると関所2は素通りします。そこで、Bashで書き換えた場合に備え、コミット前に conda-lock --check-input-hash を回す層を別に置くと抜けを減らせます。

再現できない原因をClaude Codeに洗わせる

hookは「やってはいけない操作」を止める層です。すでに再現できない環境が手元にあるときは、原因の洗い出しをClaude Codeに任せます。読み取り中心の指示にしておくと、環境を壊さずに済みます。

conda環境が別マシンで再現できない。次の順で調べて、
原因の候補を根拠つきで並べて。環境は変更しないこと。
1. environment.yml のchannelsと、全量exportの混入の有無
2. conda info でplatformを確認し、ロックファイルの対象に含まれるか
3. conda-lock.yml(またはyaml)がenvironment.ymlより古くないか
4. conda export --from-history の出力と現行ymlの差分

指示に出てくる確認項目は、冒頭の表と対応しています。Claude Codeには、conda info、conda export --from-history、diff のような読み取りコマンドを実行させ、その出力を根拠に候補を絞らせます。出力をそのまま鵜呑みにせず、候補が出たらロックファイルを作り直して別環境で conda create --file を実行し、再現を確かめる流れが確実です。

条件が許せば、Bash(conda-lock *) のような許可ルールを permissions.allow に足しておくと、ロック更新のたびの確認が減ります。依存固定のロックファイルを前提にした権限設定は、uvプロジェクトの権限設定と同じ考え方です。

ロックが効いているかを別環境で確かめる

ロックを作っただけでは、再現できる証拠にはなりません。作成したマシンとは別の場所で、ロックからの環境作成が通ることを一度は実行しておきます。

  • ロックから新しい名前の環境を作る(conda create --name verify-env --file conda-lock.yaml)
  • 作った環境で、テストか python -c "import numpy" のような最小の確認を走らせる
  • 失敗したら、失敗したプラットフォームを conda info の platform で確かめ、ロックの対象に含めたか見直す

この3手は、CIのジョブにそのまま写せます。Claude Codeに依頼するときも、「ロックを更新したらverify-envで作成と最小確認まで通す」と終了条件に書いておけば、更新だけして確認を飛ばす作業を減らせます。

凍結された環境と別OSでの解決

2つの例外にも触れておきます。

  • 凍結された環境: CEP 22に基づき、凍結マーカーのある環境ではパッケージの追加・更新・削除が EnvironmentIsFrozenError で拒否されます。--override-frozen で回避できますが、docsは推奨していません。Claude Codeに「エラーが出たので回避して」と任せると、このフラグを足す提案が出うるので、hookで --override-frozen を含むコマンドを拒否しておく選択肢があります。
  • 別OS向けの解決: conda create --platform で別OS向けの環境を作ると、仮想パッケージが足りず解決に失敗することがあります。CONDA_OVERRIDE_LINUX=1 や CONDA_OVERRIDE_GLIBC=2.17 の指定で回避できる例が載っており、--dry-run 以外での使用は勧められていません。

まとめ

再現性の鍵は、人が書く environment.yml と、ツールが作るロックファイルを分けることです。全量exportの混入とロックの更新漏れは、人もClaude Codeも起こしやすいミスなので、PreToolUseとPostToolUseのhookで機械的に止めると、レビューの手間が減ります。まずロックを1つ作り、conda create --file で別マシンから再現できることを確かめてから、hookを足す順序がやりやすい進め方です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks
  2. docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environme…
  3. github.com/conda/conda-lock
この記事を共有:XはてブLinkedIn