.env の代わりに lkr で LLM API キーを安全に管理する — セットアップから Claude Code 連携まで

AI エージェントがローカルファイルを読み書きする時代、.env に平文で置いた API キーが LLM のコンテキストに載るリスクが現実のものになっています。前回の記事ではこの問題の全体像を、aws-vault の記事では AWS 認証情報の保護を解説しました。

本記事では、LLM Key Ring(lkr)を使って LLM API キーを安全に管理する具体的な手順を解説します。aws-vault が AWS 認証情報に特化しているのに対し、lkr は OpenAI・Anthropic・Google など LLM API キーの管理に特化したツールです。

lkr が解決する問題

.env に LLM API キーを置くリスク

多くの開発者は .env ファイルに API キーを平文で保存しています。

1
2
3
# .env(平文で保存されている)
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxx
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx

このファイルには4つの攻撃ベクトルがあります。

攻撃ベクトル説明
Git への混入.gitignore に頼るヒューマンエラー。うっかりコミットは後を絶たない
シェル履歴への漏洩export OPENAI_API_KEY=sk-...~/.bash_history に残る
プロセス情報への露出ps コマンドで環境変数が見える
AI エージェントによる抽出Claude Code がファイルを読み取り、LLM の API リクエストに含まれる

4番目が AI 時代に特有の脅威です。Claude Code は.env ファイルを自動的に読み込むことが確認されており、API キーが意図せず Anthropic のサーバーに送信されるリスクがあります。

lkr のアプローチ

lkr は2つの原則で問題を解決します。

  1. 平文を置かない: API キーを macOS Keychain に暗号化保存する
  2. 必要な瞬間だけ注入: lkr exec で子プロセスの環境変数にのみ注入し、ファイルにも標準出力にも書き出さない
[従来]
.env(平文)→ アプリが読み取り → Claude Code も読める

[lkr]
macOS Keychain(暗号化)→ lkr exec → 子プロセスの環境変数にのみ注入
                                      → プロセス終了で消える
                                      → Claude Code からは見えない

セットアップ

前提条件

  • macOS が必要です(Keychain に依存するため)
  • Rust 1.85 以上 が必要です

Rust が未インストールの場合:

1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

インストール

1
2
3
git clone https://github.com/yottayoshida/llm-key-ring.git
cd llm-key-ring
cargo install --path crates/lkr-cli

インストール確認:

1
$ lkr --version

現時点では Homebrew やバイナリリリースは提供されていません。Cargo でのビルドが唯一のインストール方法です。

最初のキーを登録する

1
2
3
$ lkr set openai:prod
Enter API key for openai:prod: ****
Stored openai:prod (kind: runtime)

キーは対話的プロンプトで入力します。CLI 引数としては受け付けないため、シェル履歴や ps への漏洩を防ぎます。

クリップボードからの貼り付けも可能です:

1
$ pbpaste | lkr set openai:prod

複数のプロバイダを登録

1
2
3
$ lkr set anthropic:main
$ lkr set google:dev
$ lkr set mistral:api

登録できたことの確認

1
2
3
4
5
$ lkr list
openai:prod       runtime
anthropic:main    runtime
google:dev        runtime
mistral:api       runtime

.env ファイルを削除

1
2
3
4
5
6
# 動作確認してから削除
$ lkr exec -- python -c "import os; print(os.environ.get('OPENAI_API_KEY', 'NOT SET'))"
sk-proj-xxxx...

# 確認できたら .env を削除
$ rm .env .env.local

対応プロバイダと環境変数のマッピング

lkr は provider:label 形式のキー名から、環境変数名を自動的に解決します。

Keychain ラベル注入される環境変数
openai:prodOPENAI_API_KEY
anthropic:mainANTHROPIC_API_KEY
google:devGOOGLE_API_KEY
mistral:apiMISTRAL_API_KEY
cohere:prodCOHERE_API_KEY
groq:prodGROQ_API_KEY
deepseek:apiDEEPSEEK_API_KEY
xai:prodXAI_API_KEY

ラベル部分(: の後)は自由に名前をつけられます。同じプロバイダで複数のキーを管理する場合に便利です。

1
2
3
$ lkr set openai:prod       # 本番用
$ lkr set openai:dev        # 開発用
$ lkr set openai:test       # テスト用

lkr の管理対象外のシークレット

lkr が管理できるのは上記8プロバイダの LLM API キーのみです。.env に含まれることが多い以下のようなシークレットは lkr の管理対象外です。

シークレットの種類代替ツール
DB 接続情報DATABASE_URL, REDIS_URLDoppler, AWS Secrets Manager
アプリケーション秘密鍵SECRET_KEY, JWT_SECRETDoppler, AWS Secrets Manager
AWS 認証情報AWS_ACCESS_KEY_IDaws-vault
外部 SaaS の API キーSTRIPE_SECRET_KEY, SENDGRID_API_KEYDoppler, HashiCorp Vault

