🔐

.env に API キーを書きたくないので 軽いCLI を作った

に公開

ローカル開発では .env に API キーを置きがちです。

OPENAI_API_KEY=sk-...
DATABASE_URL=postgres://...

これは昔からある普通の運用ですが、AI agent をローカルで動かすことが増えてから少し気になるようになりました。

AI agent にリポジトリを触らせるということは、かなり雑に言うと「別の人に自分の作業ディレクトリを触ってもらう」ことに近いです。もちろん agent や実行環境ごとに安全性は違いますが、少なくとも .env にそのまま secret が入っていると、誤って読ませたり、ログに出したり、スクリーンショットやコピペに混ざったりする可能性が上がります。

この問題に対して、1Password CLI、Infisical、direnv、dotenv 系のツール、あるいは chamber のような既存の選択肢はあります。なので困ってる人はそこら辺のツールを使ってみることをお勧めします。

しかし今回は自分が欲しかったのはもう少し狭くて、

  • .env には repository-safe な参照だけを書きたい
  • 実 credential は OS の credential store に置きたい
  • ただし、可能な API では本物の provider key を child process に渡さない選択肢も欲しい
  • 個人のローカル開発で使えるくらい軽くしたい

というものでした。

そのために EnvVault という小さい CLI を作りました。

EnvVault でやりたいこと

EnvVault は local-first な secret launcher です。

.env には本物の secret ではなく、次のような参照だけを書きます。

OPENAI_API_KEY=envvault://openai/dev
DATABASE_URL=envvault://database/dev

本物の値は macOS Keychain などの OS credential store に保存します。アプリを起動するときだけ、envvault execenvvault://... 参照を解決し、child process に環境変数として渡します。

主な機能はこのあたりです。

  • credential を OS credential store に保存する
  • .env には envvault://... 参照だけを書く
  • envvault exec で child process 起動時に参照を解決する
  • .env を作らずに --env KEY=envvault://... で直接渡せる
  • browser UI から credential と optional proxy を登録できる
  • proxy mode では localhost proxy 経由で provider key を隠せる

今のデフォルトは direct credential flow です。つまり、envvault://openai/dev は OS credential store の openai/dev を読み、child process の OPENAI_API_KEY に実値を入れます。

これは chamber に近い方向の道具です。違いとしては、ローカルの OS credential store を主な保存先にしていること、.envenvvault://... 参照を置くこと、必要なら localhost proxy に切り替えられることです。

proxy機能について

最初は localhost proxy 機能を主役にしようとしていました。

OPENAI_BASE_URL=envvault://openai/dev/base-url
OPENAI_API_KEY=envvault://openai/dev/token

この形だと、child process には本物の OpenAI API key ではなく、EnvVault の local proxy に対してだけ意味を持つ token を渡せます。proxy が method/path を確認してから provider key を付けて upstream に転送します。

これは面白いのですが、当初思っていた時より実際に使える場面が限られたためメイン機能とするのはやめました。

  • SDK やアプリが custom base URL を受け取れる必要がある
  • .env の変数を provider ごとに書き換える必要がある
  • production では普通の provider URL に戻すなど、環境差分が増える
  • DB URL や custom endpoint 非対応 SDK には使えない

安全性だけを見ると proxy の方が強い場面があります。ただ、ローカル開発ツールとしての使いやすさは direct credential flow の方がかなり高いです。

基本アイデア

direct credential flow はこうです。

  1. 実 credential は OS credential store に保存する
  2. .env には envvault://<credential> だけを書く
  3. envvault exec.env を読み、参照を解決する
  4. 解決済み env で child process を起動する

図にするとこうです。

OS credential store
  envvault/credential/openai/dev/value = sk-...
        ^
        |
envvault exec
        |
        v
.env
  OPENAI_API_KEY=envvault://openai/dev
        |
        v
child process
  OPENAI_API_KEY=sk-...

重要なのは、repository に入る .env には実 secret が存在しないことです。

一方で、child process には最終的に raw credential が渡ります。EnvVault は sandbox ではありません。目的は「起動したプロセスから secret を絶対に隠す」ことではなく、「リポジトリや .env に raw secret が残る面積を減らす」ことです。

使い方

Homebrew で install できます。

brew install trknhr/tap/envvault

browser UI を使う場合は、localhost admin を起動します。

envvault admin start

表示された URL を開くと、credential の追加や optional proxy の作成ができます。UI は credential value を表示しません。登録はできますが、あとから値を見る UI にはしない方針です。

CLI で登録する場合はこうです。

printf 'YOUR_API_KEY\n' | envvault credential add openai/dev --value-stdin

登録済み credential は名前だけ確認できます。

envvault credential list

.env にはアプリが期待する env 名で EnvVault 参照を書きます。

OPENAI_API_KEY=envvault://openai/dev

