6本中3本の原稿が、機密情報の「注意書き」を理由に公開ブロックされた
図1: 「書くな」と渡した実値が出力へ再掲され、検査器が正しく遮断する自己言及トラップ。
TL;DR
- AIエージェントで記事プロットを6本生成したところ、3本が機密情報チェックでブロックされた
- 原因は元資料からの漏えいではなく、プロンプトに列挙した禁止語をAIが注意書きへ転記したことだった
- これは誤検知ではない。公開候補に実値が入った以上、検出器のブロックは正しい
- 対策は「もっと強く禁止する」ことではなく、 機密値をプロンプトへ入れず、決定的な検査器だけに持たせること
- 入力、生成物、ログの3か所を分離して検査し、検出結果に実値を表示しない
概要
ナレッジDBと既存記事をもとに、AIエージェントで技術記事のプロットを6本まとめて作ったときの話です。すべてのエージェントに公開時の禁止事項を渡し、機密情報を書かないよう明示していました。
それでも完成後の検査では、6本中3本がブロックされました。
正直、最初は「機密情報を書かないよう指示したのに、なぜ」と思いました。しかし原因を追うと、 禁止の指示そのものが、機密情報の配送経路になっていた と分かりました。
この記事では、発生した現象、最初の誤診、真因、修正後のプロンプト、機密検査をAIワークフローへ組み込む設計を、ダミー値だけで再現します。実際の禁止語や内部情報は掲載しません。
目次
- 症状
- 環境と検査の仕組み
- 誤診履歴
- 真因
- 修正
- 動作確認
- 再発防止
- 適用限界
- まとめ
- 参考
症状
記事制作の流れは、次のようなものでした。
ナレッジDB + 既存記事
↓
AIエージェントで6本のプロットを生成
↓
正規表現ベースの機密情報チェック
↓
3本 CLEAN / 3本 BLOCK
この出来事は、その場で修正して終わらせず、原因、発生件数、修正方針をSurrealDBへ記録していました。今回の記事は、その記録を既存のプロットと検査器へ再照合して作っています。「きれいに整理された成功例」だけでなく、後から再検証できる失敗履歴を残す運用については、以前の記事でも紹介しました。
SurrealDBを「AI Agents組織の記憶、集合知」として使う運用
ブロックされた3本には、本文の題材とは関係のない機密パターンが含まれていました。共通していたのは、末尾の注意事項です。
実際の値をダミーへ置き換えると、次のような文章でした。
## 公開前の注意
- 顧客名 `ACME-CONFIDENTIAL` を本文に含めない
- 内部ホスト `internal.example.invalid` を本文に含めない
- 公開前に機密情報チェックを実行する
AIは機密情報を記事の内容として暴露しようとしたわけではありません。「この値を書くな」という指示を理解し、公開前の注意書きとして律儀に再掲していました。
しかし、公開候補のファイルに値が存在する事実は同じです。検査器は意図を問わず、正しくブロックしました。
環境と検査の仕組み
今回の検査器は、JSONで管理したパターンを読み、grepで生成物を走査する小さなBashスクリプトです。実装の要点だけをダミー化すると、次のようになります。
#!/usr/bin/env bash
set -euo pipefail
input_file="$1"
pattern='ACME-CONFIDENTIAL|internal\.example\.invalid'
if grep -qiE -- "$pattern" "$input_file"; then
echo "status=BLOCK"
exit 1
fi
echo "status=CLEAN"
検査器の責務は「パターンに一致する文字列があるか」です。次の2文を意味として区別する責務は持たせていません。
接続先は internal.example.invalid です
internal.example.invalid と書いてはいけません
どちらも同じ文字列を含むため、どちらもブロックします。ここに文脈判定を加えれば賢く見えるかもしれませんが、機密情報の公開防止では安全側に倒す方が重要です。
誤診履歴
誤診1: 注意書きだから誤検知である
最初に立てた仮説は、「禁止を説明しているだけなので、誤検知として除外すればよい」でした。
これは間違いでした。
検出の契機が注意書きでも、実値はすでにプロンプトから生成物へ複製されています。そのファイルが公開、ログ送信、レビュー依頼のいずれかへ進めば、情報の移動範囲がさらに広がります。
分類するなら「誤検知」ではなく、 意図しないポリシー値の再出力 です。
誤診2: 「絶対に出力するな」と強く書けばよい
禁止表現を強める案も考えました。
以下は最高機密である。絶対に出力、引用、要約してはならない:
- ACME-CONFIDENTIAL
- internal.example.invalid
ところが、この対策もプロンプト内に実値を残しています。モデルが必ず指示へ従うことを、セキュリティ境界にはできません。
OWASPは、システムプロンプト自体を秘密やセキュリティ制御として扱うべきではないと説明しています。
the system prompt should not be considered a secret, nor should it be used as a security control.
(訳: システムプロンプトを秘密情報やセキュリティ制御として扱うべきではない)
強い命令は文章上の強調であって、アクセス制御ではありません。
誤診3: 検査器を文脈理解できるAIに置き換える
AIなら「漏えい」と「禁止の説明」を区別できそうです。しかし、今回守りたい境界は「公開候補に実値を一つも含めないこと」です。
この条件に意味理解は必要ありません。むしろ、確率的な判定を追加すると、同じ入力でも通過可否が変わる余地が生まれます。
AIは修正案の作成や分類の補助には使えます。一方、最終ゲートは再現可能な決定的検査に残します。
真因
「指示」と「出力してよいデータ」が同じ入力に入っていた
プロンプトの設計を図にすると、問題は単純でした。
┌──────────────────────────────┐
│ AIへ渡したプロンプト │
│ │
│ 指示: 機密情報を書くな │
│ データ: 書いてはいけない実値 │
└──────────────────────────────┘
↓
AIが指示を文書化する
↓
実値を含む注意書きが完成
人間は「禁止語リスト」を制御情報として読めます。しかし、モデルへ渡した時点では、制御情報も生成に利用できるトークンです。
OWASPのPrompt Injection Prevention Cheat Sheetも、多くのLLMでは自然言語の指示とデータが明確に分離されず、同時に処理されることを根本的な問題として挙げています。
natural language instructions and data are processed together without clear separation.
(訳: 自然言語の指示とデータは、明確に分離されず一緒に処理される)
今回、攻撃者によるPrompt Injectionは起きていません。ただし「指示とデータが同じ文脈へ入り、意図しない出力へ使われる」という構造は共通しています。
3本だけが失敗したことも、安全性の根拠にはならない
6本すべてが失敗したわけではありません。禁止事項を本文へ転記しなかったエージェントもありました。
ここで「半分は守れた」と評価してはいけません。同じポリシーを渡しても、生成物の構成や表現はタスクごとに変わります。今回の実測値は成功率50%ではなく、 設計上の漏えい経路が実際に3回使われた と読むべきです。
モデルの振る舞いへ期待するのではなく、モデルが値を再出力しても公開へ進めない構造を作る必要があります。
修正
修正1: プロンプトには値ではなくカテゴリを書く
プロンプトから実値の列挙を削除しました。
- 次の顧客名を出力しない: ACME-CONFIDENTIAL
- 次の内部ホストを出力しない: internal.example.invalid
+ 顧客名、内部ホスト、個人識別子を出力しない
+ 実値との照合は生成後の機密情報チェックへ委ねる
+ 注意書きにも禁止値を列挙しない
AIが記事を書くために必要なのは、公開できない情報のカテゴリと、一般化・削除の方針です。実値一覧は必要ありません。
修正2: より安全側へ一般化し、実値一覧は決定的検査器だけが読む
実運用で最初に行ったのは、プロンプトから実値を削り、非公開のポリシーファイルを参照させる修正です。これだけでも、プロンプトの注意書きがそのまま成果物へ転記される経路は閉じられました。
その上で、この失敗を他のAIワークフローにも適用できる形へ一般化すると、AI自身が実値一覧を読む必要もありません。責務は次のように分けられます。
図2: 非公開辞書をAIの文脈から外し、検査器だけに持たせる。検出結果も実値ではなくマスクして渡す。
AIエージェント
入力: 公開可能な資料 + 禁止カテゴリ + 編集方針
出力: 記事プロットまたは原稿
↓
機密情報チェック
入力: 生成物 + 非公開のパターン辞書
出力: CLEAN / BLOCK + マスク済みの位置情報
↓
人間レビュー
CLEAN の生成物だけを確認
ポイントは、AIに「辞書を見て気をつけて」と頼むだけで終わらせず、可能ならAIから辞書を遠ざけることです。検査器だけが実値を知り、AIはカテゴリだけを知ります。
修正3: 検出ログにも実値を出さない
検出位置を特定しやすくするため、初期の検査器は一致文字列をログへ表示していました。しかし、CIログやAIツールの実行結果も情報の移動先です。
検査器をCIやAIエージェントから呼ぶ場合、結果はルール名、行番号、件数に限定するのが安全です。
status=BLOCK
rule=internal_host
line=42
count=1
match=***
Secretlintも、CIログ、ターミナル、AIエージェントツールでの偶発的な露出を防ぐため、検出したシークレットをデフォルトでマスクしています。
Secretlint masks secrets in lint error messages by default.
(訳: Secretlintは検出したシークレットを標準でマスクする)
検出器が見つけた値を、その検出器自身が別の場所へ複製しないことも重要です。
修正4: 例外は値ではなく、狭い条件で管理する
テスト用ダミー値や公開済みの識別子まで検出する場合、例外が必要になることがあります。ただし、ファイル全体やルール全体を無効にすると、本物の混入も見逃します。
例外には、少なくとも次の情報を持たせます。
- 対象ルール
- 対象ファイルまたは行
- 例外にする理由
- 追加日
- 見直し期限
Secretlintにはパターン単位のallows、メッセージID単位の抑制、ファイル単位のignoreなど複数の方法があります。選択肢が多いからこそ、最も狭い範囲を選びます。Configuring Secretlint
動作確認
ダミー値を使った最小再現で、検査器が注意書きの意図に関係なく同じ判定を返すことを確認します。
pattern='ACME-CONFIDENTIAL|internal\.example\.invalid'
check() {
name="$1"
text="$2"
if printf '%s\n' "$text" | grep -qiE -- "$pattern"; then
printf '%s\tBLOCK\n' "$name"
else
printf '%s\tCLEAN\n' "$name"
fi
}
check A '顧客名や内部ホストを書かない'
check B 'ACME-CONFIDENTIAL を書かない'
check C '内部情報は公開可能な表現へ一般化する'
check D '接続先は internal.example.invalid です'
期待結果:
A CLEAN
B BLOCK
C CLEAN
D BLOCK
ケースBが今回の自己言及トラップです。ケースDと文章の意図は違いますが、検査結果は同じになります。
実運用へ組み込むときは、次のケースを回帰テストとして残します。
| ケース | プロンプト | 生成物 | 期待結果 |
|---|---|---|---|
| A | カテゴリだけ | 実値なし | CLEAN |
| B | ダミー実値あり | 注意書きへ転記 | BLOCK |
| C | カテゴリだけ | 「内部ホストは書かない」と記載 | CLEAN |
| D | カテゴリだけ | ダミー実値を本文へ混入 | BLOCK |
| E | カテゴリだけ | 警告ログへダミー実値を混入 | BLOCK |
特にケースBを残すことが大切です。今回と同じ失敗を意図的に再現できれば、将来プロンプトや検査器を変更したときも、自己言及トラップが戻っていないか確認できます。
カスタムパターンを本番適用する前に、既存データへdry runし、誤検知を確認する流れはGitHub Secret Scanningの公式手順にも含まれています。
Review the results and identify any false positive results.
(訳: 結果を確認し、誤検知を特定する)
— Defining custom patterns for secret scanning — GitHub Docs
今回のような「実値は一致しているが、出所が注意書き」というケースもdry runで見つかります。ただし、見つけた後の対応はallowlist追加ではなく、入力経路の修正です。
再発防止
1. 入力前、出力後、保存前の3か所で止める
AIワークフローでは、生成物だけを検査すれば十分とは限りません。
[入力前] 公開可能な資料だけか
↓
[出力後] 禁止パターンが生成されていないか
↓
[保存・送信前] 対象ファイルと送信先は正しいか
入力前の確認は、そもそもモデルへ不要な機密情報を渡さないためのものです。出力後の確認は、元資料の見落としや推測による生成を止めます。保存・送信前の確認は、ログ、外部レビュー、公開先を最後に確認する境界です。
2. 「禁止値を知る役」と「文章を書く役」を分ける
一つのAIへ、秘密の一覧、公開文章の生成、公開可否の判断をすべて任せると、責務が衝突します。
- AI: 公開可能な材料から文章を作る
- 検査器: 非公開辞書と照合する
- 人間: 文脈、権利、公開先を判断する
この3者は得意な判定が異なります。AIを使わないのではなく、AIへ渡さなくてよい情報を決めます。
これはモデルごとの能力差をプロンプトで埋める話ではなく、規約、検査、承認をAIの外側へ置く話です。以前まとめた複合QA運用でも、AIの出力を別のAIに承認させて終わるのではなく、決定的な検証と人間判断へ戻すことを重視しました。今回の3本のブロックは、その安全境界が文章生成でも必要だと分かった具体例です。
user-level AI環境でつくる複合QA — Claude / Codex / Gemini相互レビュー運用
3. 検査結果の差分を記録する
推敲で口語表現や比喩を追加すると、一般語がパターンへ偶然一致することもありました。そのため、単に最終結果を見るだけでなく、編集前後で次を比較します。
- ブロック対象の件数
- 警告カテゴリの件数
- 新しく増えた一致
- 例外ルールの増減
一般語の部分一致は、パターン改善の対象です。一方、実値の注意書きへの転記は入力設計の対象です。同じ「検出された」でも、直す場所を分けます。
4. 公開前チェックをエージェントの完了条件に入れる
「原稿を書いたら完了」ではなく、次を完了条件にします。
- 原稿ファイルが存在する
- 機密情報チェックがCLEANである
- 検査ログに実値が出ていない
- 引用元とリンクを確認した
- 人間が公開対象と公開先を確認した
チェックを後工程の善意に任せるより、生成タスクのDefinition of Doneへ入れた方が手戻りを減らせます。
適用限界
正規表現による検査だけで、すべての漏えいを防げるわけではありません。
- 表記揺れ、分割、エンコードで一致しない
- 既知の値しか検出できない
- 文脈から推定できる情報の組み合わせを判定しにくい
- 画像、音声、埋め込みメタデータは別の検査が必要
- 本来公開できる語との部分一致が起きる
そのため、正規表現は最後の1枚ではなく、防御の一層として使います。アクセス制御、入力の最小化、出力検査、ログのマスク、人間レビューを重ねる前提です。
また、検査器を厳しくするほど安全になるとも限りません。警告が多すぎると確認が形骸化します。ブロック対象は実値や高確度パターンへ絞り、一般語や組み合わせリスクは警告として分離すると運用しやすくなります。
まとめ
- 「この機密語を書くな」という指示に実値を含めると、その指示自体が漏えい経路になる
- 注意書きへの転記でも、公開候補に実値が入れば検出器のブロックは正しい
- AIには禁止カテゴリと編集方針だけを渡し、実値辞書は決定的検査器だけに持たせる
- 検出ログもマスクし、入力前、出力後、保存前の3段階で確認する
- 誤検知と入力設計の失敗を分け、例外追加より先に情報の流れを直す
今回いちばん効いた修正は、高度な分類器の追加ではありませんでした。 秘密を守る役に秘密を話しすぎない という、単純な責務分離です。
正直に書くと、最初は検査器を融通の利かない門番だと感じました。答え合わせをすると、融通が利かなかったからこそ、プロンプト設計の問題を3本まとめて止められたわけです。
同じようにAIエージェントへ禁止事項を渡している方は、そのプロンプト自体を一度検査してみてください。値を列挙していたなら、AIの指示遵守率を上げる前に、AIが値を知らなくても仕事を完了できる構造へ変える余地があります。
参考
公式1次資料
- LLM07:2025 System Prompt Leakage — OWASP — システムプロンプトへ機密情報を含めない原則
- LLM Prompt Injection Prevention Cheat Sheet — OWASP — 指示とデータの分離、出力検証、多層防御の実装指針
- Defining custom patterns for secret scanning — GitHub Docs — カスタムパターンのdry runと誤検知確認
関連実装
- Secretlint — GitHub — シークレット検出、マスク、pre-commit連携を行うOSS
- Configuring Secretlint — GitHub — allow、ignore、ルール抑制の設定方法
補足解説
- Custom patterns reference — GitHub Docs — カスタムパターンで利用できる正規表現と追加条件
- SurrealDBを「AI Agents組織の記憶、集合知」として使う運用 — Qiita — 今回の失敗履歴を呼び戻した知識管理の運用
- user-level AI環境でつくる複合QA — Qiita — AIの外側に規約、ログ、安全境界を置く設計

