Claude CodeのCLAUDE_ENV_FILEでvenvとcondaを引き継ぐ
Bashツールはコマンドごとに別プロセスで動くため、activateの結果は次のコマンドに残りません。CLAUDE_ENV_FILEの静的指定とフック経由の書き込みを、venv・condaの例で使い分けます。
CLAUDE_ENV_FILEは、Claude CodeがBashコマンドを実行する前に毎回読み込むシェルスクリプトのパスを指定する環境変数です。仮想環境をactivateしても次のコマンドで元に戻ってしまう場合、このファイルに有効化の一行を置くと、Pythonの環境がコマンドをまたいで保たれます。
CLAUDE_ENV_FILEとは — 各Bashコマンドの前に走る前処理
Bashツールは、コマンドごとに別のプロセスで動きます。あるexportが次のコマンドで見えなくなるのはそのためです。
一方、~/.zshrc・~/.bashrc・~/.profileで定義したエイリアスと関数は、セッション開始時に読み込まれて全コマンドに反映されます。持ち越せないのは、exportのような環境変数の変更だけです。
CLAUDE_ENV_FILEは、この環境変数の穴を埋める仕組みです。指定したスクリプトの中身が、各Bashコマンドの前に同じシェルプロセスで実行されるので、ファイル内のexportがコマンドから見えます。公式の説明では、用途の例として仮想環境やcondaの有効化の持ち越しが挙がっています。
書き手は2通りあります。
- 自分でファイルを用意し、Claude Codeの起動前に
CLAUDE_ENV_FILEへパスを入れておく SessionStart・Setup・CwdChanged・FileChangedの4種類のフックが、実行時に書き込む
この4つ以外のフックは、この変数を持ちません。
venvを引き継ぐ最小構成
固定の仮想環境を使うなら、静的ファイルが最も単純です。プロジェクトの.venvを前提に、有効化の一行だけを書いたスクリプトを用意します。
cat > ~/.claude/venv-env.sh <<'EOF'
source "$HOME/work/myapp/.venv/bin/activate"
EOF
CLAUDE_ENV_FILE=~/.claude/venv-env.sh claudeパスは自分のプロジェクトに読み替えてください。Claude Codeを介さない素のbashで、この前処理の効き方を確かめました(v2.1.295のインストール環境、モデル呼び出しなし、ホームのパスは<dir>に置換)。
python3 -m venv .venv
echo 'source "<dir>/.venv/bin/activate"' > env.sh
bash -c 'echo "VIRTUAL_ENV=[${VIRTUAL_ENV}]"; command -v python'
bash -c 'source ./env.sh; echo "VIRTUAL_ENV=[${VIRTUAL_ENV}]"; command -v python'VIRTUAL_ENV=[]
VIRTUAL_ENV=[<dir>/.venv]
<dir>/.venv/bin/python前処理を挟まないとVIRTUAL_ENVは空で、挟むと.venvのpythonが先頭に来ます。Claude Codeの前処理は、このsource ./env.shを毎回のコマンドの手前で自動的に行う動きに当たります。
毎回実行されても、activateは何度sourceしてもPATHが積み上がりません。手元で2回続けて読み込んだところ、PATHの先頭に.venv/binが1つだけ並びました。べき等である点が、毎コマンド前に走らせる前処理と相性が良い理由です。
condaを引き継ぐ書き方
condaはactivateの仕組みが違います。conda activateはシェル関数で、非対話のシェルには関数がまだ定義されていません。環境ファイルの中で、関数を初期化してから呼ぶ形にします。
# ~/.claude/conda-env.sh
eval "$(conda shell.bash hook)"
conda activate myenvconda shell.bash hookがシェル関数を定義し、続くconda activateが環境変数を書き換えます。myenvは自分の環境名に置き換えてください。zshを使うならconda shell.zsh hookが対応する形です。
この書き方は公式ドキュメントの例ではなく、conda側の標準的な初期化を前処理に載せた一例です。確かめるには、Claudeにecho $CONDA_DEFAULT_ENVを実行させ、環境名が返るか見ます。空ならcondaコマンドのパスが通っているか(command -v conda)を先に調べます。
起動ファイルにconda initが書いた初期化ブロックがある場合、関数だけはセッション開始時に読み込まれます。ただしconda activateが変える環境変数は、先ほどの原則どおり次のコマンドで消えます。持ち越しには、結局この前処理ファイルが要ります。
起動前のactivate・静的ファイル・フックの使い分け
仮想環境を引き継ぐ方法は、持続する範囲と手間が違います。
| 方法 | 向く場面 | 値が残る範囲 |
|---|---|---|
起動前にactivateしてからclaude | 向く場面環境が1つに決まる作業 | 値が残る範囲セッション全体 |
CLAUDE_ENV_FILEの静的ファイル | 向く場面毎回同じ環境を使う | 値が残る範囲各Bashコマンドの前に毎回 |
SessionStartフック | 向く場面起動時のディレクトリで環境が決まる | 値が残る範囲セッション中 |
CwdChanged・FileChangedフック | 向く場面cdやファイル更新で環境が変わる | 値が残る範囲次のCwdChangedまで |
公式のtools-referenceにも、仮想環境やcondaは起動前に有効化しておくよう書かれています。持ち越しが要る場合の手段が、静的に指定するCLAUDE_ENV_FILEか、SessionStartフックによる動的な書き込みです。
フックで環境を動的に切り替える
モノレポのように、ディレクトリごとに仮想環境が違うなら、フックから書き出します。以下は、cdした先に.venvがあるときだけ有効化の行を書き足すCwdChangedフックの例です。
#!/bin/bash
# CwdChanged: 移動先に.venvがあればactivateをCLAUDE_ENV_FILEへ書く
new_cwd=$(jq -r '.new_cwd')
[ -f "$new_cwd/.venv/bin/activate" ] || exit 0
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo "source \"$new_cwd/.venv/bin/activate\"" > "$CLAUDE_ENV_FILE"
fi
exit 0new_cwdはフックの標準入力に渡るJSONのフィールドです。.venvが無いディレクトリでは何も書かずに抜けるので、無関係な移動で余計な処理が走りません。起動時のディレクトリ用には、同じ内容をSessionStartにも登録します。公式のdirenvの例も、SessionStartとCwdChangedを同じコマンドで組み合わせています。
上書きと追記の使い分け
書き込みの記号は、結果が変わる要所です。素のbashで比べると差は明らかでした。
echo 'export A=1' > e2.sh; echo 'export B=2' > e2.sh
bash -c 'source ./e2.sh; echo "A=[${A}] B=[${B}]"'
echo 'export A=1' > e3.sh; echo 'export B=2' >> e3.sh
bash -c 'source ./e3.sh; echo "A=[${A}] B=[${B}]"'A=[] B=[2]
A=[1] B=[2]>で2回書くと、先のAが消えます。hooksのリファレンスは、他のフックが書いた変数を保つために追記(>>)を使うよう案内しています。一方で公式のdirenvの例は、毎回全体を作り直すコマンドなので>で上書きしています。
つまり、複数のフックが同じファイルに書く構成では>>、一つのコマンドが環境の全体像を出力し直すなら>、と選べます。上のCwdChangedの例は、移動のたびにファイル全体を作り直す前提で>にしました。ほかのフックの書き込みも残したい場合は>>にします。
CwdChangedの書き込みは次の移動で消える
CwdChangedとFileChangedが書いた内容は、次のCwdChangedが起きた時点でClaude Codeがクリアします。前のディレクトリの仮想環境が、新しいディレクトリまで残る事態は起きません。
逆に言えば、全ディレクトリで有効にしたい環境をCwdChangedだけで書くと、移動後に消えた状態から積み直す設計が要ります。固定の環境なら静的ファイルのほうが素直です。
FileChangedは、監視するファイル名をmatcherに|区切りで並べます。たとえば.python-version|requirements.txtのように、環境定義を変える可能性のあるファイルを指定する使い方があります。matcherは正規表現ではなくファイル名のリテラルとして分割される点に注意が要ります。仕様の細部はFileChangedフックの解説にあります。
反映されたかを確かめる
設定したつもりで効いていない状態は、Pythonの記事で最も見つけにくい失敗です。pip installがグローバルに入っていても、エラーは出ません。Claudeに次の2行を実行させ、結果を読んでもらいます。
command -v python pip
echo "VIRTUAL_ENV=${VIRTUAL_ENV} CONDA_DEFAULT_ENV=${CONDA_DEFAULT_ENV}"pythonとpipのパスが.venv/bin配下を指し、環境名の変数が埋まっていれば前処理は効いています。システムのpythonが返る場合は、ファイルのパス、実行権限の要らないsource形式で書いたか、フックがCLAUDE_ENV_FILEの空でない値を受け取っているかを順に疑います。ディレクトリを移った直後にもう一度実行すると、CwdChanged系の切り替えも確認できます。
direnvで環境定義をプロジェクト側に寄せる
環境ごとにactivateの行を書く代わりに、公式はdirenvの例を挙げています。SessionStartとCwdChangedの両方にdirenv export bash > "$CLAUDE_ENV_FILE"を登録し、.envrcを置いたディレクトリでは一度direnv allowを実行しておきます。devboxやnixを使う場合は、direnv export bashの代わりにdevbox shellenvなどを同じ形で使えます。
.envrcを編集した後の再読み込みには、FileChangedのmatcherに.envrc|.envを指定する形が公式の例です。環境の定義がリポジトリ側に残るため、フックの設定はプロジェクトをまたいで同じままで済みます。venvの場所を変えても、書き換えるのは.envrcだけです。
認証情報つきのインデックスURLに注意する
社内のPyPIミラーを使うプロジェクトでは、PIP_INDEX_URLにトークン付きのURLを入れることがあります。CLAUDE_CODE_SUBPROCESS_ENV_SCRUBを1にすると、Claude CodeがBashコマンドやフックに渡す環境から認証情報が取り除かれます。公式の表では、パスワードを含むPIP_INDEX_URLはURLを残したままユーザー名とパスワードの部分だけが削られます。
環境ファイルでその変数をexportしていても、スクラブの扱いが変わるかどうかは公式の説明に記載がありません。社内ミラー経由でインストールする作業でスクラブを有効にするときは、まずClaudeに小さなパッケージを入れさせて、認証が通るか確かめてから本番の作業に移るのが安全です。前処理ファイルはBashコマンドの直前に毎回実行される実行可能なスクリプトなので、トークンを直書きしたファイルは権限を絞って置きます。
ハマりやすいところ
- 静的ファイルとフックの併用: 自分で指定したファイルと、フックが書く先が同じ扱いになるかは、公式の説明がありません。静的指定とフックの動的書き込みは、どちらかに寄せたほうが挙動を読み違えません
Setupフックは普通の起動では動かない:--init-onlyか、非対話の-pと--init・--maintenanceの組み合わせでだけ発火します。対話セッションの毎回の起動にはSessionStartを使います- 再開後に値が届かない不具合:
/resume・/clearの後にSessionStartの書いた変数が古いままになる不具合はv2.1.136で、/resume・/branchの後にBashツールへ届かなくなる不具合はv2.1.295で修正されています。古いバージョンでは再開後にwhich pythonを確かめると原因を切り分けられます - 末尾が
#のコメント行: ファイルの最後をコメント行にするとBashツールの出力が空になる不具合が、v2.1.108で直っています - Windows:
CLAUDE_ENV_FILEは、v2.1.111より前のWindowsでは何もしない状態でした
起動ファイルの扱いやPowerShellとの違いはシェル起動設定の解説に、cdで発火するフックの詳細はCwdChangedフックの解説にあります。zoxideのようなシェル関数が使えない場合は、関数の取り込みと環境変数の持ち越しが別の問題であるとzoxideの記事で整理しています。
まとめ
環境が1つに決まるなら静的ファイル、ディレクトリで変わるならフック、と選ぶと迷いません。venvはsource .../activateの一行で足り、condaはconda shell.bash hookの評価を先に置く形が要ります。フックから書くときは、>か>>かで残る変数が変わるため、書き込み先を共有するフックの数で決めます。