あとは envvault exec 経由で起動します。

envvault exec --env-file .env -- npm run dev

.env ファイルを作りたくない場合は inline env として渡せます。

envvault exec \
  --env OPENAI_API_KEY=envvault://openai/dev \
  -- npm run dev

-- より後ろが child command です。これは地味に重要で、次のように書く必要があります。

envvault exec --env-file .env -- sh -lc 'echo "$OPENAI_API_KEY"'

Gemini SDK での例

公式 Gemini SDK のように API key を env から読む SDK では、direct credential flow がそのまま使えます。

.env はこうです。

GEMINI_API_KEY=envvault://gemini/dev
GEMINI_MODEL=gemini-3.5-flash

credential を登録します。

printf 'YOUR_GEMINI_API_KEY\n' | envvault credential add gemini/dev --value-stdin

アプリ側は普通に環境変数を読むだけです。

import { GoogleGenAI } from "@google/genai";

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const interaction = await ai.interactions.create({
  model: process.env.GEMINI_MODEL || "gemini-3.5-flash",
  input: "Say pong in one short sentence.",
});

console.log(interaction.output_text);

起動はこうです。

envvault exec --env-file .env -- npm start

この場合、child process は本物の Gemini API key を受け取ります。つまり SDK 互換性は高いですが、起動後のプロセスから隠せるわけではありません。

optional: proxy mode

アプリや SDK が custom base URL と bearer token を受け取れる場合は、proxy mode も使えます。

たとえば Gemini の OpenAI-compatible endpoint を AI SDK から使う場合、まず credential を登録します。

printf 'YOUR_GEMINI_API_KEY\n' | envvault credential add gemini-api-key \
  --value-stdin

次に proxy を作ります。

envvault proxy add gemini-openai/dev \
  --credential gemini-api-key \
  --provider openai-compatible \
  --target https://generativelanguage.googleapis.com/v1beta/openai \
  --allow-path /chat/completions \
  --allow-method POST \
  --project-binding none

proxy を作ると、EnvVault は .env snippet を出します。

ENVVAULT_PROXY_URL=envvault://gemini-openai/dev/base-url
ENVVAULT_PROXY_TOKEN=envvault://gemini-openai/dev/token

base-urltoken は credential 名ではなく、proxy が自動で持つ出力です。別途登録するものではありません。

browser UI でも proxy 作成後に同じ snippet を表示し、copy button から .env に貼れるようにしています。

AI SDK 側はこう書けます。

import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText } from "ai";

const gemini = createOpenAICompatible({
  name: "gemini",
  baseURL: process.env.ENVVAULT_PROXY_URL,
  apiKey: process.env.ENVVAULT_PROXY_TOKEN,
});

const { text } = await generateText({
  model: gemini.chatModel(process.env.GEMINI_MODEL || "gemini-3.5-flash"),
  prompt: "Explain EnvVault in one sentence.",
});

console.log(text);

runtime では ENVVAULT_PROXY_URLhttp://127.0.0.1:xxxxx/gemini-openai/dev のような localhost URL に、ENVVAULT_PROXY_TOKEN が local-only bearer token に置き換わります。

本物の Gemini API key は OS credential store に残り、proxy が allowlist を通った request にだけ付与します。

技術構成

実装は Go 製の CLI です。

大きく分けると、次の部品でできています。

CLI
  |
  +-- OS credential store
  |
  +-- config store
  |
  +-- envvault:// reference parser
  |
  +-- env resolver
  |     |
  |     +-- direct credential resolver
  |     +-- optional provider proxy resolver
  |
  +-- child process runner
  |
  +-- localhost admin UI

OS credential store

credential の実値は OS credential store に保存します。

key は次のような階層にしています。

envvault/credential/openai/dev/value
envvault/credential/database/dev/value

macOS では /usr/bin/security を使って default keychain に generic password として保存しています。service は envvault、account は上の key です。

non-darwin 環境では github.com/zalando/go-keyring 経由です。Windows や Linux もこの層を差し替えれば対応できる設計にしていますが、v0.1.x 時点では macOS を主な動作環境として見ています。

config store

config には secret 本体を入れません。

入るのは、credential 名の一覧や optional proxy の policy です。たとえば proxy なら、どの credential を使うか、target URL、許可する method/path、project binding などを持ちます。

config file は OS ごとの user config/data/cache directory に置きます。macOS なら ~/Library/Application Support/envvault/config.yaml です。

reference parser

.env の値が envvault:// から始まる場合だけ、EnvVault reference として扱います。

OPENAI_API_KEY=envvault://openai/dev

この parser はかなり strict にしています。

  • query string を許可しない
  • fragment を許可しない
  • backslash を許可しない
  • %2f%5c のような encoded separator を許可しない
  • ... の path traversal segment を許可しない
  • /value suffix は使わない

