何を試したか

Cloudflare が 2026 年 6 月に公開した security-audit-skill を、RIKKA M&A の Drupal 基盤モジュール(PHP 54 ファイル・約 6,100 行)に対して走らせました。

この skill は、Claude Code のようなコーディングエージェントを「セキュリティ監査者」に変える手順書です。実体は Markdown 15 本と、Node.js だけで動く検査スクリプト 2 本(validate-findings.cjsvalidate-coverage-ledger.cjs)で、依存パッケージはありません。ライセンスは MIT です。

手順は 6 段階に分かれています。

段階やること担当
1 偵察製品・主体・入口面・ローカル実行可否を 4 本のエージェントが並列で調べるresearch × 4
2 探索「面 × 境界 × 攻撃クラス」の単位ごとに hunter を割り当て、候補を JSON で返させる。終わったら critic が「見ていない単位」を指摘するhunter 多数 + critic
3 候補の検証候補を書いていない別のエージェントに「反証せよ」と渡すverifier
4 構造化出力findings.json を書き、スキーマ検査を通す
5 独立再検証さらに別のエージェントが最終レコードを読み直すverifier
6 報告REPORT.md を JSON から導出する。散文が JSON と食い違うことを禁じる

Cloudflare 自身は、この skill の種になった仕組みで 128 リポジトリを回し、候補 20,799 件から検証を通った 12,057 件を絞り込んだとブログに書いています。ただし、この数字は社内ハーネスのもので、公開 skill の検出率ではありません。README には「1 回の実行で、繰り返し実行が見つけた脆弱性のおよそ半分を見つけた」とだけあり、条件の記載はありません。精度は自分の対象で測るしかないというのが出発点でした。

これまでの監査と何が違うのか

RIKKA M&A では、セキュリティ監査を含む監査を Claude Code で実施してきました。ローンチ前の 2026 年 9 月上旬時点で監査文書は 81 本・30,446 行(セキュリティ以外の検証も含む)あります。手順は自前で決めており、到達面の全列挙、検証環境が本番を再現しているかの測定、稼働環境での否定テスト(「止めるべきものが止まる」を実際に接続して確かめる)、同型の全件つぶし、という流れです。

この手順で見つかった中で最も重い 2 件は「本番の欠陥」ではなく「dev と本番の差」でした。ソースコードを読んでも分からず、稼働環境を測って初めて出る種類の検出です。

一方で、弱点もはっきりしていました。直近 4 本の監査文書を「誤り・誤検知・訂正・撤回」で走査すると、同じエージェントが自分の断定を後から訂正した記録が 18 件ありました。うち 1 件は重要度を「高」で起票してから「低」へ格下げしています。18 件のうちソースコードの読み違いは 2 件だけで、残り 16 件は「Cloudflare のプランは何か」「そのイメージは今のビルドか」といった環境事実の読み違いでした。もう 1 つ、過去監査の「未検証点」欄に 68 件の未検証宣言が溜まっており、回収の走査語に含まれていなかったため、どの棚卸しにも一度も載っていなかったことも判明しています。

Cloudflare の skill は、この 2 つの弱点に「注意深く判断せよ」と指示するのではなく、構造で縛っています。

機構自前の手順(改修前)Cloudflare の skill
発見と検証同じエージェントが発見・判定・記録候補を書いていない別エージェントが反証してから確定
判定の種類高/中/低 + 散文の「未検証点」confirmed / needs_validation / rejected の 3 値
重要度未検証でも付けていたneeds_validation に重要度を付けるとスキーマ検査が落ちる
未検証の回収散文の走査語に依存次回実行が前回の needs_validation を必ず読む
網羅性「N 件中 M 件を見た」を散文で記録coverage-ledger.jsoncovered には証拠が必須
上限無しbudget = エージェント起動回数の上限。尽きたら incomplete と報告する

verifier に渡されるプロンプトの冒頭はこうです。

You did not write this candidate. Try to refute it from repository source and bounded local evidence.

「自分が書いていない候補を、ソースから反証せよ」。これが skill の中核です。

ただし、機械で強制されるのはスキーマとバリデータの範囲だけです。「発見者と検証者を分ける」「サンドボックスが無ければ実行しない」は親エージェントの手順遵守に依存します。hunter が誤った trace を書くこと自体は防げず、それを別エージェントの反証で拾う二段構えです。

起動して 12 分でセッション制限に当たった

