direnv でプロジェクトごとに環境変数を自動切替する実践メモ
朝イチで別プロジェクトの作業に切り替えた瞬間、OPENAI_API_KEY が前のプロジェクトのままだった。テストは通る。見た目も正常。でも API 課金だけ別口座から引かれていた、というヒヤッとした出来事を半年前にやらかしました。
あれ以来、ローカルの環境変数は全部 direnv に寄せています。
cd するだけで .envrc が自動で読み込まれて、ディレクトリを抜けたら消える。たったそれだけの仕組みなんですが、1 年運用して気付いたら、もうこれ無しには戻れない体になっていました。
direnv とは何か、そして何じゃないか
direnv はシェルに拡張機能を追加する小さなツールです。具体的には、プロンプトが出る直前にフックを仕込んで、カレントディレクトリに .envrc があれば読み込む。抜けたら unset する。やっていることはそれだけ。
誤解されがちなのは、direnv はシークレットマネージャーではないということ。.envrc はただのシェルスクリプトなので、そのままコミットすると鍵が丸出しになります。.gitignore には必ず入れます。
あと、dotenv (.env) とも別物です。dotenv はアプリ側でパース、direnv はシェルに export。レイヤーが違います。うちでは役割分担していて、アプリが読む設定値は .env、シェルコマンドに渡す鍵類は .envrc、という切り分けにしています。
インストールと有効化
Ubuntu 系なら apt で十分。macOS は brew。バージョンが古くて use flake が動かなかったことがあるので、シビアに使うなら GitHub リリースから落とすほうが無難です。
# Ubuntu / Debian
sudo apt install direnv
# macOS
brew install direnv
次にシェルへのフック。bash なら ~/.bashrc の末尾、zsh なら ~/.zshrc に追記。
eval "$(direnv hook bash)" # bash の場合
eval "$(direnv hook zsh)" # zsh の場合
ここを忘れると何も起きず「壊れてる?」と小一時間悩むやつです。自分も一度やりました。
最小の .envrc から始める
試しに適当な作業ディレクトリで、こう書いてみます。
cd ~/work/blog-api
cat > .envrc <<'EOF'
export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxx"
export DATABASE_URL="postgres://localhost/blog_dev"
export PATH="$PWD/bin:$PATH"
EOF
direnv allow
direnv allow を明示的に打たないと読み込まれません。これは任意のディレクトリで勝手にコード実行されないための安全装置で、.envrc が変更されるたびに再承認が要ります。地味に大事。
許可したあと、試しに別ディレクトリに cd してから戻ると、「direnv: loading .envrc」「direnv: export +OPENAI_API_KEY +DATABASE_URL ~PATH」みたいなログが出ます。出なければフックが効いていない合図。
ハマったポイント、正直に書く
1. git にうっかり上げる
一番やりがちなやつ。.envrc は必ず .gitignore に入れるんですが、うちではテンプレートだけコミットするスタイルに落ち着きました。
# .gitignore
.envrc
# .envrc.example (コミット対象)
export OPENAI_API_KEY="sk-..."
export DATABASE_URL="postgres://localhost/mydb"
新しくリポジトリを clone した人は cp .envrc.example .envrc から始める。チーム開発だとこの運用が結局ラク。
2. Python の venv と喧嘩する
これは自分がやらかした最大の沼。.envrc に source .venv/bin/activate って書いたら、ディレクトリを抜けたとき VIRTUAL_ENV は消えるんですが PATH の先頭がゴミになることがあって、python が混線しました。
direnv には layout python という専用ヘルパーがあるので、素直にそっちを使うのが正解です。
# .envrc
layout python python3.11
# これで .direnv/python-3.11/ に venv が自動作成・自動activate
ディレクトリから出れば自動で deactivate される。venv のパスを手で書かずに済む。uv を使うワークフローに移行したあとも、このパターンは併用しています。
3. allow し忘れて CI がコケる
CI サーバーで direnv allow してないディレクトリに入ると、当然 .envrc は読み込まれない。ローカルでは動くのに CI で謎の KeyError: 'API_KEY'。直す方法は単純で、CI では direnv を使わず環境変数を直接注入する。GitHub Actions なら env: セクション、ローカルだけ direnv、という役割分担が現実解です。
手元の .envrc、こう書いてる
auto_daily_trader という自動売買の検証システムを動かしているのですが、そこの .envrc はだいたいこんな感じ。
#!/usr/bin/env bash
# Python 環境
layout python python3.11
# API 鍵類(gitignore 対象)
export BROKER_API_KEY="$(cat ~/.secrets/broker_key)"
export NOTIFY_TOPIC="$(cat ~/.secrets/ntfy_topic)"
# プロジェクト固有の PATH
PATH_add ./scripts
PATH_add ./tools
# ログ出力先
export LOG_DIR="$PWD/logs"
mkdir -p "$LOG_DIR"
鍵そのものを .envrc に書かず、~/.secrets/ 以下のファイルから読むようにしたのは、誤って画面共有で鍵が映り込む事故を一度やりかけたから。cat するだけなら履歴に鍵本体は残りません。
PATH_add は direnv 組み込みの関数で、重複を避けつつ PATH 先頭に追加してくれます。自前で export PATH="$PWD/bin:$PATH" って書くより安全。
チーム運用でハッキリした 3 つのルール
- .envrc は commit しない、.envrc.example だけ commit する。新入社員がいきなり鍵を見る事故が防げる。
- 鍵は別ファイルから読み込む。
.envrcが万一流出しても、実鍵は漏れない二重防御。 - CI では direnv に頼らない。CI は environment secret 経由で環境変数を注入。ローカル専用ツールとして割り切る。
このへんを以前書いた Claude Code の自動化と組み合わせると、シェルから Python から cron まで、環境変数が統一された状態で呼び出せるので、「あの変数どこで定義してたっけ」問題が激減します。
似ているツールとの比較、短く
direnv、nix-shell、devenv、shadowenv あたりが競合になります。うちの結論から言うと、Mac と Linux だけなら direnv で足りる。Nix の宗教戦争に巻き込まれたくない人には direnv の軽さが刺さります。
逆に、コンパイラのバージョンまで含めて完全に再現したいなら Nix や devenv のほうが強いです。ここは用途次第で、うちは環境変数と venv だけ揃えば十分だったので direnv で停まりました。
ちなみに自前のサーバーで同じ .envrc 運用をする場合、お名前.comの高性能VPS あたりの Ubuntu にも同じ手順がそのまま通ります。ローカルと本番で手順が揃うのはけっこう気持ちいい。![]()
運用 1 年で定着したかどうか
結論、定着しました。頻度で言うと 1 日あたり 10 回以上 cd が発生していて、そのたびに direnv が環境を入れ替えている計算です。ミスった瞬間にシェルが教えてくれる安心感は、一度味わうと戻れません。
正直まだ試行錯誤中なのは、Docker コンテナ内でどう扱うか。.envrc をコンテナにマウントするか、あえて使わず --env-file に統一するか、悩んでいます。このへんはまた別の記事で。
systemd user timer で走る自動処理とも相性がよく、cron から systemd timer に移行した話で書いたような常駐ジョブの前処理にも使えます。開発者が手で動かすときと、timer が勝手に動かすとき、環境変数の出どころを揃えておくとデバッグが一気にラクになる、というのが運用してみての実感です。
小さい道具ですが、効いてます。