envvault://openai/dev は credential openai/dev として解決します。

proxy の場合だけ、次の suffix を特別扱いします。

OPENAI_BASE_URL=envvault://openai-proxy/dev/base-url
OPENAI_API_KEY=envvault://openai-proxy/dev/token

env resolver

envvault exec はざっくり次の順番で env を組み立てます。

  1. 親プロセスの環境変数を読む
  2. --env-file の dotenv を読む
  3. --env KEY=VALUE の inline env を上書きする
  4. 値全体が envvault://... なら reference として parse する
  5. reference を解決する
  6. 解決済み env で child process を起動する

direct credential の場合、resolver は OS credential store から envvault/credential/<name>/value を読みます。

proxy の場合、base-urltoken の参照を見たタイミングで 127.0.0.1:0 に ephemeral server を立てます。同じ envvault exec の中で同じ proxy を複数回参照しても、同じ proxy lease を共有します。

reference の解決に失敗した場合は child process を起動しません。ここは fail closed にしています。

たとえば OPENAI_API_KEY=envvault://openai/dev を解決できないときに、親プロセスの OPENAI_API_KEY に fallback してしまうと、意図せず raw secret を渡す事故が起こります。

localhost proxy

proxy mode では child process に次のような値を渡します。

OPENAI_BASE_URL=http://127.0.0.1:xxxxx/openai-proxy/dev
OPENAI_API_KEY=envvault-local-...

proxy は request に対して、次の順番で確認します。

  1. local token が Authorization: Bearer ... として付いているか
  2. local token が期限切れではないか
  3. method が allowlist に含まれているか
  4. path が allowlist に含まれているか

通った場合だけ、OS credential store から取り出した provider key を upstream 用の Authorization: Bearer ... に付け直して転送します。

ここで child process が持っている token は、provider の API key ではありません。EnvVault の local proxy にしか意味がない token です。

browser UI

CLI だけでも使えますが、credential や proxy を毎回 command で登録するのは面倒なので localhost の admin UI もあります。

envvault admin start

admin server の URL には run ごとの local token が付きます。browser UI はその token を使って localhost API を呼びます。

UI でできることは、credential の追加、credential 名の一覧表示、proxy の作成、proxy 用 .env snippet の copy です。stored credential value は表示しません。

セキュリティモデルと限界

EnvVault は sandbox ではありません。

守れるもの:

  • repository に raw secret を置かない
  • .env を repository-safe な参照だけにできる
  • reference parser を strict にして、意図しない参照表現を弾く
  • reference 解決に失敗したら fail closed にできる
  • proxy mode では provider key を child process に直接渡さない
  • proxy mode では method/path を制限できる

守れないもの:

  • child process が自分の環境変数を読める問題
  • 同じ OS user が OS credential store にアクセスできる問題
  • stdout / stderr / logs / shell history への漏洩
  • 悪意ある process からの完全な隔離
  • provider に送る prompt や request body の中身

direct credential flow は、最終的に child process に credential を渡します。これは SDK 互換性が高い反面、起動後のプロセスから secret を隠すものではありません。

proxy mode は provider key の露出を減らせますが、SDK が custom base URL と bearer token を受け取れる場合に限られます。

EnvVault の目的は「完璧に secret を守る」ことではなく、「ローカル開発で raw secret が雑に露出する面積を減らす」ことです。

agent skill

EnvVault には agent 向けの skill も置いています。

npx skills add trknhr/envvault 

これは EnvVault を使って command を起動したい agent に、envvault execenvvault:// reference の扱いを覚えさせるためのものです。

AI agent に .env の中身を直接見せたくないという動機で作ったツールなので、agent 側から使いやすくしておくのは相性がいいと思っています。

今後

自分のためのツールなので、正直なところ巨大な roadmap はありません。

ただ、時間があればこのあたりは触りたいです。

  • browser UI の改善
  • provider preset の追加
  • Windows / Linux の keyring 動作検証
  • proxy mode の使いどころをもう少し整理する
  • browser automation 時の ID / password 扱いの検討

特に proxy mode は面白い一方で、日常的には direct credential flow の方が使いやすそうです。まずは軽量な secret launcher としてちゃんと使える状態にしたいです。

まとめ

.env に secret を置かないだけなら、既存の選択肢はたくさんあります。正直、EnvVault を使う必要がない人も多いと思います。

ただ、OS credential store に実値を置き、repository には envvault://... 参照だけを置き、起動時にだけ解決する、という小さい道具は自分のローカル開発にはちょうどよさそうでした。

EnvVault はまだ小さい MVP ですが、.env を repository-safe にしたい、でも大きな secret manager は入れたくない、macのkeychainをbackendに使いたいという場面では使えるかもしれません。

Discussion