対象を基盤モジュール 1 つに絞り、quick プロファイル(hunter の wave 1 回、critic 1 回、候補ごとに verifier 1 本)で、予算(エージェント起動回数の上限)を 30 に設定して始めました。

10:52 に偵察エージェント 4 本、11:12 に hunter 11 本を同時に起動しました。セッションのモデルは Opus で、サブエージェントはそれを引き継ぎます。

11:23 から 11:26 にかけて、hunter 9 本が次のエラーで止まりました。

Agent terminated early due to an API error:
You've hit your session limit · resets 3pm (Asia/Tokyo)
(error type rate_limit, HTTP 429, model sent to the API: claude-opus-5)

契約は Max プラン(5x)です。公式ヘルプによれば、セッション単位の使用量上限は 5 時間ごとにリセットされ、Max 5x は Pro の 5 倍のセッション使用量です(出典: What is the Max plan?)。また Opus は「Sonnet より 1 ターンあたり数倍のコスト」がかかります(出典: Models, usage, and limits in Claude Code)。10 時に始まった 5 時間窓の枠を、11:26 の時点で使い切っていました。 この窓の中で起動した Opus のサブエージェントは 15 本、うち 11 本は同時起動です。

止まった 9 本のうち 3 本は、落ちる直前に結果を scratch/result.json へ書き終えていました。skill が hunter に「結果をファイルへ書いてから返す」ことを求めていたおかげで、この 3 本は救えました。残り 6 本はソースを読んでいる途中で止まり、成果ゼロのまま予算 6 回分を消費しました。

15:00 に制限が解除され、15:55 に再開しました。再開にあたって 2 つ変えています。

  • セッション本体のモデルを Sonnet に切り替え、サブエージェントのモデルは役割ごとに指定した。 hunter の再起動 6 本は、完了済みの 5 本と揃えて Opus(model: "opus" を明示)。critic 1 本と verifier 19 本は Sonnet。Agent ツールはサブエージェントごとにモデルを指定できます
  • hunter は 3 本ずつ 2 回に分けた。 止まっても失う量を減らすためです

予算は 30 から 36 へ(429 で失敗した 6 回分の補填)、さらに 41 へ(候補が 19 件出て verifier の予備枠 14 を超えたため)引き上げました。消費は 41 回ちょうどです。findings.json が揃ったのは 17:09 で、待ち時間を除いた実行時間は 2 時間弱でした。

多くのサブエージェントを同時に走らせる設計の skill を、Max 5x で Opus のまま動かした当社では、最初の wave で止まりました。 並列数を絞るか、探索と検証でモデルを分けるか、あるいはその両方が要ります。

何が見つかったか

