[Claude Code] env scrubで何が消えるのか確認し、secretlint・gitleaksでシークレット漏洩を多層防御する

[Claude Code] env scrubで何が消えるのか確認し、secretlint・gitleaksでシークレット漏洩を多層防御する

2026.04.30

こんにちは。サービス開発室の武田です。

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の原文を引用します。

https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md

2.1.83

  • Added CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 to 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_KEYAWS_*などです。

少し後のv2.1.98では、Linux環境でのみPID名前空間分離が追加されました。

2.1.98

  • Added subprocess sandboxing with PID namespace isolation on Linux when CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is set, and CLAUDE_CODE_SCRIPT_CAPS env 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」という表現から想像する範囲と比べると、対象はかなり限定的なホワイトリスト方式でした。

  1. AWS_ACCESS_KEY_IDはON時も残った。secretとsession tokenとBedrock APIキーが消える一方、AWS_PROFILEAWS_REGIONはそのまま伝播する
  2. OPENAI_API_KEYCLOUDFLARE_API_TOKENはON時に残った。少なくとも今回投入した値はscrubされていない
  3. ANTHROPIC_API_KEYANTHROPIC_FOUNDRY_API_KEYは消える。一方、エンドポイント設定のANTHROPIC_BEDROCK_BASE_URLは残る
  4. 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点です。

  1. AWS公式ダミー(AKIAIOSFODNN7EXAMPLEwJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY)はBUILTIN_IGNOREDで意図的に除外されている。AWS公式サンプルキーがStack Overflow等で大量に拡散されているための措置
  2. 裸のAKIA...単独値の検出はenableIDScanRule: trueのオプトイン。誤検知を避けるためデフォルトはオフ
  3. 正規表現が示すとおり、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.shgitleaks 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と組み合わせる構成が実用的です。

この記事をシェアする

関連記事