無人ランのログを翌朝に見返していたとき、標準出力の一行が目に留まりました。デプロイ用のトークンが、そのままの形でログファイルに残っていたのです。エージェント自身は何も間違っていません。私が渡し方を間違えていました。
秘密情報の事故は、たいてい「盗まれた」のではなく「うっかり書き残した」ところから起きます。リポジトリにコミットしてしまう。ログに吐き出してしまう。エージェントのコンテキストに紛れ込み、そのまま外部モデルへ送られてしまう。個人開発でアプリを運用している私自身、この一件までは注入の作法を軽く見ていました。以下では、Antigravity のエージェントに鍵を渡すときの漏洩経路を三つに分けて塞ぐ構成を、本番運用でそのまま使っている形のまま整理します。
漏洩はどこで起きるのか
まず経路を分けて考えると、対策の重複と抜けがなくなります。私が実際に踏んだもの、踏みかけたものを表にしました。
| 漏洩経路 | 典型的な原因 | この構成での対策 |
| リポジトリ | .env をコミット、コード例に実キーを直書き | gitignore + 実行時注入 |
| ログ・標準出力 | エージェントがコマンド結果をそのまま出力 | マスキングフィルタ |
| エージェントのコンテキスト | 鍵を含むファイルを読み込ませ、モデルへ送信 | 読込対象からの除外 |
三つは別々の対策が要ります。gitignore はログを守りませんし、マスキングはリポジトリを守りません。順に見ていきます。
1. 実行の瞬間だけ注入する起動ラッパー
秘密情報を「ファイルに書いて置いておく」のをやめ、「実行するときだけ環境変数に載せる」方針にします。鍵の保管場所は OS のキーチェーンでも、権限を絞った .secrets.env でも構いません。要点は、エージェントのプロセスにだけ渡し、シェル履歴やリポジトリには残さないことです。
次のラッパーは、gitignore 済みの秘密ファイルを読み、必要なキーだけを export してエージェントを起動します。読み込んだ後すぐにプロセスを差し替えることで、後続のシェルへ意図せず継承されるのを防ぎます。
#!/usr/bin/env bash
# run-agent.sh — 秘密情報を実行時だけ注入してエージェントを起動する
set -euo pipefail
SECRETS_FILE="${HOME}/.config/antigravity/.secrets.env"
# 権限チェック: 本人だけが読める状態でなければ拒否する
if [ "$(stat -c '%a' "$SECRETS_FILE")" != "600" ]; then
echo "拒否: $SECRETS_FILE の権限は 600 にしてください" >&2
exit 1
fi
# 必要なキーだけを許可リストで抜き出す(全 env を丸ごと渡さない)
ALLOWED="DEPLOY_TOKEN API_KEY DB_URL"
env_args=()
while IFS='=' read -r key value; do
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
for a in $ALLOWED; do
if [ "$key" = "$a" ]; then
env_args+=("$key=$value")
fi
done
done < "$SECRETS_FILE"
# env で一時的に付与。呼び出し元のシェルには残らない
exec env "${env_args[@]}" antigravity agent run "$@"
肝は許可リスト方式です。秘密ファイルに書かれた全変数を無条件に渡すと、いつの間にか増えた鍵まで一緒に流れます。「このランに必要なキーはこれだけ」と明示することで、注入する情報の範囲を自分の理解の範囲に留められます。私はこの許可リストを、ランの種類ごとに分けて管理することを推奨します。
2. 出力から鍵を消すマスキングフィルタ
注入した鍵は、コマンドの実行結果やエラーメッセージに紛れて出力に現れます。curl -v の一行、設定のダンプ、スタックトレース。人間が読む前に消したい情報です。
エージェントの標準出力・標準エラーをパイプで受け、既知の秘密値とパターンに一致した部分を伏せ字にします。Node で書いた例です。
// redact.js — 標準入力を読み、秘密値をマスクして標準出力へ流す
const SECRET_VALUES = (process.env.REDACT_VALUES || "")
.split(",")
.filter(Boolean);
// 形状ベースの検出(値そのものを知らなくても消す)
const PATTERNS = [
/\b(gh[pousr]_[A-Za-z0-9]{20,})\b/g, // GitHub トークン
/\b(sk-[A-Za-z0-9]{20,})\b/g, // API シークレット鍵
/\b(AIza[0-9A-Za-z\-_]{20,})\b/g, // Google API キー
/\b([A-Za-z0-9+/]{40,}={0,2})\b/g, // 長い base64 らしき塊
];
function redact(line) {
let out = line;
for (const v of SECRET_VALUES) {
if (v.length >= 6) out = out.split(v).join("****");
}
for (const p of PATTERNS) out = out.replace(p, "****");
return out;
}
let buf = "";
process.stdin.on("data", (chunk) => {
buf += chunk;
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) process.stdout.write(redact(line) + "\n");
});
process.stdin.on("end", () => {
if (buf) process.stdout.write(redact(buf));
});
ここで注意したいのは、値ベースとパターンベースの両方を持つことです。値ベース(SECRET_VALUES)は自分が注入した鍵を確実に消せますが、知らない鍵には無力です。パターンベースはその逆で、形状から未知の鍵も拾いますが、正規表現の網から漏れることもあります。両方を重ねて、片方の穴をもう片方が塞ぐようにしておきます。
ラッパーと組み合わせるなら、起動の最後をこう変えます。
export REDACT_VALUES="${DEPLOY_TOKEN},${API_KEY}"
exec env "${env_args[@]}" antigravity agent run "$@" 2>&1 | node redact.js
3. コミット前・実行前の二段スキャンゲート
マスキングは「出てしまったもの」への防御です。その手前に「そもそも書き込ませない」ゲートを置きます。私は二段に分けています。コミット前と、無人ラン開始前です。
| ゲート | タイミング | 検査対象 | 不合格時 |
| pre-commit | コミット直前 | ステージされた差分 | コミットを中断 |
| pre-run | ラン開始前 | 読込予定のファイル一覧 | ランを中止 |
pre-commit フックの本体は短くて済みます。ステージ済みの差分だけを検査し、鍵らしき形状を見つけたら止めます。
#!/usr/bin/env bash
# .git/hooks/pre-commit — 秘密らしき文字列をステージ差分から検出
set -euo pipefail
pattern='(gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{20,})'
if git diff --cached -U0 | grep -nE "^\+" | grep -Eq "$pattern"; then
echo "拒否: 秘密情報らしき文字列がステージされています。" >&2
echo " git diff --cached で該当行を確認してください。" >&2
exit 1
fi
pre-run 側は、エージェントに読み込ませるファイル一覧を先にスキャンし、秘密ファイルや .env が対象に含まれていないかを確かめます。エージェントは指示されればどんなファイルでも素直に読みますから、読ませる前に人間側で線を引いておきます。読込対象から秘密ファイルを外すことが、三つ目の経路への直接の回避策になります。
セットアップの三手順
導入は次の順に進めると、途中で手が止まりにくくなります。
- 秘密ファイルを
chmod 600 で作成し、gitignore に追加する。次に起動ラッパーの許可リストへ、今回のランで必要な鍵名だけを書く。
redact.js を配置し、ラッパーの末尾を | node redact.js でつなぐ。まず自分の鍵をわざと出力させ、伏せ字になることを確認する。
- pre-commit フックを
.git/hooks に置いて実行権を付与し、pre-run スキャンを無人ランの起動前に差し込む。
実際に入れてみて変わったこと
この三段を二週間ほど運用したあと、過去のログを手元のスキャナで走査してみました。マスキング導入前は、ログ全体の約 4% の行に鍵らしき塊が残っていましたが、フィルタを通した以降の分では検出は 0 件になりました。pre-commit フックは、この間に 2 回、私自身のうっかりコミットを止めてくれました。どちらも .env の貼り付けミスです。
数字よりも大きかったのは、心理的な変化でした。「ログをそのまま誰かに見せられる」という状態が、無人ランを回す上での安心につながります。漏れていないことを毎回目視で確かめる作業から解放されると、エージェントに任せられる範囲そのものが広がっていきました。
完璧な遮断はありません。パターンは新しい鍵の形式に追従して更新が要りますし、マスキングを過信すれば油断も生まれます。それでも、経路を三つに分けて一つずつ塞ぐという考え方は、抜けを見つけやすくしてくれます。どこかで一度ヒヤリとした経験のある方にとって、この構成が小さな安心の土台になれば幸いです。お読みいただきありがとうございました。