lkr gen でテンプレートから .env を生成する際も、対応プロバイダに一致しない変数は解決されずそのまま残ります(後述の lkr gen の例で DATABASE_URL が「Kept as-is」となる動作を参照)。

つまり、.env を完全に廃止するには、lkr 単体では不十分であり、シークレットの種類に応じたツールの組み合わせが必要です。

シークレットツール
LLM API キーlkr
AWS 認証情報aws-vault
DB・SaaS 等その他Doppler / AWS Secrets Manager

詳しくは Claude Code 時代の .env 管理 を参照してください。

基本的な使い方

lkr exec — 推奨ワークフロー

最も安全な使い方です。キーは子プロセスの環境変数にのみ注入され、ファイルにも標準出力にも一切書き出されません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 全ての runtime キーを注入してスクリプトを実行
$ lkr exec -- python script.py
Injecting 2 key(s) as env vars:
  OPENAI_API_KEY
  ANTHROPIC_API_KEY

# 特定のキーだけを選択して注入
$ lkr exec -k openai:prod -- python script.py

# 複数のキーを選択
$ lkr exec -k openai:prod -k anthropic:main -- node app.js

# Django の開発サーバーを起動
$ lkr exec -- python manage.py runserver

# Jupyter Notebook を起動
$ lkr exec -- jupyter notebook

lkr get — 手動取得

環境変数が使えない場面での手動取得手段です。

1
2
3
$ lkr get openai:prod
Copied to clipboard (auto-clears in 30s)
  sk-p...3xYz  (runtime)

デフォルトの動作:

  • 出力はマスク表示(末尾4文字のみ)
  • クリップボードに自動コピー
  • 30秒後にクリップボードを自動消去(SHA-256 でハッシュ照合し、別の内容がコピーされていた場合はスキップ)

オプション:

1
2
3
4
5
6
7
8
# 全文を表示(ターミナルで目視確認したい場合)
$ lkr get openai:prod --show

# 生値を出力(パイプに渡す場合。非対話環境ではブロックされる)
$ lkr get openai:prod --plain

# JSON 形式で出力
$ lkr get openai:prod --json

lkr gen — テンプレートからの設定ファイル生成

.env 形式や JSON 形式のテンプレートから、Keychain の値を解決して設定ファイルを生成します。exec が使えない場合のフォールバックです。

.env テンプレートから生成

1
2
3
4
# .env.example(テンプレート)
OPENAI_API_KEY=your-key-here
ANTHROPIC_API_KEY=
DATABASE_URL=postgres://localhost/mydb
1
2
3
4
5
6
7
8
$ lkr gen .env.example -o .env
  Resolved from Keychain:
    OPENAI_API_KEY       <- openai:prod
    ANTHROPIC_API_KEY    <- anthropic:main
  Kept as-is (no matching key):
    DATABASE_URL

  Generated: .env (2 resolved, 1 unresolved)

生成されたファイルは 0600(所有者のみ読み書き可能)のパーミッションが設定されます。.gitignore に含まれていない場合は警告が表示されます。

JSON テンプレートから生成(MCP サーバー設定)

MCP サーバーの設定ファイルなど、JSON 形式が必要な場合に使います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// mcp.json.template
{
  "mcpServers": {
    "codex": {
      "env": {
        "OPENAI_API_KEY": "{{lkr:openai:prod}}"
      }
    }
  }
}
1
$ lkr gen mcp.json.template -o mcp.json

{{lkr:provider:label}} プレースホルダが Keychain の値に解決されます。

lkr rm — キーの削除

1
2
3
4
5
# 確認プロンプトあり
$ lkr rm openai:prod

# 確認なしで削除
$ lkr rm openai:prod --force

lkr usage — API 使用量の確認

1
2
3
4
5
6
7
8
# 特定のプロバイダの使用量を確認
$ lkr usage openai

# 全プロバイダの使用量を確認
$ lkr usage

# JSON 形式で出力
$ lkr usage --json

使用量の確認には admin キーが必要です。

runtime キーと admin キーの分離

lkr のキー管理の中核にある設計です。推論用キーと管理用キーでは、漏洩時の影響が大きく異なります。

分類用途漏洩時の影響
runtimeAPI 推論呼び出し不正利用による課金増加
admin課金・使用量管理アカウント全体の制御を奪われる
1
2
3
4
5
# デフォルトは runtime
$ lkr set openai:prod

# 明示的に admin に指定
$ lkr set openai:admin --kind admin
操作runtimeadmin
lkr exec注入される注入されない
lkr gen解決される解決されない
lkr list(デフォルト)表示非表示
lkr list --all表示表示
lkr usage不可必要

「全キーを一括注入」という安易な運用を防ぎ、権限の最小化を実現しています。

