
enject: .envの秘密を覗き見から守る:秘密はプロジェクトごとにローカルの暗号化ストアに保存され、実行時にアプリに直接注入されるため、平文でディスクに触れることはありません。
.envの秘密情報をprAIngの目から隠します。
(注:このプロジェクトは以前はenveilと呼ばれていましたが、enjectに改名されました)
Claude Code、Copilot、CursorなどのAIコーディングツールはプロジェクトディレクトリ内のファイルを読み取ることができるため、平文の.envファイルは偶発的な秘密情報のダンプが発生する危険があります。これは理論上の話ではありません。実際に何度か(Claude Codeのsettings.jsonファイルで覗かないように明示的に指示した後でも)発生した既知の問題です。enjectは、平文の秘密情報がディスク上に一切存在しないようにすることでこれを解決します。.envファイルにはシンボリック参照のみが含まれ、実際の値は暗号化されたローカルストアに保存され、起動時にサブプロセスに直接注入されます。
このプロジェクトは、Filip Hricのソリューション/ブログ記事に触発されています。彼のソリューションは1Passwordを活用した同様のコンセプトを使用しています。私はサードパーティのサービスに依存しない自己完結型のソリューションを求めていました。そして、このプロジェクトはほぼ全体がClaude Codeで構築され、手動での検証とテストが多数行われています。
このプロジェクトは主に、AI/LLMツールがプロジェクト内の.envの秘密情報を誤って読み取るという既知の問題を軽減するために設計されています。追加の利点として、.envが誤ってリポジトリにコミットされた場合の秘密情報の漏洩防止、参照のみを含む.envファイルを平文の秘密情報の代わりに共有できること、暗号化ストア自体を共有できるオプションなどがあります。
このプロジェクトは、AIエージェントが秘密情報を取得するのを防ぐための万能薬ではありません。たとえば、エージェントは(事故またはプロンプトインジェクションによって)実行時に秘密情報を端末出力やファイルに漏洩させるコードを書き出す可能性があります。このツールや一般的な.envファイルに本番環境の秘密情報を保存することは強くお勧めしません。
.envファイルは次のようになります:
DATABASE_URL=en://database_url
STRIPE_KEY=en://stripe_key
PORT=3000
技術的にはコミットしても安全ですが(ただし、しないほうがいいでしょう)、さらに重要なのは、AIツールが誤って(あるいはそれほど偶然でなく)覗き見しても安全であるということです。
enject run -- npm start を実行すると、以下の処理が行われます:
en://参照を解決ストアファイルはバイナリブロブです。マスターパスワードがなければ、ランダムノイズと区別できません。nonceは書き込みのたびに新しく生成されるため、AES-GCMのnonce再利用は不可能です。暗号文への変更(1ビットの反転でも)は認証に失敗し、復号が拒否されます。
このリリースはまだアルファ版のため、cargo install を呼び出す際に最新バージョンを追加する必要があります。
cargo install enject --version 0.2.0-alpha
Rust 1.70+ が必要です。
git clone https://github.com/greatscott/enject
cd enject
cargo build --release
コンパイルされたバイナリは target/release/enject にあります。PATH 上の場所に一度インストールすれば、どのプロジェクトからでも実行できます。
macOS / Linux(bash または zsh)
# オプションA: ~/.local/bin(sudo不要、Linuxで一般的)
mkdir -p ~/.local/bin
cp target/release/enject ~/.local/bin/
# オプションB: /usr/local/bin(sudoが必要、システム全体で使用可能)
sudo cp target/release/enject /usr/local/bin/
# オプションC: ~/.cargo/bin(rustupを使用している場合はすでにPATHに入っている)
cp target/release/enject ~/.cargo/bin/
オプションAを使用し、~/.local/bin がまだ PATH にない場合は、シェル設定ファイル(~/.zshrc、~/.bashrc、または ~/.bash_profile)に以下を追加します:
export PATH="$HOME/.local/bin:$PATH"
その後、再読み込みします:
source ~/.zshrc # または ~/.bashrc
動作を確認します:
enject --version
バイナリはグローバルにインストールされます — 再インストールする必要はありません。ただし、各プロジェクトには独自の暗号化ストアが必要です:
cd your-project
enject init
これにより、カレントディレクトリに .enject/ が作成され、プロジェクトの設定と暗号化ストアが配置されます。.gitignore に追加してください — コミットしてはいけません。
プロジェクトルートで一度だけ実行します:
enject init
これにより、ランダムな32バイトのソルトが生成され、.enject/config.toml が書き込まれ、空の暗号化ストアが .enject/store に作成され、マスターパスワードの設定が求められます。.enject/ を .gitignore に追加してください — ストアはコミットしてはいけません。
enject set some_database_url
# プロンプト: Value for 'database_url': (非表示)
enject set some_api_key
値は常に対話的に入力されます。コマンドライン引数として値を渡す方法はありません — これにより、シェル履歴や ps の出力に秘密情報が表示されるのを防ぎます。
.env での秘密情報の参照DATABASE_URL=en://some_database_url
MY_API_KEY=en://stripe_key
PORT=3000
プレーンな KEY=VALUE 行はそのまま通過します。en:// 参照のみが解決されます。
enject run -- npm start
enject run -- python manage.py runserver
enject run -- cargo run
-- 以降はすべてOSにそのまま渡されます。サブプロセスは完全なシェル環境を継承し(PATH、HOME などが存在)、その上に .env の値が重ねられます。
enject list # 保存されたキー名を表示(値は表示しない)
enject delete <key> # 秘密情報を削除
enject import <file> # 平文の .env 内のすべての値を暗号化し、en:// テンプレートとして書き換える
enject rotate # 新しいマスターパスワードでストアを再暗号化
get や export はありません。秘密情報の値を標準出力に出力すると、AIが読み取れる漏洩ベクターになります。enjectの目的は、値をディスク上や読み取り可能な出力ストリームから遠ざけることです。
すべてのセキュリティ不変条件には、対応する自動テストと手動検査パスがあります。
cargo test
31個のテストがあり、すべて以下の主張をカバーしています。
自動テスト: store::password::tests::test_encrypt_decrypt_roundtrip
秘密情報を保存し、ストアを永続化し、ディスクから再読み込みし、復号して値が正しくラウンドトリップするかを確認します。ディスク上のバイトが有効な暗号文である場合にのみ成功します。平文の場合は復号に失敗します。
cargo test store::password::tests::test_encrypt_decrypt_roundtrip
手動検査:
enject init # パスワード: test123
enject set mykey # 値: my-super-secret
xxd .enject/store | head -5
strings .enject/store
xxd はバイナリデータを表示します。strings は何も返しません — 抽出可能なASCIIシーケンスはありません。最初の12バイトはランダムなnonceで、それ以降は16バイトの認証タグが付加されたAES-GCM暗号文です。
自動テスト: store::password::tests::test_nonce_changes_on_each_save
ストアを連続して2回保存し、毎回ファイルの最初の12バイトを読み取り、それらが異なることをアサートします。
cargo test store::password::tests::test_nonce_changes_on_each_save
手動検査:
xxd .enject/store | head -1 # 最初の12バイトに注目
enject set anotherkey # 書き込みごとにnonceが変更される
xxd .enject/store | head -1 # 最初の12バイトが異なる
自動テスト: store::password::tests::test_wrong_password_returns_err
あるパスワードでストアを作成し、別のパスワードでロック解除を試み、Err が返されることをアサートします。
cargo test store::password::tests::test_wrong_password_returns_err
手動:
enject list # 間違ったパスワードを入力
# 出力: "Wrong master password or corrupted store."
# 終了コード: 1
AES-GCMは暗号文に対して16バイトの認証タグを生成します。1ビットの反転でも変更があると、復号が行われる前に検証が失敗します。平文が露出することはありません。
自動テスト: store::password::tests::test_tampered_ciphertext_returns_err
ストアファイルの暗号文領域(12バイトのnonce以降)の1バイトを反転し、復号を試みて Err をアサートします。
cargo test store::password::tests::test_tampered_ciphertext_returns_err
手動:
# 20バイト目(nonceを過ぎた暗号文内部)を反転
python3 -c "
data = open('.enject/store', 'rb').read()
bad = data[:20] + bytes([data[20] ^ 0xFF]) + data[21:]
open('.enject/store', 'wb').write(bad)
"
enject list
# 出力: "Wrong master password or corrupted store."
en:// 参照に対してハードエラー.env 内の参照に対応するキーがストアにない場合、enject run は即座に非ゼロの終了コードで終了します。サブプロセスは起動されません。
自動テスト: env_template::tests::test_unknown_ev_ref_returns_err
一致するエントリがない参照を使用して resolve() を呼び出し、Err をアサートします。
cargo test env_template::tests::test_unknown_ev_ref_returns_err
手動:
echo "DB=en://nonexistent_key" > .env
enject run -- env
# 出力: Secret 'nonexistent_key' not found in store. Add it with: enject set nonexistent_key
# 終了コード: 1 (env サブプロセスは実行されない)
複数のプロジェクトで使用される秘密情報の管理を容易にするために、オプション/追加のシステム全体のストアを実装します。
更新のたびにストアのパスワードを手動で入力する必要を減らします。