値を絶対に表示しない .env diff ツールを作った
きっかけ
深夜 3 時。staging は動いている。本番が燃えている。原因は env のどこかだと分かっている。で、やる:
diff .env.staging .env.prod
そして固まる。diff の出力に両ファイルの全キーの全値が含まれている。ターミナルのスクロールバックに残る。パイプしていたら——ログ、チャット、PR コメント——シークレットがそこに座っている。
そこで .env 用の diff を書いた。ルールは 1 つ:値は絶対に表示しない。
📦 GitHub: https://github.com/sen-ltd/env-diff

作ったもの
TypeScript 約 400 行、ランタイム依存ゼロの CLI。2 つの .env ファイルを比較して、追加・削除・変更されたキーを報告する。値の代わりに SHA-256 のフィンガープリント(8 文字)を表示:
$ env-diff .env.staging .env.prod
~ DATABASE_URL (8a3f12c7 -> 4e91b2d5)
~ LOG_LEVEL (06271baf -> 4d1e008c)
+ SENTRY_DSN (c8a29e4f)
summary: +1 added, -0 removed, ~2 changed, =3 same, 0 excluded
値は出ない。でもどのキーが異なるかは分かる。フィンガープリントが違えば値も違う。
技術的なポイント
ハッシュによる差異証明
「2 つの秘密が異なる」ことを、どちらの秘密も明かさずに伝えるには、値が異なるとき異なり、同じとき同じになる証拠があればよい:
import { createHash } from 'node:crypto';
export function hashValue(value: string): string {
return createHash('sha256')
.update(value, 'utf8')
.digest('hex')
.slice(0, 8);
}
8 文字(32 ビット)に切り詰める。64 文字の hex はノイズだし、100 キー以下のファイルでの衝突確率は宇宙線の心配が先のレベル。
注意:これは値を秘密にするための仕組みではない。true や 8080 のような低エントロピーの値は半秒でレインボーテーブルから逆引きできる。約束は「生の値がこのプロセスのメモリから出ない」ことであって、前像耐性ではない。
–show-values はわざと使いにくくした
- ロングオプションのみ。
-vは他の CLI で “verbose” を意味するので使わない - 完全スペル。
--showでも--valuesでもなく--show-values - 警告表示。出力の前に stderr に警告を出す。消せない。色も付かない
セキュリティ用語でいう意図的な摩擦。フラグが存在するのは時に必要だから。使いにくいのは、ルーチン化すると破滅的だから。
.env パーサー
dotenv は使わず自作した。約 120 行。処理するケース:
export KEY=value(シェルスクリプトからのコピペ対策)- 非クォート値の末尾
# コメント(#の前に空白が必要) - ダブルクォート文字列のエスケープシーケンス(
\n→ 改行) - シングルクォート文字列(リテラル:
'$PATH'は文字列$PATH) - CRLF 改行(Windows ユーザー対応)
- 値中の
=(TOKEN=a=b=c→ 値はa=b=c)
やらないこと:
- 変数補完(FOO=$BAR はリテラル $BAR)。dotenv と docker-compose と systemd で挙動が違うので、どれも選ばない
- 複数行値。複数行が必要な .env はだいたい base64 にすべき
GitHub Actions アノテーション
--format github で GitHub Actions のワークフローコマンドを出力:
lines.push(
`::warning title=env-diff::key ${escape(entry.key)} ` +
`changed ${entry.hashA} -> ${entry.hashB}`,
);
PR の “Files changed” ビューに黄色いアノテーションが表示される。--fail-on-diff と組み合わせれば、.env.example の更新漏れを PR レビュー前にキャッチできる。
おわりに
docker run --rm -v "$PWD":/work ghcr.io/sen-ltd/env-diff /work/a.env /work/b.env
47 件の vitest テスト、MIT ライセンス。
こういう小さなセキュリティ寄りの CLI を作り続ける理由は、デフォルトが製品そのものだから。深夜 3 時に誰もフラグのヘルプを読まない。-v が “verbose” なのか “values” なのか覚えていない。env-diff .env.staging .env.prod と打てば正しいことが起き、シークレットがどこにも漏れない。それ以外は全部、この 1 つの事実の周りを回っている。