TTY ガード — AI エージェント対策

lkr の最も特徴的な防御機構です。Claude Code のような AI エージェントがキーを抽出するのを防ぐ3つのレイヤーがあります。

レイヤー 1: 生値出力のブロック

1
2
3
4
5
# パイプ経由(非対話環境)での --plain はブロックされる
$ echo | lkr get openai:prod --plain
Error: --plain and --show are blocked in non-interactive environments.
  This prevents AI agents from extracting raw API keys via pipe.
  Use --force-plain to override (at your own risk).

検出には isatty(fd レベルの TTY チェック)を使用しています。CITERM 環境変数は容易に偽装できるため、意図的に無視しています。

レイヤー 2: クリップボードコピーのブロック

1
2
$ echo | lkr get openai:prod
Clipboard copy skipped (non-interactive environment).

lkr get key && pbpaste のような抽出チェーンを防ぎます。

レイヤー 3: exec ワークフローの優先

そもそもシークレットを出力しない exec を推奨ワークフローとすることで、出力ブロックに依存しない設計を実現しています。

既知の制限

IDE の統合ターミナル(VS Code のターミナル等)は isatty(true) を返すため、TTY ガードはバイパスされます。完全な防御ではなく、攻撃コストを上げる設計です。

メモリ保護 — Zeroizing

全てのシークレット値は String ではなく Zeroizing<String> で保持されます。スコープを抜けてドロップされる際に、メモリ上の値がゼロ埋めされてから解放されます。

対象:

  • API キーの取得・保存時の一時バッファ
  • exec での子プロセス環境変数の構築時
  • テンプレート解決時の中間値

ヒープダンプやコアファイルからのシークレット復元を防ぐ仕組みです。

Claude Code との連携

apiKeyHelper で Anthropic API キーを動的取得

Claude Code の settings.jsonapiKeyHelper を設定すると、API キー自体も .env から排除できます。

1
2
3
{
  "apiKeyHelper": "lkr get anthropic:prod --force-plain"
}

Claude Code は起動時にこのコマンドを実行し、出力を API キーとして使用します。デフォルトでは5分ごとに再取得されます。TTL のカスタマイズも可能です。

1
export CLAUDE_CODE_API_KEY_HELPER_TTL_MS=600000  # 10分

deny 設定との併用

Claude Code が .env を読み取らないよう、プロジェクトの .claude/settings.json に deny ルールを追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "Read(./**/credentials*)"
    ]
  }
}

Claude Code を lkr exec で起動する

Claude Code 自体が LLM API キーを使うスクリプトを実行する場合、lkr exec の子プロセスとして起動できます。

1
$ lkr exec -- claude

ただし、Claude Code に API キーが環境変数として渡されるため、Claude Code がそれを読み取る可能性はあります。通常は apiKeyHelper 経由の方が安全です。

実践的なワークフロー

Python(OpenAI / Anthropic SDK)

1
2
3
4
5
6
7
8
# OpenAI SDK を使うスクリプト
$ lkr exec -k openai:prod -- python my_ai_app.py

# Anthropic SDK を使うスクリプト
$ lkr exec -k anthropic:main -- python claude_app.py

# 両方使う場合
$ lkr exec -- python multi_llm_app.py

OpenAI SDK や Anthropic SDK は、デフォルトで OPENAI_API_KEY / ANTHROPIC_API_KEY 環境変数を参照するため、コード変更は不要です。

Node.js

1
2
$ lkr exec -- node app.js
$ lkr exec -- npm run dev

Jupyter Notebook

1
$ lkr exec -- jupyter notebook

Notebook 内のセルで os.environ["OPENAI_API_KEY"] が使えます。

Docker コンテナへの受け渡し

1
2
# 環境変数を Docker に渡す
$ lkr exec -- bash -c 'docker run -e OPENAI_API_KEY=$OPENAI_API_KEY my-app'

Makefile での統合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Makefile
.PHONY: ai-app jupyter test

ai-app:
	lkr exec -- python my_ai_app.py

jupyter:
	lkr exec -- jupyter notebook

test:
	lkr exec -- pytest tests/

server:
	lkr exec -- python manage.py runserver
1
2
$ make ai-app
$ make jupyter

aws-vault との併用

AWS リソースにもアクセスする LLM アプリケーションでは、aws-vault と lkr を組み合わせます。

1
2
# AWS 認証 + LLM API キーの両方を注入
$ aws-vault exec dev -- lkr exec -- python manage.py runserver
認証情報ツール保存場所
AWS アクセスキーaws-vaultmacOS Keychain
AWS 一時認証aws-vault (STS)メモリ上のみ
OpenAI API キーlkrmacOS Keychain
Anthropic API キーlkrmacOS Keychain

Makefile に組み込む場合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# AWS + LLM の両方が必要なアプリ
server:
	aws-vault exec dev -- lkr exec -- python manage.py runserver

