Claude Codeにうっかり貼ってしまった秘密情報・個人情報を、送信前に止めるフックを自作してみた

Claude Codeにうっかり貼ってしまった秘密情報・個人情報を、送信前に止めるフックを自作してみた

Claude Codeを使った開発時に、うっかり秘密鍵やAPIトークン、顧客のメールアドレスなどをClaudeに送信してしまうのを防ぐため、送信前フックで自動検出・ブロックする仕組みを実装してみました。ダミーデータもまとめてキャッチし、マスク済みテキストをクリップボードに用意するので、手間なく安全に再送信できます。
2026.10.01

はじめに

Claude Codeでの開発中、エラーログやAPIレスポンス、DBのレコードなどをそのまま貼り付けて相談したくなりますよね。しかし、そこにアクセストークンや顧客の個人情報が含まれていた場合、一度送信してしまうと取り消すことはできません。

そこで、入力・貼り付けたプロンプトをローカルで検査し、機密情報を検出したら送信をブロックするフックを作成しました。ブロック時には該当箇所をマスクしたテキストがクリップボードにコピーされるため、内容を確認してすぐに貼り直して送信できます。

前提:この仕組みでカバーできる範囲

  • できること: 送信前にローカルでルール検査を行い、合致したプロンプトの送信を止める。
  • できないこと: あらゆる機密情報の100%検出。また、元の入力をバックグラウンドで自動置換して送信すること。
  • 注意点: スクリプトの起動失敗やタイムアウト時はプロンプトがそのまま通過します(完全な流出防止策ではありません)。

以前の記事「AutoModeでも安心できるセキュリティ設定をしてみた話」ではファイル読み取り等の対策を紹介しましたが、今回はユーザー自身の直接入力ミスに対象を絞った対策です。

なぜ送信前に止めるのか

sandboxやpermissionsは、主にClaude側が実行するツール操作(コマンド実行やファイル読み書き)を制限する仕組みです。ユーザーが入力欄へ直接貼り付けた文字列まではマスクしてくれません。

利用プランや学習設定にかかわらず、誤って送信した秘密情報はトークンの再発行が必要になりますし、個人情報であれば社内規程によってはインシデント報告が必要になります。また、Claudeがその値をコードやコミットメッセージに引用し、リポジトリ内に拡散してしまうリスクもあります。

送信後の対応に追われるのではなく、入力の段階で「もう一段のセーフティネット」を設けるために作成しました。

UserPromptSubmit でできること・できないこと

今回利用したのは、プロンプトがClaudeに渡される直前に実行される UserPromptSubmit フックです。標準入力からJSON(prompt フィールド)を受け取り、送信ごとに呼び出されます(デフォルトタイムアウト30秒)。

項目 できること できないこと
本文の扱い 送信前に本文を取得・検査できる 本文を書き換えて送信することはできない(additionalContext の追記のみ可能)
制御 exit 2 や decision: "block" で送信を止められる exit 1 等のエラー終了やタイムアウト時は、ブロックされず送信されてしまう
通知 ユーザー画面にのみブロック理由を表示できる —

フック側でプロンプトを自動置換して送信することは仕様上できません。そのため、「検出したらいったん送信を止め、ユーザーがマスク結果を確認して手動で再送信する」 という設計にしています。

参考:Hooksリファレンス

動作フロー

  1. ユーザーがテキストを入力して送信。
  2. ローカルのPythonスクリプトが呼び出され、prompt の内容を検査。
  3. 機密情報を検出した場合は送信をブロックし、該当箇所を **** に置き換えたテキストをクリップボードにコピー。
  4. ユーザーがマスク結果を目視確認したうえで貼り付け、再送信。

人名や住所など、正規表現ルールで拾いきれなかった情報が残っている可能性があるため、再送信前の目視確認を前提とした運用にしています。

検出対象とアプローチ

Pythonの標準ライブラリのみで実装し、パターン化しやすい秘密情報と代表的な個人情報に対象を絞っています。

アプローチ 主な対象 判定の考え方
固定パターン AWSアクセスキー、GitHubトークン、JWTなど 接頭辞、使用文字種、文字列長をチェック
前後の文脈 Bearerトークン、URL内のパスワードなど 認証を示すキーワードや前後の区切り文字から抽出
キーと値のペア APP_CLIENT_SECRET=...、password: ...など 設定ファイル等でよく使われるキー名と値の組み合わせをチェック
形式+バリデーション クレジットカード番号、マイナンバーなど 桁数に加え、チェックデジットの計算アルゴリズムで検証
形式+除外条件 メールアドレス、日本の電話番号 形式の一致を確認しつつ、テスト用ドメイン(example.com等)を除外

