![[Claude Code] env scrubで何が消えるのか確認し、secretlint・gitleaksでシークレット漏洩を多層防御する](https://images.ctfassets.net/ct0aopd36mqt/3KBTm8tdpO9RJJuaVvVzod/a9964bb03097b448b2327edc6920bf9f/Claude.png?w=3840&fm=webp)
[Claude Code] env scrubで何が消えるのか確認し、secretlint・gitleaksでシークレット漏洩を多層防御する
こんにちは。サービス開発室の武田です。
Claude Codeに作業をお願いすると、生成コードに認証情報がそのまま入ってしまうことがあります。環境変数から読んだ値を設定ファイルに書き出したり、.envの中身をREADMEのサンプルに転記したり、といった挙動です。つい見逃してしまうことも多いです。
Claude Code v2.1.83で、この経路の一部を塞ぐCLAUDE_CODE_SUBPROCESS_ENV_SCRUBという環境変数が追加されました。サブプロセスへの環境変数伝播が対象で、ファイルやプロンプトに直接書かれるシークレットは別途対策が必要です。
今回はCLAUDE_CODE_SUBPROCESS_ENV_SCRUBの挙動を確認してみました。合わせて、secretlint・gitleaks・Claude Code Hooksを組み合わせた多層防御の構成例も整理します。
v2.1.83で追加されたCLAUDE_CODE_SUBPROCESS_ENV_SCRUB
CHANGELOG.mdの原文を引用します。
2.1.83
- Added
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1to strip Anthropic and cloud provider credentials from subprocess environments (Bash tool, hooks, MCP stdio servers)
対象は次の3つのサブプロセスです。
Bashツールが起動するシェル- Hooksの実行
- MCP stdioサーバー
スコープは「Anthropic and cloud provider credentials」、つまりANTHROPIC_API_KEYやAWS_*などです。
少し後のv2.1.98では、Linux環境でのみPID名前空間分離が追加されました。
2.1.98
- Added subprocess sandboxing with PID namespace isolation on Linux when
CLAUDE_CODE_SUBPROCESS_ENV_SCRUBis set, andCLAUDE_CODE_SCRIPT_CAPSenv var to limit per-session script invocations
macOSは対象外です。執筆時点(2026年4月)で公式日本語ドキュメントにはCLAUDE_CODE_SUBPROCESS_ENV_SCRUBに関する記述はなく、明示的に有効化する必要があります。
scrub OFF/ONで何が消えるのか確認してみた
CHANGELOGの「Anthropic and cloud provider credentials」だけでは、具体的な変数名までは分かりません。実際にOFF/ON比較を取り、差分を見てみます。
検証セットアップ
ダミー認証情報を23個仕込んだ親シェルからclaude --printをheadlessで起動し、UserPromptSubmitフックでprintenvの結果を保存します。CLAUDE_CODE_SUBPROCESS_ENV_SCRUB未設定/=1の2回で差分を取りました。
なぜフック経由で取るかというと、Claude Codeのモデルが安全配慮でprintenvの出力をそのまま返すのを拒否することがあり、かつフック自身がscrubの対象サブプロセスだからです。
仕込んだ変数の抜粋がこちらです。
# 公式に存在する認証情報変数
ANTHROPIC_API_KEY
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
AWS_PROFILE, AWS_REGION
AWS_BEARER_TOKEN_BEDROCK
GOOGLE_APPLICATION_CREDENTIALS, GCP_SERVICE_ACCOUNT_KEY
AZURE_CLIENT_SECRET
OPENAI_API_KEY, CLOUDFLARE_API_TOKEN
ANTHROPIC_FOUNDRY_API_KEY, ANTHROPIC_BEDROCK_BASE_URL
# 接頭辞付きの自前命名
FAKE_AWS_SECRET_ACCESS_KEY, FAKE_AWS_ACCESS_KEY_ID
FAKE_ANTHROPIC_API_KEY, FAKE_OPENAI_API_KEY
FAKE_GOOGLE_APPLICATION_CREDENTIALS, FAKE_AZURE_CLIENT_SECRET
FAKE_CLOUDFLARE_API_TOKEN, FAKE_GITHUB_TOKEN, FAKE_DB_PASSWORD
ANTHROPIC_API_KEYにダミー値を入れるとClaude Code自身の認証は失敗します。ただしUserPromptSubmitフックはプロンプト処理段階で実行されるので、サブプロセス環境への伝播だけは観測できます。
結果
| 変数名 | 親シェル | scrub OFF | scrub ON |
|---|---|---|---|
ANTHROPIC_API_KEY |
✅ | ✅ | ❌ scrubbed |
AWS_ACCESS_KEY_ID |
✅ | ✅ | ✅ |
AWS_SECRET_ACCESS_KEY |
✅ | ✅ | ❌ scrubbed |
AWS_SESSION_TOKEN |
✅ | ✅ | ❌ scrubbed |
AWS_PROFILE |
✅ | ✅ | ✅ |
AWS_REGION |
✅ | ✅ | ✅ |
AWS_BEARER_TOKEN_BEDROCK |
✅ | ✅ | ❌ scrubbed |
GOOGLE_APPLICATION_CREDENTIALS |
✅ | ✅ | ❌ scrubbed |
GCP_SERVICE_ACCOUNT_KEY |
✅ | ✅ | ✅ |
AZURE_CLIENT_SECRET |
✅ | ✅ | ❌ scrubbed |
OPENAI_API_KEY |
✅ | ✅ | ✅ |
CLOUDFLARE_API_TOKEN |
✅ | ✅ | ✅ |
ANTHROPIC_FOUNDRY_API_KEY |
✅ | ✅ | ❌ scrubbed |
ANTHROPIC_BEDROCK_BASE_URL |
✅ | ✅ | ✅ |
FAKE_AWS_SECRET_ACCESS_KEY |
✅ | ✅ | ✅ |
FAKE_AWS_ACCESS_KEY_ID |
✅ | ✅ | ✅ |
FAKE_ANTHROPIC_API_KEY |
✅ | ✅ | ✅ |
FAKE_OPENAI_API_KEY |
✅ | ✅ | ✅ |
FAKE_GOOGLE_APPLICATION_CREDENTIALS |
✅ | ✅ | ✅ |
| そのほかFAKE_* | ✅ | ✅ | ✅ |
scrub ONで消えたのは7変数でした。OFF/ONの差分はこんな感じです。
< ANTHROPIC_API_KEY
< AWS_SECRET_ACCESS_KEY
< AWS_SESSION_TOKEN
< AWS_BEARER_TOKEN_BEDROCK
< AZURE_CLIENT_SECRET
< GOOGLE_APPLICATION_CREDENTIALS
< ANTHROPIC_FOUNDRY_API_KEY
> CLAUDE_CODE_SUBPROCESS_ENV_SCRUB
結果から見えること
CHANGELOGの「Anthropic and cloud provider credentials」という表現から想像する範囲と比べると、対象はかなり限定的なホワイトリスト方式でした。
AWS_ACCESS_KEY_IDはON時も残った。secretとsession tokenとBedrock APIキーが消える一方、AWS_PROFILEやAWS_REGIONはそのまま伝播するOPENAI_API_KEYやCLOUDFLARE_API_TOKENはON時に残った。少なくとも今回投入した値はscrubされていないANTHROPIC_API_KEYとANTHROPIC_FOUNDRY_API_KEYは消える。一方、エンドポイント設定のANTHROPIC_BEDROCK_BASE_URLは残るFAKE_AWS_SECRET_ACCESS_KEYのように、対象変数名に接頭辞や接尾辞が付くとマッチしない
今回消えたのは次の7変数です。
- Anthropic API key
- AWS secret / session / Bedrock bearer token
- Azure client secret
- Google ADC
- Anthropic Foundry API key
ファイルやプロンプトへの直接記述、観測範囲外の変数名などは対象外で、別の層で守る必要があります。投入したのは公式変数14個と接頭辞付きダミー9個で、実装全体を網羅した検証ではない点はご注意ください。
ファイルに書かれたシークレットは別途防御する
env scrubは実行時の環境変数伝播をやめる機能で、ファイル内に書かれたAWS_SECRET_ACCESS_KEY=...のような文字列は対象外です。プロンプト本文に貼られたシークレットも同様です。
第2層・第3層として、次の2ツールを組み合わせます。
- secretlint
- Markdown含む幅広いファイルを対象とできるNode.js製シークレットリンター
- gitleaks
- Goバイナリの定番、Git stagedや履歴も対象にできる
secretlintの検出ロジックをソースから確認する
両ツールのカバー範囲を見積もる前に、まずsecretlintがどこまで検出するかを実装で確認します。secretlint v12.3.1のソース(secretlint/secretlintリポジトリのv12.3.1タグ)を見ると、各ルールは本物のキーフォーマットに厳密にパターンマッチする設計でした。
AWSルール
packages/@secretlint/secretlint-rule-aws/src/index.tsの該当箇所を引用します。
// L10(オプション型定義)
enableIDScanRule?: boolean; // Default: false
// L17-20
export const BUILTIN_IGNORED = {
AWSAccountID: ["AKIAIOSFODNN7EXAMPLE"],
AWSSecretAccessKey: ["wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"],
};
// L64(AWS Access Key ID 用パターン)
const AWSAccessKeyIDPattern =
/\b(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g;
// L103(AWS Secret Access Key 用パターン。コメントに「AWS?_SECRET_ACCESS_KEY=XXX のみマッチ」と明記)
const AWSSecretPatten = new RegExp(
String.raw`${QUOTE}${AWS}(?:SECRET|secret|Secret)_?(?:ACCESS|access|Access)_?(?:KEY|key|Key)${QUOTE}${CONNECT}${QUOTE}([A-Za-z0-9/\+=]{40})${QUOTE}\b`,
"g"
);
読み取れることは次の3点です。
- AWS公式ダミー(
AKIAIOSFODNN7EXAMPLE、wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY)はBUILTIN_IGNOREDで意図的に除外されている。AWS公式サンプルキーがStack Overflow等で大量に拡散されているための措置 - 裸の
AKIA...単独値の検出はenableIDScanRule: trueのオプトイン。誤検知を避けるためデフォルトはオフ - 正規表現が示すとおり、AWS Secret Access Keyは
AWS_SECRET_ACCESS_KEY=<40文字>のような代入形式を要求する。値だけ単独では検出されない
GitHub / OpenAI / Anthropicの正規表現
secretlint-rule-github/src/index.ts L102〜:
// classic PAT
const CLASSIC_GITHUB_TOKEN_PATTERN =
/(?<!\p{L})(?<type>ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36}(?![A-Za-z0-9_])/gu;
// fine-grained PAT
const FINE_GRAINED_GITHUB_TOKEN_PATTERN =
/(?<!\p{L})(?<type>github_pat)_[A-Za-z0-9_]{82}(?![A-Za-z0-9_])/gu;
secretlint-rule-openai/src/index.ts L25:
const pattern =
/sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}/g;
secretlint-rule-anthropic/src/index.ts L34:
const pattern = /(?<!\p{L})sk-ant-api0\d-[A-Za-z0-9_-]{90,128}AA(?![A-Za-z0-9_-])/gu;
それぞれ要求するフォーマットは次のとおりです。
- GitHub PAT:
ghp_+ちょうど36文字 - OpenAI:中央に
T3BlbkFJという本物キーに必ず含まれる目印 - Anthropic:
sk-ant-api03-またはsk-ant-api04-に続く90〜128文字の本体 + 末尾AA
いずれも本物のキー形式にそろったときだけマッチします。
逆に言えば、長さを一文字でも外したFAKEダミーは検出されません。記事執筆中、私自身もghp_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTu0123のような35文字のダミーを作って一度ハマりました。
検出ベンチマーク
正規表現を満たすフォーマットでサンプルをそろえ、secretlintとgitleaks 8.30.1で検出結果を比較しました。
| サンプル | secretlint(デフォルト) | secretlint(enableIDScanRule: true) |
gitleaks 8.30.1 |
|---|---|---|---|
AWS公式ダミー AKIAIOSFODNN7EXAMPLE |
❌ BUILTIN_IGNORED | ❌ BUILTIN_IGNORED | ❌ |
AKIA + ランダム16文字 |
❌(デフォルトoff) | ✅ AWSAccessKeyID |
✅ |
AWS_SECRET_ACCESS_KEY=<40文字> |
✅ AWSSecretAccessKey |
✅ | ✅ generic-api-key |
ghp_<ちょうど36文字> 形式 |
✅ | ✅ | ✅ github-pat |
sk-proj-…T3BlbkFJ… 形式 |
✅ | ✅ | ✅ |
sk-ant-api03-…AA 形式 |
✅ | ✅ | ❌(gitleaks 8.30.1 デフォルトルール未収録) |
| Slack Incoming Webhook | ✅ secretlint-rule-slack |
✅ | ❌ |
Stripe sk_live_... |
❌(preset未収録) | ❌ | ✅ stripe-access-token |
| PEM Private Key(中身ダミー) | ❌ | ❌ | ✅ private-key |
| PEM Private Key(実鍵フォーマット) | ✅ PrivateKey |
✅ | ✅ private-key |
役割分担
ベンチマーク結果を見ると、両ツールは検出方針が違いますね。
secretlintは前述のとおり、本物のキー形式(接頭辞・長さ・マジックワード・末尾パターン)に厳密に一致したものだけを検出します。一方のgitleaksはREADMEに記載があるとおり、正規表現に加えて文字列のシャノンエントロピー(情報理論でランダム文字列ほど高い値になる指標)の閾値判定を組み合わせる方式です。
本物のAPIキーはたいていランダム性が高いので、フォーマットが多少崩れても「キーっぽい文字列」として拾えます。その反面、ランダム性が高いだけのハッシュ値やBase64文字列まで拾ってしまう誤検知も起きます。
整理するとこうです。
- secretlint:本物キーの指紋に厳密マッチ。誤検知は少ないが、フォーマットが少しでも違えば見逃す。AWS Access Key IDのようなID系は誤検知リスクが高いためデフォルトでオフ
- gitleaks:正規表現+ランダム性の閾値で広めに拾う。誤検知は出やすいが、StripeやPEMなどsecretlint preset未収録のサービスもカバー
- Slack Webhookは逆にsecretlintのみ拾う(gitleaksのデフォルトルールは未収録)
役割を分けるなら、CIやコードレビューにはsecretlint(誤検知が少なく開発を止めにくい)、コミット直前のゲートにはgitleaks(過剰でもいったんやめる)という配置がよさそうです。
Claude Code Hooksで自動化する
Claude Code Hooksでこれらを実行タイミング別に組み込みます。検出時はexit code 2でブロックし、stderrの内容がClaudeにフィードバックされて自己修正が促されます。
| Hook | タイミング | 役割 |
|---|---|---|
PostToolUse Edit|Write |
ファイル書き込み直後 | secretlintで対象ファイルを検査 |
PreToolUse Bash if Bash(git commit *) |
Git commit / push 前 | gitleaksでステージ領域を検査 |
| UserPromptSubmit | プロンプト送信前 | secretlintでプロンプト本文を検査 |
settings.local.jsonの例
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/secretlint-postedit.sh" }
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"if": "Bash(git commit *) || Bash(git push *)",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/gitleaks-precommit.sh" }
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/secretlint-prompt.sh" }
]
}
]
}
}
secretlint-postedit.sh
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
[[ -z "$file" ]] && exit 0
[[ ! -f "$file" ]] && exit 0
cd "${CLAUDE_PROJECT_DIR:-$(pwd)}"
if ! result=$(npx --no-install secretlint --secretlintrc .secretlintrc.json "$file" 2>&1); then
{
echo "Secret leak detected in $file by secretlint:"
echo "----"
echo "$result"
echo "----"
echo "Replace any real credential with a placeholder (e.g. \"<REDACTED>\") and re-attempt the edit."
} >&2
exit 2
fi
exit 0
gitleaks-precommit.shはgitleaks git --pre-commit --staged --no-banner --redactを呼びます。secretlint-prompt.shはプロンプト本文を一時ファイルに書き出してsecretlintに通します。いずれも検出時はexit 2で、Claude側に修正タスクとして返ります。
スクリプトのテスト結果
| テスト | 期待値 | 実測 |
|---|---|---|
secretlint-postedit.shにdirty file(Slack Webhook含む) |
exit 2 + stderr出力 | OK |
secretlint-postedit.shにclean file |
exit 0 | OK |
secretlint-prompt.shにSlack Webhook入りプロンプト |
exit 2 + stderr出力 | OK |
gitleaks-precommit.sh(dirty fileをstaged) |
exit 2 + stderr出力 | OK(leaks found: 6でブロック) |
実際のend-to-end動作
スクリプト単体だけでなく、Claude Code本体からHookを経由する流れもclaude --printで検証しました。テスト用プロジェクトに上記設定を仕込み、「Slack Incoming Webhookを含むファイルを書いて」と指示した結果がこちらです。
[1回目の Write] SLACK=https://hooks.slack.com/services/T00000000/B00000000/FAKEFAKEFAKEFAKEFAKE
↓
[PostToolUse hook] secretlint が検出 → exit 2 + stderr "Replace any real credential with a placeholder..."
↓
[Claude の応答] 「フックが Slack Webhook を検出してブロックしました。指示通りプレースホルダーに置き換えて再試行します」
↓
[2回目の Write] SLACK=<REDACTED>
↓
[PostToolUse hook] 検出なし → exit 0 → 通過、最終ファイルは <REDACTED> 版
結果として、ディスク上のファイルは<REDACTED>へ置換された安全な内容になりました。Claudeのテキスト応答でも「該当値を<REDACTED>に置換して再書き込みしています」と明示しているのが確認できます。
なおPostToolUseはツール実行の直後に動くので、1回目の書き込みはいったん完了してからHookが走ります。Claudeが即座に上書きしてくれるため最終状態はクリーンになります。書き込み自体を事前にやめたい場合は、PreToolUseでtool_input.contentを一時ファイルに書き出してsecretlintに通す構成が必要です。
多層防御マトリックス
3層を整理するとこうなります。
| 層 | 何を守るか | しくみ | スコープ |
|---|---|---|---|
| 1. env scrub | サブプロセスへの環境変数経由の漏洩 | CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 |
対象は7変数(Anthropic API key、AWS secret/session/Bedrock bearer token、Azure client secret、Google ADC、Anthropic Foundry API key)。OpenAI / Cloudflare / 対象外の命名は伝播する。macOSではPID isolationなし |
| 2. secretlint Hook | ファイルやプロンプトに書かれたシークレット | PostToolUse / UserPromptSubmit | 本物キーの指紋に厳密マッチ。誤検知少/フォーマット外は見逃す |
| 3. gitleaks Hook | Git stagedに混入したシークレット | PreToolUse Bash if git commit/push |
正規表現+ランダム性の閾値で広めに拾う。Slack Webhookは未対応 |
各層がカバーする範囲は重ならない部分もあるため、組み合わせて運用する形ですね。
まとめ
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1の実測スコープはAnthropic / AWS / Azure / Googleの認証情報7変数。それ以外の認証情報変数は伝播する- 執筆時点(2026年4月)で公式日本語ドキュメントには本変数の記載がない。明示的に有効化する必要あり
- secretlintは本物のキーフォーマットを厳密に検出する設計。誤検知が少ない一方、長さや構成が外れた偽物は見逃す
- gitleaksは正規表現+文字列のランダム性(エントロピー)の閾値で広めに拾う。フォーマットが多少崩れても拾えるが誤検知も出やすい。Stripe / PEM / JWTなどsecretlint preset未収録サービスもカバー
- Slack Webhookはsecretlintのみカバーでgitleaksは未対応
- Claude Code HooksのPostToolUse / PreToolUse / UserPromptSubmitに組み込むと、検出時にClaudeへ修正タスクとして返るループが組める
env scrubだけで全経路をカバーできるわけではないので、secretlintやgitleaksと組み合わせる構成が実用的です。