# LLM のみ
ai-script:
	lkr exec -- python ai_script.py

# AWS のみ
deploy:
	aws-vault exec prod -- ./scripts/deploy.sh

根本的な限界 — 復号されたシークレットとエージェント

lkr や aws-vault を導入しても、復号されたシークレットが子プロセスの環境変数に注入されるという構造は変わりません。AI エージェントがその子プロセス内で動作していれば、環境変数を読み取ることができます。

lkr exec -- claude
         ↓
  子プロセス(Claude Code)の環境変数に API キーが注入される
         ↓
  Claude Code は os.environ を読める
         ↓
  キーが LLM の API リクエストに含まれる可能性がある

aws-vault でも同じです。aws-vault exec dev -- claude とすれば、AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY が Claude Code のプロセスから読めます。

防げるもの・防げないもの

攻撃ベクトル.env(平文)lkr / aws-vault
ファイルの読み取り(.env防げない防げる(ファイルが存在しない)
シェル履歴への漏洩防げない防げる
Git への混入防げない防げる
エージェントが環境変数を読む防げない防げない

つまり、これらのツールは「平文ファイルの排除」には有効ですが、「実行中のプロセスからの読み取り」には無力です。

リスクを下げるための多層防御

完全な解決は難しいですが、複数のレイヤーを重ねてリスクを下げるアプローチがあります。

1. apiKeyHelper を使う(Claude Code 自身の API キー)

1
{ "apiKeyHelper": "lkr get anthropic:prod --force-plain" }

Claude Code の API キーは環境変数に載らず、Claude Code の内部機構が直接取得します。少なくとも「自分自身の API キー」はコンテキストに載りにくくなります。

2. -k で必要最小限のキーだけ注入する

1
2
3
4
5
# 全キー注入(不必要なキーまで渡してしまう)
$ lkr exec -- python script.py

# OpenAI だけ注入(他のキーは渡さない)
$ lkr exec -k openai:prod -- python script.py

3. エージェントとシークレットを使うプロセスを分離する

最も根本的な対策は、エージェントのプロセスにシークレットを渡さないことです。

1
2
3
4
5
6
7
8
9
# エージェントがシークレットを持つ(危険)
$ lkr exec -- claude

# エージェントとアプリを分離(より安全)
# ターミナル1: Claude Code(シークレットなし)
$ claude

# ターミナル2: アプリ(シークレットあり)
$ lkr exec -- python manage.py runserver

Claude Code にはコードの編集とレビューだけを任せ、シークレットが必要な実行は別プロセスで行う運用です。

4. deny ルールで補強する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "Read(./**/credentials*)"
    ]
  }
}

環境変数の読み取りは防げませんが、ファイル経由のアクセスをブロックする追加の防御層になります。

現実的な結論

lkr や aws-vault は「攻撃コストを上げる」ためのツールであり、「完全な防御」ではありません。lkr 自身も TTY ガードについて同様の位置づけを明示しています。セキュリティは単一のツールで完結するものではなく、複数の防御層を重ねて総合的にリスクを下げるのが現実的なアプローチです。

トラブルシューティング

Keychain のパスワード入力が頻繁に求められる

macOS の「キーチェーンアクセス.app」で、lkr が使用するキーチェーンのロック時間を調整します。

cargo install でビルドエラー

Rust のバージョンが 1.85 未満の場合:

1
$ rustup update

環境変数が注入されない

lkr list でキーが登録されていることを確認してください。admin キーは exec では注入されません。

1
$ lkr list --all  # admin キーも表示

gen で生成したファイルの .gitignore 漏れ

lkr は .gitignore に出力ファイルが含まれていない場合に警告を表示します。必ず .gitignore に追加してください。

1
2
echo ".env" >> .gitignore
echo "mcp.json" >> .gitignore

まとめ

  • 平文を排除: lkr set で API キーを macOS Keychain に暗号化保存し、.env ファイルを削除する
  • 実行時注入: lkr exec でキーを子プロセスの環境変数にのみ注入。ファイルにも標準出力にも書き出さない
  • AI エージェント対策: TTY ガードが非対話環境でのキー抽出をブロックする
  • 権限の最小化: runtime / admin の分離により、推論キーと管理キーを使い分ける
  • コード変更不要: OpenAI SDK / Anthropic SDK はデフォルトで環境変数を参照するため、アプリケーション側の変更は不要
  • Claude Code 連携: apiKeyHelper で API キー自体も動的取得に移行できる
  • aws-vault と併用可能: aws-vault exec dev -- lkr exec -- command で AWS + LLM の認証情報を同時に管理
  • 根本的な限界を理解する: 復号されたシークレットが環境変数に載る以上、エージェントからの読み取りは防げない。プロセス分離と多層防御で対処する

参考