※人名や住所、独自仕様のトークン、キー名のないランダムな文字列、画像内の文字などは網羅的に検出できません。

設計の工夫

  1. 判定にLLMを使わない
    保護対象の機密情報を判定のために外部モデルへ送信しては本末転倒なため、完全ローカルのPythonスクリプト(command 型フック)で処理しています。
  2. ブロック画面に機密情報を出さない(suppressOriginalPrompt)
    ブロック時のメッセージには、検出した「種別」と「件数」のみを表示します。また、デフォルトではブロック画面に元の入力が Original prompt: として露出してしまうため、JSON出力で hookSpecificOutput.suppressOriginalPrompt: true を指定してターミナルへの表示を抑制しています(スラッシュコマンド(UserPromptExpansion)で止めた場合は対象外です。また、消えるのはブロック時のメッセージだけで、手元の会話の記録や入力履歴には元の文章が残ります)。
  3. スクリプト内のエラーはブロック側に倒す
    例外発生時に誤って送信を通過させないよう、スクリプト内部のエラーはすべてキャッチして exit 2(ブロック)で終了させます。
  4. マスク処理の二重検知防止
    送り直したマスク版が再び止まらないよう、マスク後の **** を検出対象から除外しています。また、複数ルールが同じ箇所に重複ヒットした場合のズレを防ぐため、範囲をマージしてから一括マスクしています。
  5. 誤検知用のスキップタグ
    安全なダミーデータが引っかかった場合に備え、プロンプトの文頭に [allow-pii] や [allow-secret] を記述すると検査を意図的にスキップできる仕組みを取り入れました(文中のタグは無効です)。

実際にブロックされたときの画面です。検出した種類と件数だけが表示され、元の文章は表示されません。

ブロック時の画面。検出した種類と件数だけが表示され、元の文章は表示されない

導入方法

スクリプトを ~/.claude/hooks/prompt-guard.py に配置し、~/.claude/settings.json に登録します。スラッシュコマンド入力も検査対象に含めるため、UserPromptExpansion にも登録します。

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 ~/.claude/hooks/prompt-guard.py",
            "timeout": 10
          }
        ]
      }
    ],
    "UserPromptExpansion": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 ~/.claude/hooks/prompt-guard.py",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

設定ミスの影響でプロンプト送信がブロックされ続けた場合は、以下のコマンドですべてのフックを無効化して起動できます。

claude --settings '{"disableAllHooks": true}'

参考:Hooksリファレンス

検証結果

サンプルデータによる検証

値はすべて架空のもので、トークン類は乱数から生成しました。ブロックされたものは、マスク後の文章をもう一度検査にかけると通過することを確認しています。

区分 件数 結果
秘密情報(止まるべき) 15 15件ブロック
個人情報(止まるべき) 12 12件ブロック
開発で通常入力するもの(止まってはいけない) 11 11件通過
検出できないと分かっているもの 7 7件通過(想定どおり)

1. 止めたサンプル(秘密情報)

サンプル 入力 検出した種類 Claudeに届く内容
ログに出ていたアクセストークン このエラーの原因わかる? Authorization: Bearer eyJhbGci… JWT …Authorization: Bearer ****
OAuthのトークンレスポンス {"access_token": "eyJhbGci…", "refresh_token": "…", "expires_in": 1800} JWT、パスワード等の値 {"access_token": "****", "refresh_token": "****", "expires_in": 1800}
AWSアクセスキー aws configure で AKIA0NWJ… を入れた AWSアクセスキー aws configure で **** を入れた
GitHubトークン(classic) GH_TOKEN=ghp_9KQ7… で gh が動かない GitHubトークン GH_TOKEN=**** で gh が動かない
GitHubトークン(fine-grained) github_pat_3iEq… GitHubトークン ****
Anthropic APIキー ANTHROPIC_API_KEY=sk-ant-api03-wKDC… Anthropic APIキー ANTHROPIC_API_KEY=****
Slack Webhook URL https://hooks.slack.com/services/TD46… Webhook URL ****
秘密鍵 -----BEGIN OPENSSH PRIVATE KEY-----… 秘密鍵 ****
DB接続文字列のパスワード DATABASE_URL=postgres://app:soCR…@db.internal:5432/app URL内のパスワード DATABASE_URL=postgres://app:****:5432/app
.envの中身 APP_CLIENT_ID=3f10…⏎APP_CLIENT_SECRET=MZyx… パスワード等の値 APP_CLIENT_ID=3f10…⏎APP_CLIENT_SECRET=****
curlのBasic認証 curl -H 'Authorization: Basic dLp4…' https://api.example.jp/token 認証ヘッダーの値 curl -H 'Authorization: Basic ****' https://api.example.jp/token
日本語のキー名 外部APIのクライアントシークレットは ANixJQMi… です(英字のみ24文字) パスワード等の値 外部APIのクライアントシークレットは **** です
Stripeのキー sk_live_9MmR… Stripeキー ****
32桁の16進数 api_secret c13b…(32桁) 16進数の文字列 api_secret ****
ダミー語を含むパスワード password=MyTestLife2024! パスワード等の値 password=****

