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点を点検します。
channelsにnodefaultsが入っているか(チームの規則としての点検)名前=版=ビルドの形でビルド文字列が固定されていないか- ロックファイルが
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 0settings.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を足す順序がやりやすい進め方です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。