区分件数
台帳の単位(面 × 境界 × 攻撃クラス)65
見た(covered31
候補 → 最終検証まで到達19
うち confirmed0
うち needs_validation18
うち rejected1
部分レビュー(blocked4
保留(deferred4
対象外(out_of_scope3
境界を越えない強化メモ63

needs_validation 18 件が指しているのは、運営が負担する外部 API 支出の計上漏れ、緊急停止スイッチの状態がデプロイや操作をまたいで保たれるか、鍵や設定が欠けたときに安全側へ倒れるか、監視の死角、匿名で到達できるエンドポイントの可用性とログの整合性、フィルタとキャッシュパージの精度、といった領域です。いずれも「ソース上はこう読める。実行して確かめていない」という状態で、重要度は付いていません。

verifier が hunter の誤検知を 1 件止めた

rejected の 1 件が、この skill の設計が効いた実例です。

hunter は「匿名で使える有料機能のクォータが IP 単位なので、IP を変えれば回避できる」と主張しました。根拠は、フラッド制御のソースに残っていた古いコメントです。hunter は実際の識別子を生成する関数を「未解決の blocker」として残したまま候補を出していました。

verifier はその関数を読みました。実行不要でリポジトリから直接読める通常のソースです。識別子は IP ではなく、OAuth 後に確定する別の値で、IP を変えても回避できないよう意図して設計されていました。verifier は候補を却下し、findings.json に載る前に止まりました。

人間が hunter の trace(入口 → 伝播 → シンク)を 1 回読んだだけでは、説得力のある書き方に乗ってしまった可能性があります。自前の手順で自己訂正が 18 件起きていたのは、発見者が自分の主張を自分で判定していたからです。

陽性対照が汚染されていた

パイロットには陽性対照を置きました。既に是正済みの欠陥(有料 API の呼び出しが失敗したとき、成功時にしか動かない計上処理のせいで上限や予算が進まない)の是正コミット直前のソースを対象にし、skill がこれを拾えるかを見る設計です。

skill はこの欠陥を needs_validation として拾いました。ただし、実行中に問題が分かりました。プロジェクトの CLAUDE.md(全サブエージェントに自動で注入される指示ファイル)に、この欠陥の是正内容が書いてあったのです。対象のソースは是正前でも、注入される指示は是正後の状態を記述していました。

該当の hunter と verifier は、注入された文言を引用するのではなく、guard()record() の trace、呼び出し元 9 件の全件、プロジェクト自身のテストの列挙漏れから独立に同じ欠陥を再導出していました。それでも「ヒントが無い状態で拾えたか」は採点できません。この 1 件は盲検の陽性対照として無効と記録しました。

過去のコミットを対象にした再現実験でだけ起きる事情ですが、プロジェクト固有の指示ファイルが監査対象の「答え」を含みうることは、同種のパイロットを組む人が先に知っておくべき点です。

confirmed が 0 件なのは設計どおり

README の Requirements には、対象コードを実行するには OS が強制するサンドボックスが要る、と書いてあります。外部ネットワーク無し、空の環境変数、対象は読み取り専用、書き込みは scratch のみ、資源上限あり。この条件が無ければ、skill は対象のコードを一切実行せず、候補を needs_validation に留めます。

当社の dev 環境はこの条件を 1 つも満たしません。コンテナは外部 API へ到達でき、.env に秘密が並び、bind mount で書き込めます。したがって PHPUnit を回す確認は全て needs_validation になり、confirmed はソースの静的読解だけで立つものに限られます。今回はそれが 0 件でした。

confirmed に要求されるのは「完全なソース trace と、束縛された実測結果」です。実行していないのだから 0 件になる。これは skill が正しく動いた結果であり、「confirmed が少ない=安全」と読んではいけないことを、運用で固定する必要があります。

もう 1 つの境界は、SKILL.md の「Respect source visibility」にあります。

Deployment controls, proxy behavior, provider settings, browser headers, identity policy, broker ACLs, packaging, and topology are real controls. If they are required and absent from the repository, do not assume either presence or absence. Use needs_validation with the exact missing fact

配備・プロキシ・プロバイダ設定はリポジトリの外にあるので、存在も不在も仮定しない。稼働環境をプローブすることも禁じられています。自前の手順が見つけた最も重い 2 件(dev と本番の差)は、この skill の枠組みでは構造的に needs_validation で止まります。

どう組み込んだか

置き換えではなく補完にしました。

  • 入口は自前の監査手順のまま。 skill 単体で始めると、到達面の全列挙と稼働環境の否定テストが落ちます
  • skill は「ソースコードの脆弱性」のレンズとして、モジュール単位の scoped run で呼ぶ。 全モジュール一括だと台帳の単位が数百になります
  • skill の needs_validation を、自前の手順の否定テスト段階の入力にする。 今回の 18 件がその第 1 便です
  • skill が構造で持っている 3 機構を自前の手順にも移植した。 中以上の検出は別エージェントに反証させる(環境事実も対象)、未検証には重要度を付けない、未検証の回収は散文の走査語ではなく fingerprint で行う

skill 本体は改変せず、上流の commit を pin して複製しました。上流は 2026-09-10 に全面改修が入っており(初期の約 450 行から約 1,900 行へ)、機構だけ自前に写すより上流に追随する方が保守で有利と判断したためです。

まとめ

  • Cloudflare の security-audit skill の中核は「候補を書いていないエージェントに反証させる」構造です。今回の実行では hunter の誤検知を 1 件、findings.json に載る前に止めました
  • 機械で強制されるのはスキーマとバリデータの範囲だけです。発見者と検証者の分離、サンドボックス無しでの不実行は、親エージェントの手順遵守に依存します
  • OS 強制のサンドボックスが無い環境では confirmed はほぼ出ません。0 件は健全性の証拠ではなく「実行検証をしていない」という意味です
  • 配備・プロキシ・プロバイダ設定はソースの外なので、この skill は構造的に見つけられません。稼働環境を測る手順は別に要ります
  • 並列サブエージェントを多用する設計です。Max 5x で Opus のまま 11 本同時に走らせたところ、12 分で 5 時間分の枠が尽きました。 探索と検証でモデルを分け、並列数を絞ってください
  • 全サブエージェントに注入されるプロジェクトの指示ファイルは、監査対象の「答え」を含みうることがあります。陽性対照を置くなら、先に確かめてください