2. 止めたサンプル(個人情報)

サンプル 入力 検出した種類 Claudeに届く内容
問い合わせ文中のメール 会員の yamada.hanako@mail.test さんがログインできないそうです メールアドレス 会員の **** さんがログインできないそうです
携帯番号 折り返しは 080-0000-1234 までとのこと 電話番号 折り返しは **** までとのこと
携帯番号(ハイフンなし) tel: 07000001234 電話番号 tel: ****
固定電話 店舗の番号は 06-0000-1234 です 電話番号 店舗の番号は **** です
国際形式 +81 80 0000 1234 電話番号 ****
全角数字の電話番号 080-0000-1234 電話番号 ****
クレジットカード番号 カード 4242 4242 4242 4242 で決済エラー クレジットカード番号 カード **** で決済エラー
マイナンバー マイナンバーは123456789018です マイナンバー マイナンバーは****です
DBの会員レコード {"userId": "c887…", "email": "k.suzuki@mail.test", "tel": "090-0000-4408"} メールアドレス、電話番号 {"userId": "c887…", "email": "****", "tel": "****"}
会員一覧のCSV 10000001,sato.ren@mail.test,090-0000-2031(2行) メールアドレス×2、電話番号×2 10000001,****,****(2行とも)
テストコードのダミー値 { email: "dummy_01@mail.test", tel: "090-0000-0000", password: "testPassword123" } メールアドレス、電話番号、パスワード等の値 { email: "****", tel: "****", password: "****" }
ブロック時の画面表示を貼ったもの 解除タグの説明文とOriginal prompt: const member = { email: "dummy_01@mail.test", tel: "090-0000-0000" }を含む画面表示 メールアドレス、電話番号 Original prompt: const member = { email: "****", tel: "****" }(説明文中のタグでは解除されない)

3. 通ったサンプル(検出できないと分かっているもの)

サンプル 入力
氏名と住所 山田花子さん(東京都千代田区丸の内9-9-9)から問い合わせ
区切りを崩したメール yamada.hanako at mail dot test
キー名のない乱数 これ使って z6VW…(24文字)
接頭辞のない長い乱数トークン ZQ6g8NWc…(173文字)
独自の接頭辞のキー acme_live_2Cat…
Base64にくるんだ値 ZXhhbXBsZTpQYXNzdzByZCE=
画像(スクリーンショット) 文字がないので検査されない

この仕組みで防げないこと(限界事項)

  1. 非定型のテキストや画像内の文字
    人名、住所、特定SaaS独自のトークン、キー名のないランダムな文字列、Base64文字列、画像(スクリーンショット)内の文字は検出できません。
  2. ファイル参照やツールによる読み取り
    検査できるのは「入力欄へ直接打ち込んだプロンプト本文」のみです。@ 記法で参照したファイルや、ClaudeがReadツール等で自律的に読み込むファイルの内容は防げません。これらは permissions.deny の Read(...) などで制限する必要があります(sandboxの denyRead が効くのはBashなどのシェルコマンドだけで、Readツールや @ での参照は止められません)。
  3. Claudeが自分で行う処理の中身
    このフックが検査するのは、ユーザーが送信したプロンプトだけです。Claudeが自分で呼び出したサブエージェントへの指示や、WebFetchなどのツールで取得した内容は検査されません。

参考:Hooksリファレンス、サンドボックス、権限の設定

おわりに

UserPromptSubmitフックを使って、うっかり貼り付けた秘密情報や個人情報を送信前に止め、マスク済みの文章を用意する仕組みを作りました。

ただし、正規表現では拾えない情報もあり、フックが動かなかった場合はそのまま送信されます。あくまで「貼らないのが大前提で、うっかりミスを拾うための安全網」です。何が止まり、何がすり抜けるのかを把握したうえで使うことが大切です。

今後は運用しながら、防ぎたいものが見つかるたびにルールを改善していく予定です。参考になれば幸いです。


Claudeならクラスメソッドにお任せください

クラスメソッドは、Anthropic社とリセラー契約を締結しています。各種製品ガイドから、業種別の活用法、フェーズごとのお悩み解決などサービス支援ページにまとめております。まずはご覧いただき、お気軽にご相談ください。

サービス詳細を見る

この記事をシェアする

AI白書

関連記事