<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ai-heartland.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ai-heartland.com/" rel="alternate" type="text/html" /><updated>2026-08-19T12:33:58+09:00</updated><id>https://ai-heartland.com/feed.xml</id><title type="html">AI Heartland</title><subtitle>Claude Code・MCP・AIエージェント・RAG・ローカルLLMなどのAI開発OSSと、OSS／開発者向けセキュリティ（脆弱性・CVE解説と自システム確認コマンド）を日本語で解説。コード例・比較表・導入手順をエンジニア向けに毎日更新。2026年版フレームワーク選定ガイドも掲載中。</subtitle><entry><title type="html">MeTube使い方ガイド2026｜yt-dlpをセルフホストのWeb UIで共有する手順と信頼境界</title><link href="https://ai-heartland.com/tool/metube-selfhosted-yt-dlp-web-ui/" rel="alternate" type="text/html" title="MeTube使い方ガイド2026｜yt-dlpをセルフホストのWeb UIで共有する手順と信頼境界" /><published>2026-08-19T12:00:00+09:00</published><updated>2026-08-19T12:00:00+09:00</updated><id>https://ai-heartland.com/tool/metube-selfhosted-yt-dlp-web-ui</id><content type="html" xml:base="https://ai-heartland.com/tool/metube-selfhosted-yt-dlp-web-ui/"><![CDATA[<p><strong>MeTube</strong>は、<a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a>をブラウザから操作できるようにする<strong>セルフホスト型のWeb UI</strong>だ。CLIを触らない家族やチームメンバーとダウンロード環境を共有したいときの最短手段で、GitHubで★14,437を集めている。にもかかわらず日本語の解説はほぼ存在せず、検索しても英語のセルフホスト系ブログとDocker Hubが並ぶだけだ。本記事では<strong>起動手順・yt-dlpオプションの3層合成・そして信頼境界</strong>を、READMEとソースの両方を確認して整理する。</p>

<figure class="video-embed">
  <img loading="lazy" src="/generated_images/metube-selfhosted-yt-dlp-web-ui-demo.gif" width="800" height="475" alt="MeTubeのWeb UIにURLを貼り付けてダウンロードが進行する様子" style="width:100%;height:auto;border-radius:.5rem;" />
  <figcaption>公式リポジトリ同梱のデモ。URLを貼るとキューに入り進捗が出る（出典: <a href="https://github.com/alexta69/metube">alexta69/metube</a>）</figcaption>
</figure>

<blockquote>
  <p>この記事はyt-dlpのセルフホストWeb UI「MeTube」を解説します。自動化ツール全体の地図は<a href="/explain/ai-automation-tools-guide-2026/">AI自動化ツール｜ノーコードからコードまで2026年版の比較と選び方</a>をご覧ください。</p>
</blockquote>

<div class="point-box">

  <p><strong>30秒でわかる MeTube</strong></p>

  <p>・yt-dlpを<strong>Dockerコンテナ1つでWeb UI化</strong>する。CLIを知らない人にも渡せる<br />
・yt-dlpオプションは<strong>グローバル → プリセット → 個別上書き</strong>の3層で合成され、具体的な層が勝つ<br />
・<strong>認証機構は無い</strong>。既定は<code class="language-plaintext highlighter-rouge">HOST=0.0.0.0</code> <code class="language-plaintext highlighter-rouge">PORT=8081</code>なので、外に出すなら手前で必ず絞る<br />
・<code class="language-plaintext highlighter-rouge">ALLOW_YTDL_OPTIONS_OVERRIDES</code>の既定は<code class="language-plaintext highlighter-rouge">false</code>。公式が<strong>コンテナ内での任意コード実行になりうる</strong>と明記している<br />
・ライセンスは<strong>AGPL-3.0</strong>。改造して提供する場合は条項の確認が要る</p>

</div>

<h2 id="metubeとはyt-dlpの共有しづらさを埋める層">MeTubeとは：yt-dlpの「共有しづらさ」を埋める層</h2>

<p>yt-dlpは強力だが、CLIである以上「使える人」が限られる。SSHもターミナルも使わない相手に渡すには、何らかのUIを被せるしかない。MeTubeはそこを埋める。公式の表現は「YouTubeと他の数十のサイトからメディアをダウンロードするための、yt-dlpのセルフホスト型Web UI」だ。</p>

<p>似た立ち位置のツールと比べると、性格の違いがはっきりする。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/metube-selfhosted-yt-dlp-web-ui-stat.webp" width="1280" height="340" alt="MeTubeの素性：GitHub★14.4k、既定ポート8081、AGPL-3.0、最新リリース2026.08.18" style="width:100%;height:auto;" />
  <figcaption>リリースは yt-dlp の更新に追随して頻繁に出ている</figcaption>
</figure>

<table>
  <thead>
    <tr>
      <th>観点</th>
      <th>MeTube</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>形態</td>
      <td>Dockerコンテナ1つのWebアプリ</td>
    </tr>
    <tr>
      <td>中身</td>
      <td>yt-dlp（同梱）＋ Angular製UI ＋ Python製バックエンド</td>
    </tr>
    <tr>
      <td>既定の待受</td>
      <td><code class="language-plaintext highlighter-rouge">HOST=0.0.0.0</code> / <code class="language-plaintext highlighter-rouge">PORT=8081</code></td>
    </tr>
    <tr>
      <td>認証</td>
      <td><strong>無し</strong>（手前のリバースプロキシ等で担保する）</td>
    </tr>
    <tr>
      <td>ライセンス</td>
      <td>AGPL-3.0</td>
    </tr>
    <tr>
      <td>GitHub ★</td>
      <td>14,437（2026-08-19時点）</td>
    </tr>
    <tr>
      <td>最新リリース</td>
      <td>2026.08.18</td>
    </tr>
  </tbody>
</table>

<p>「yt-dlpのラッパー」と一言でまとめると取り違えやすいのは、<strong>MeTubeが独自のダウンロード実装を持たない</strong>点だ。実行しているのはyt-dlpのPython APIであり、MeTubeが提供しているのは<strong>キュー・進捗・保存先・オプション管理</strong>というオーケストレーション層である。だから「yt-dlpでできないことがMeTubeでできる」ことは原則として無いし、逆にyt-dlp側の仕様変更はそのまま効いてくる。</p>

<h2 id="docker一行で起動する">Docker一行で起動する</h2>

<p>導入は非常に短い。公式が示す最短の起動はDockerの1コマンドだ。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ダウンロード先をホストの ./downloads に置いて起動する</span>
docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">-p</span> 8081:8081 <span class="se">\</span>
  <span class="nt">-v</span> /path/to/downloads:/downloads <span class="se">\</span>
  ghcr.io/alexta69/metube
</code></pre></div></div>

<p>継続運用するならCompose化しておく。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># docker-compose.yml</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">metube</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/alexta69/metube</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">metube</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">127.0.0.1:8081:8081"</span>   <span class="c1"># ← まずループバックに閉じる</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./downloads:/downloads</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">UID=1000</span>
      <span class="pi">-</span> <span class="s">GID=1000</span>
      <span class="pi">-</span> <span class="s">OUTPUT_TEMPLATE=%(title)s.%(ext)s</span>
</code></pre></div></div>

<p>ポート公開を<code class="language-plaintext highlighter-rouge">127.0.0.1:8081:8081</code>にしている点が重要だ。既定の<code class="language-plaintext highlighter-rouge">-p 8081:8081</code>は<strong>全インターフェースで待ち受ける</strong>ため、そのままLANや、場合によってはインターネットへ露出する。MeTubeに認証は無いので、最初はループバックに閉じ、必要になってから公開範囲を広げるほうが安全側に倒れる。</p>

<p>主な環境変数の既定値はソース（<code class="language-plaintext highlighter-rouge">app/main.py</code>）で確認できる。</p>

<table>
  <thead>
    <tr>
      <th>環境変数</th>
      <th>既定値</th>
      <th>意味</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DOWNLOAD_DIR</code></td>
      <td><code class="language-plaintext highlighter-rouge">.</code></td>
      <td>保存先ディレクトリ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OUTPUT_TEMPLATE</code></td>
      <td><code class="language-plaintext highlighter-rouge">%(title)s.%(ext)s</code></td>
      <td>ファイル名テンプレート</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OUTPUT_TEMPLATE_PLAYLIST</code></td>
      <td><code class="language-plaintext highlighter-rouge">%(playlist_title)s/%(title)s.%(ext)s</code></td>
      <td>プレイリスト時の階層</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OUTPUT_TEMPLATE_CHANNEL</code></td>
      <td><code class="language-plaintext highlighter-rouge">%(channel)s/%(title)s.%(ext)s</code></td>
      <td>チャンネル単位の階層</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HOST</code></td>
      <td><code class="language-plaintext highlighter-rouge">0.0.0.0</code></td>
      <td>待受アドレス</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PORT</code></td>
      <td><code class="language-plaintext highlighter-rouge">8081</code></td>
      <td>待受ポート</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DEFAULT_THEME</code></td>
      <td><code class="language-plaintext highlighter-rouge">auto</code></td>
      <td>UIテーマ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ALLOW_YTDL_OPTIONS_OVERRIDES</code></td>
      <td><code class="language-plaintext highlighter-rouge">false</code></td>
      <td>個別上書き欄の有効化</td>
    </tr>
  </tbody>
</table>

<p>テンプレート記法はyt-dlpの<code class="language-plaintext highlighter-rouge">-o</code>とまったく同じものだ。プレイリスト用・チャンネル用・チャプター用がそれぞれ独立した変数になっているので、<strong>保存先の階層設計をUIに触れずに決められる</strong>のが実用上ありがたい。</p>

<h2 id="yt-dlpオプションは3層で合成される">yt-dlpオプションは3層で合成される</h2>

<p>MeTubeがただのボタン付きラッパーに留まらないのは、この設計があるからだ。yt-dlpのオプションを<strong>広い順に3層</strong>で与え、衝突したら具体的な層が勝つ。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/metube-ytdlp-option-layers.webp" width="1280" height="320" alt="グローバル、プリセット、個別上書きの3層でyt-dlpオプションが合成される" style="width:100%;height:auto;" />
  <figcaption>同じキーがあれば下の層（より具体的な層）が勝つ</figcaption>
</figure>

<div class="mermaid">
flowchart TB
  A["① グローバル<br />YTDL_OPTIONS / YTDL_OPTIONS_FILE"] --&gt; D["最終的なオプション集合"]
  B["② プリセット<br />YTDL_OPTIONS_PRESETS / _FILE<br />（UIで複数選択可・後勝ち）"] --&gt; D
  C["③ 個別上書き<br />UIの Custom yt-dlp Options<br />（既定では無効）"] --&gt; D
  D --&gt; E["yt-dlp の Python API を実行"]
  F["extract_flat / noplaylist 等は<br />MeTubeが強制（上書き不可）"] --&gt; E
</div>

<p>重要なのは<strong>書式がコマンドラインフラグではない</strong>ことだ。MeTubeはyt-dlpのPython APIオプション名をJSONで受け取る。おおむね「フラグのハイフンをアンダースコアに寄せた名前」になる。</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"writesubtitles"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"subtitleslangs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"ja"</span><span class="p">,</span><span class="w"> </span><span class="s2">"en"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"updatetime"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"writethumbnail"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--write-subs</code>が<code class="language-plaintext highlighter-rouge">"writesubtitles": true</code>になる、という対応だ。ただし<strong>1対1で対応しないフラグがある</strong>点は要注意で、<code class="language-plaintext highlighter-rouge">--embed-thumbnail</code>や<code class="language-plaintext highlighter-rouge">--recode-video</code>は<code class="language-plaintext highlighter-rouge">"postprocessors"</code>の配列として表現しなければならない。yt-dlp側に<code class="language-plaintext highlighter-rouge">devscripts/cli_to_api.py</code>という変換スクリプトが用意されているので、手元のコマンドライン設定を移植するときはそれを通すのが確実だ。</p>

<p>プリセットは「よく使う組合せに名前を付けてUIに出す」機能で、たとえばSponsorBlockでのスポンサー区間除去、字幕の埋め込み、速度制限などを名前つきの束にできる。ファイルで与えた場合は<strong>変更が監視されて自動リロード</strong>されるため、コンテナ再起動なしで設定を回せる。</p>

<div class="box-tip">

  <p><strong>合成規則で押さえる3点</strong></p>

  <p>・同じキーが複数の層にあれば、<strong>個別上書き &gt; プリセット &gt; グローバル</strong>の順で勝つ<br />
・プリセットを複数選んだ場合は<strong>後に適用されたものが勝つ</strong><br />
・値に<code class="language-plaintext highlighter-rouge">null</code>を入れるとそのキーを<strong>打ち消せる</strong>（例: <code class="language-plaintext highlighter-rouge">"download_archive": null</code>でグローバルのアーカイブ設定を無効化）</p>

</div>

<p>なおメタデータ取得フェーズでMeTubeが使う<code class="language-plaintext highlighter-rouge">extract_flat</code>・<code class="language-plaintext highlighter-rouge">noplaylist</code>などのキーは<strong>MeTube側が強制</strong>し、プリセットからは上書きできない。「なぜかプレイリストの扱いが設定どおりにならない」ときは、この強制キーに当たっていないか疑うとよい。</p>

<h2 id="信頼境界どこまでを内側に置くか">信頼境界：どこまでを内側に置くか</h2>

<p>MeTubeを運用するうえで、日本語圏でまったく共有されていないが最も重要なのがここだ。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/metube-override-risk.webp" width="1280" height="430" alt="ALLOW_YTDL_OPTIONS_OVERRIDESを既定のfalseにする場合とtrueにする場合の違い" style="width:100%;height:auto;" />
  <figcaption>公式READMEが自ら注意書きを置いている項目</figcaption>
</figure>

<p><code class="language-plaintext highlighter-rouge">ALLOW_YTDL_OPTIONS_OVERRIDES=true</code>にすると、UIに自由記述のJSON欄（Custom yt-dlp Options）が出る。ここに入力された内容は<strong>最優先で適用される</strong>。公式READMEはこの機能に対して、有効化すると「UIにアクセスできる誰もが任意のyt-dlp APIオプションを与えられるようになり、使うオプション次第ではコンテナ内での任意コード実行を可能にしうる」と明記し、信頼できる環境でのみ有効化するよう求めている。</p>

<p>実際の既定値をソースで確認したところ、<code class="language-plaintext highlighter-rouge">app/main.py</code>の設定ブロックに<code class="language-plaintext highlighter-rouge">'ALLOW_YTDL_OPTIONS_OVERRIDES': 'false'</code>とあり、ブール値として扱われるキーの一覧にも含まれていた。<strong>既定は無効</strong>である。</p>

<p>ここから導かれる運用方針は明快だ。</p>

<p>・<strong>MeTubeに認証は無い</strong>。到達できる＝操作できる、と考える<br />
・したがって<code class="language-plaintext highlighter-rouge">-p 8081:8081</code>のまま外に出さない。リバースプロキシで認証を挟むか、VPN・Tailscale等の内側に置く<br />
・<code class="language-plaintext highlighter-rouge">ALLOW_YTDL_OPTIONS_OVERRIDES</code>は、<strong>UIに触れる人間を全員信頼できる場合のみ</strong><code class="language-plaintext highlighter-rouge">true</code>にする<br />
・家庭内で家族に開放する、社内の限定メンバーに配る、といった用途なら既定のままで十分機能する</p>

<p>MeTubeはHTTPS対応とリバースプロキシ配下での動作を公式にサポートしており、Cookieを使った制限付き動画のダウンロードにも対応している。<strong>Cookieを預けるということは、そのアカウントへのアクセス権をコンテナに渡すことに等しい</strong>ので、認証境界の設計はいっそう重要になる。</p>

<h2 id="どれを選ぶかcliのまま使うかuiを被せるか">どれを選ぶか：CLIのまま使うか、UIを被せるか</h2>

<p>MeTubeは「yt-dlpの上位互換」ではなく、<strong>配布形態の選択肢</strong>だと捉えるのが正しい。</p>

<table>
  <thead>
    <tr>
      <th>やりたいこと</th>
      <th>選ぶもの</th>
      <th>理由</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>自分の手元で細かく制御したい</td>
      <td><a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a></td>
      <td>オプションの表現力とCI組み込みやすさ</td>
    </tr>
    <tr>
      <td>CLIを使わない人にも渡したい</td>
      <td><strong>MeTube</strong></td>
      <td>Docker1つでWeb UI化。認証は手前で足す</td>
    </tr>
    <tr>
      <td>静止画ギャラリーを集めたい</td>
      <td><a href="/tool/gallery-dl-image-gallery-downloader/">gallery-dl</a></td>
      <td>投稿者・タグ単位の集合を展開できる</td>
    </tr>
    <tr>
      <td>進行中のライブ配信を録りたい</td>
      <td><a href="/tool/streamlink-live-stream-recorder/">Streamlink</a></td>
      <td>終端未定のストリームに追従できる</td>
    </tr>
  </tbody>
</table>

<p>MeTubeが担うのはあくまでyt-dlpの守備範囲であり、静止画ギャラリーやライブ配信の追従はカバーしない。<strong>UI化で解けるのは「誰が使えるか」であって「何が取れるか」ではない</strong>——この区別を持っておくと、ツール構成を考えるときに迷いが減る。</p>

<h2 id="まとめ">まとめ</h2>

<p>・MeTubeは<strong>yt-dlpをDockerコンテナ1つでWeb UI化</strong>するセルフホストツール（★14,437 / AGPL-3.0）<br />
・独自のダウンロード実装は持たず、提供するのは<strong>キュー・進捗・保存先・オプション管理</strong>の層<br />
・yt-dlpオプションは<strong>グローバル → プリセット → 個別上書き</strong>の3層合成。書式はフラグではなく<strong>JSONのAPIオプション名</strong><br />
・ファイル指定の設定（<code class="language-plaintext highlighter-rouge">YTDL_OPTIONS_FILE</code>等）は<strong>変更監視つきで自動リロード</strong>される<br />
・<strong>認証機構は無く</strong>、既定は<code class="language-plaintext highlighter-rouge">0.0.0.0:8081</code>。<code class="language-plaintext highlighter-rouge">ALLOW_YTDL_OPTIONS_OVERRIDES</code>は既定<code class="language-plaintext highlighter-rouge">false</code>で、公式が任意コード実行のリスクを明記している</p>

<h2 id="参照ソース">参照ソース</h2>

<ul>
  <li><a href="https://github.com/alexta69/metube">alexta69/metube — GitHub リポジトリ</a>（★14,437 / AGPL-3.0 / 2026-08-19時点）</li>
  <li><a href="https://github.com/alexta69/metube#-configuring-yt-dlp-options">MeTube README — Configuring yt-dlp options / Security note</a></li>
  <li><a href="https://github.com/alexta69/metube/blob/master/app/main.py">alexta69/metube — app/main.py（環境変数の既定値）</a></li>
  <li><a href="https://github.com/alexta69/metube/releases/tag/2026.08.18">MeTube リリース 2026.08.18</a></li>
</ul>

<!--
Distribution memo (2026-08-19)
- X: 「MeTubeのALLOW_YTDL_OPTIONS_OVERRIDES、既定falseなのは理由がある」＋信頼境界図。08時台JST枠
- セルフリプ: docker-compose を 127.0.0.1 バインドで書く例を貼る
- 内部リンク: gallery-dl記事・Streamlink記事と相互リンク済み
-->]]></content><author><name></name></author><category term="tool" /><category term="metube" /><category term="automation" /><category term="oss" /><category term="docker" /><category term="selfhosted" /><category term="web-ui" /><category term="downloader" /><summary type="html"><![CDATA[MeTubeはyt-dlpをブラウザから使えるようにするセルフホスト型Web UI。Docker一行での起動、yt-dlpオプションが3層（グローバル・プリセット・個別上書き）で合成される仕組み、そして既定でfalseのALLOW_YTDL_OPTIONS_OVERRIDESが持つ信頼境界までソース確認つきで解説する。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_metube-selfhosted-yt-dlp-web-ui.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_metube-selfhosted-yt-dlp-web-ui.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Streamlink使い方ガイド2026｜135プラグインでライブ配信を録画するCLIをyt-dlpと使い分ける</title><link href="https://ai-heartland.com/tool/streamlink-live-stream-recorder/" rel="alternate" type="text/html" title="Streamlink使い方ガイド2026｜135プラグインでライブ配信を録画するCLIをyt-dlpと使い分ける" /><published>2026-08-19T11:00:00+09:00</published><updated>2026-08-19T11:00:00+09:00</updated><id>https://ai-heartland.com/tool/streamlink-live-stream-recorder</id><content type="html" xml:base="https://ai-heartland.com/tool/streamlink-live-stream-recorder/"><![CDATA[<p><strong>Streamlink</strong>は、ライブ配信のページURLからストリームを解決し、プレイヤーへ流す・ファイルへ録画するためのPython製CLIだ。動画ファイル取得の定番である<a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a>と混同されやすいが、<strong>扱う時間軸が違う</strong>。本記事はv8.5.0を実際にインストールし、<strong>同梱プラグイン数・日本のサービスの対応状況・<code class="language-plaintext highlighter-rouge">--json</code>の実出力</strong>を確認したうえで書いている。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/streamlink-live-stream-recorder-fv.webp" width="1280" height="340" alt="Streamlink v8.5.0 の実測値：同梱プラグイン135件、日本のサービス7件、BSD-2-Clause、GitHub★11.7k" style="width:100%;height:auto;" />
  <figcaption>v8.5.0 を実際にインストールして <code>--plugins</code> を数えた結果</figcaption>
</figure>

<blockquote>
  <p>この記事はライブ配信録画CLI「Streamlink」を解説します。自動化ツール全体の地図は<a href="/explain/ai-automation-tools-guide-2026/">AI自動化ツール｜ノーコードからコードまで2026年版の比較と選び方</a>をご覧ください。</p>
</blockquote>

<div class="point-box">

  <p><strong>30秒でわかる Streamlink</strong></p>

  <p>・<strong>進行中のライブ配信</strong>が対象。完成済みの動画ファイルは<a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a>の担当<br />
・実測で<strong>同梱プラグイン135件</strong>（v8.5.0）。うち<strong>日本のサービスが7件</strong>（ABEMA・ニコ生・OPENREC・radiko・ツイキャス・SHOWROOM・PIA ULIZA）<br />
・本質は「ダウンローダー」ではなく<strong>ストリーム解決器</strong>。出力先をプレイヤー・<code class="language-plaintext highlighter-rouge">--record</code>・<code class="language-plaintext highlighter-rouge">--stdout</code>から選ぶ<br />
・<code class="language-plaintext highlighter-rouge">--retry-streams</code>で<strong>配信開始を待てる</strong>。cronと組み合わせれば自動録画になる<br />
・<code class="language-plaintext highlighter-rouge">--json</code>で plugin・metadata・streams が機械可読に返るので、スクリプトから扱いやすい</p>

</div>

<div class="box-warning">

  <p><strong>法令遵守の前提</strong>：本記事は技術解説です。ライブ配信の録画可否は各サービスの利用規約と著作権法に従います。日本では2021年施行の改正著作権法により、違法配信と知りながら著作物をダウンロードする行為は刑事罰の対象です。私的複製の範囲を超える保存・再配布は行わないでください。</p>

</div>

<div class="box-tip">

  <p><strong>この記事のポイント</strong></p>

  <ul>
    <li>実測（v8.5.0）で<strong>同梱プラグインは135件</strong>、うち<strong>日本のサービスが7件</strong>。追加インストールは不要</li>
    <li>yt-dlpとの違いは対応数ではなく<strong>終端が確定しているかどうか</strong>。ライブは終わるまで走り続ける</li>
    <li><code class="language-plaintext highlighter-rouge">--retry-streams</code>で<strong>配信開始を待って録画</strong>でき、<code class="language-plaintext highlighter-rouge">--json</code>で解決結果を機械可読に取り出せる</li>
  </ul>

</div>

<h2 id="streamlinkとは録画とダウンロードは時間軸が違う">Streamlinkとは：「録画」と「ダウンロード」は時間軸が違う</h2>

<p>Streamlinkの公式説明は「様々な配信サービスからビデオ・オーディオをメディアプレーヤーへパイプするCLIユーティリティ」だ。<strong>「ダウンローダー」と名乗っていない</strong>ところに設計思想が出ている。</p>

<p>yt-dlpとの違いを「対応サイトの数」で比べても本質に届かない。決定的なのは、扱う対象の<strong>終端が確定しているかどうか</strong>である。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/streamlink-vs-yt-dlp-timeaxis.webp" width="1280" height="430" alt="yt-dlpは終わったファイルを取る、Streamlinkは進行中の配信に追従する" style="width:100%;height:auto;" />
  <figcaption>進捗率が出せるかどうかは、この違いの分かりやすい副作用</figcaption>
</figure>

<table>
  <thead>
    <tr>
      <th>観点</th>
      <th>Streamlink</th>
      <th>yt-dlp</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>主対象</td>
      <td>進行中のライブ配信</td>
      <td>完成済みの動画・音声</td>
    </tr>
    <tr>
      <td>終端</td>
      <td><strong>未定</strong>（配信が終わるまで走る）</td>
      <td>確定（尺が既知）</td>
    </tr>
    <tr>
      <td>進捗表示</td>
      <td>経過時間・取得バイト数</td>
      <td>パーセンテージ</td>
    </tr>
    <tr>
      <td>標準の出力</td>
      <td>プレイヤーへパイプ／<code class="language-plaintext highlighter-rouge">.ts</code>へ録画</td>
      <td>ファイル保存</td>
    </tr>
    <tr>
      <td>開始待ち</td>
      <td><code class="language-plaintext highlighter-rouge">--retry-streams</code>で待てる</td>
      <td>待つ機構は持たない</td>
    </tr>
    <tr>
      <td>実測の対応規模</td>
      <td>135プラグイン（v8.5.0）</td>
      <td>1,400+サイト（公式表記）</td>
    </tr>
    <tr>
      <td>ライセンス</td>
      <td>BSD-2-Clause</td>
      <td>Unlicense</td>
    </tr>
  </tbody>
</table>

<p>「対応数はyt-dlpのほうが1桁多いのだから、Streamlinkは要らないのでは」と考えたくなるが、<strong>この2つの数字は同じものを数えていない</strong>。yt-dlpの1,400+はアーカイブ済みコンテンツを含む総サイト数で、Streamlinkの135は「ライブ配信を解決できるプラグイン」の数だ。ライブに限れば後者のほうが手厚い領域が多い。</p>

<h2 id="同梱プラグインを実測で数える日本のサービスは7件">同梱プラグインを実測で数える——日本のサービスは7件</h2>

<p><code class="language-plaintext highlighter-rouge">--plugins</code>でプラグイン一覧が出る。カンマ区切りの1行なので、そのまま数えると1件になってしまう。分解して数える。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 同梱プラグインの件数を数える</span>
streamlink <span class="nt">--plugins</span> <span class="se">\</span>
  | <span class="nb">sed</span> <span class="s1">'s/^Available plugins: //'</span> <span class="se">\</span>
  | <span class="nb">tr</span> <span class="s1">','</span> <span class="s1">'\n'</span> | <span class="nb">sed</span> <span class="s1">'s/ //g'</span> | <span class="nb">grep</span> <span class="nt">-c</span> <span class="nb">.</span>
</code></pre></div></div>

<p>v8.5.0での結果は<strong>135</strong>だった。このうち日本のサービスに対応するものを抜き出すと7件になる。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/streamlink-jp-plugins.webp" width="1280" height="330" alt="同梱プラグインに含まれる日本のサービス7件：abematv、nicolive、openrectv、radiko、twitcasting、showroom、piaulizaportal" style="width:100%;height:auto;" />
  <figcaption>追加インストール不要。標準で同梱されている</figcaption>
</figure>

<table>
  <thead>
    <tr>
      <th>プラグイン名</th>
      <th>サービス</th>
      <th>種別</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">abematv</code></td>
      <td>ABEMA</td>
      <td>動画配信</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nicolive</code></td>
      <td>ニコニコ生放送</td>
      <td>ライブ配信</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">openrectv</code></td>
      <td>OPENREC.tv</td>
      <td>ゲーム配信</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">radiko</code></td>
      <td>radiko</td>
      <td>ラジオ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">twitcasting</code></td>
      <td>ツイキャス</td>
      <td>ライブ配信</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">showroom</code></td>
      <td>SHOWROOM</td>
      <td>ライブ配信</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">piaulizaportal</code></td>
      <td>PIA ULIZA</td>
      <td>配信基盤</td>
    </tr>
  </tbody>
</table>

<p>海外向けでは<code class="language-plaintext highlighter-rouge">twitch</code>・<code class="language-plaintext highlighter-rouge">youtube</code>・<code class="language-plaintext highlighter-rouge">kick</code>・<code class="language-plaintext highlighter-rouge">chzzk</code>（韓国）・<code class="language-plaintext highlighter-rouge">bilibili</code>・<code class="language-plaintext highlighter-rouge">douyin</code>・<code class="language-plaintext highlighter-rouge">soop</code>などが並ぶ。汎用の<code class="language-plaintext highlighter-rouge">hls</code>・<code class="language-plaintext highlighter-rouge">dash</code>・<code class="language-plaintext highlighter-rouge">http</code>プラグインもあり、<strong>プラグインが無いサイトでもストリームURLを直接渡せば処理できる</strong>。この汎用プラグインの存在が、135という数字以上の守備範囲を生んでいる。</p>

<h2 id="インストールと基本の使い方">インストールと基本の使い方</h2>

<p>Pythonのパッケージとして配布されている。pipで入るのが一番手軽だ。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># インストールとバージョン確認</span>
python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">-U</span> streamlink
streamlink <span class="nt">--version</span>

<span class="c"># 利用可能な画質を一覧する（ダウンロードはしない）</span>
streamlink <span class="s2">"https://www.youtube.com/watch?v=XXXXXXXXXXX"</span>

<span class="c"># 画質を指定してプレイヤーで再生する</span>
streamlink <span class="s2">"配信URL"</span> best

<span class="c"># ファイルへ録画する</span>
streamlink <span class="nt">--record</span> <span class="s2">"output.ts"</span> <span class="s2">"配信URL"</span> best
</code></pre></div></div>

<p>引数を1つだけ渡すと<strong>画質の一覧が表示されて終わる</strong>。これがStreamlinkの標準的な使い方で、<code class="language-plaintext highlighter-rouge">best</code> / <code class="language-plaintext highlighter-rouge">worst</code> / <code class="language-plaintext highlighter-rouge">1080p</code> / <code class="language-plaintext highlighter-rouge">720p</code>のような画質名を第2引数で指定して初めて処理が走る。yt-dlpの<code class="language-plaintext highlighter-rouge">-f</code>に相当するが、こちらは位置引数である点が違う。</p>

<p>処理の流れを図にすると次のようになる。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/streamlink-pipeline.webp" width="1280" height="320" alt="URLを渡す→画質を列挙→出力先を選ぶという3段の流れ" style="width:100%;height:auto;" />
  <figcaption>Streamlinkは解決してパイプに流すところまでを担当する</figcaption>
</figure>

<div class="mermaid">
flowchart LR
  A["配信ページURL"] --&gt; B["135プラグインから<br />自動マッチ"]
  B --&gt; C["ストリームを列挙<br />best / 1080p / worst …"]
  C --&gt; D{"出力先"}
  D --&gt; E["プレイヤーへパイプ<br />（既定）"]
  D --&gt; F["--record file.ts<br />録画"]
  D --&gt; G["--stdout<br />ffmpeg等へパイプ"]
  C --&gt; H["--json<br />plugin/metadata/streams"]
</div>

<h2 id="--jsonで機械可読に扱う"><code class="language-plaintext highlighter-rouge">--json</code>で機械可読に扱う</h2>

<p>Streamlinkがスクリプトに組み込みやすいのは、<code class="language-plaintext highlighter-rouge">--json</code>が用意されているからだ。実際にYouTubeの公開動画へ投げた出力（URLは長大なので省略）は次の構造だった。</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"plugin"</span><span class="p">:</span><span class="w"> </span><span class="s2">"youtube"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jNQXAC9IVRw"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jawed"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Film &amp; Animation"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Me at the zoo"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"streams"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"240p"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http"</span><span class="p">,</span><span class="w"> </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GET"</span><span class="p">,</span><span class="w"> </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"User-Agent"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"worst"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http"</span><span class="p">,</span><span class="w"> </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GET"</span><span class="p">,</span><span class="w"> </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"User-Agent"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>得られる情報は3層に分かれている。</p>

<p>・<code class="language-plaintext highlighter-rouge">plugin</code> — どのプラグインがマッチしたか。対応外URLの判定に使える<br />
・<code class="language-plaintext highlighter-rouge">metadata</code> — <code class="language-plaintext highlighter-rouge">id</code> / <code class="language-plaintext highlighter-rouge">author</code> / <code class="language-plaintext highlighter-rouge">category</code> / <code class="language-plaintext highlighter-rouge">title</code>。<strong>録画ファイル名の材料</strong>になる<br />
・<code class="language-plaintext highlighter-rouge">streams</code> — 画質ごとの<code class="language-plaintext highlighter-rouge">type</code> / <code class="language-plaintext highlighter-rouge">url</code> / <code class="language-plaintext highlighter-rouge">headers</code>。実体のストリームURLとリクエストヘッダー</p>

<div class="box-warning">

  <p><strong><code class="language-plaintext highlighter-rouge">--json</code>の出力を安易に貼らない</strong>：<code class="language-plaintext highlighter-rouge">streams[].url</code>には配信基盤が発行した署名つきURLが入り、<strong>アクセス元のIPアドレスがクエリパラメータに含まれることがある</strong>。実際に手元で試した際も、YouTubeの<code class="language-plaintext highlighter-rouge">videoplayback</code> URLに接続元IPv6アドレスが埋め込まれていた。issueやブログに貼るときは必ず削るか、<code class="language-plaintext highlighter-rouge">plugin</code>と<code class="language-plaintext highlighter-rouge">metadata</code>だけを引用する。</p>

</div>

<h2 id="自動録画--retry-streamsと出力テンプレート">自動録画：<code class="language-plaintext highlighter-rouge">--retry-streams</code>と出力テンプレート</h2>

<p>「配信が始まったら録る」を実現するのがStreamlinkの本領だ。素朴にcronで叩くと、配信前はエラー終了してしまう。<code class="language-plaintext highlighter-rouge">--retry-streams</code>を使う。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 30秒ごとに再試行して配信開始を待ち、始まったら録画する</span>
streamlink <span class="se">\</span>
  <span class="nt">--retry-streams</span> 30 <span class="se">\</span>
  <span class="nt">--retry-max</span> 120 <span class="se">\</span>
  <span class="nt">--record</span> <span class="s2">"~/rec/{author}/{id}-{time:%Y%m%d%H%M%S}.ts"</span> <span class="se">\</span>
  <span class="s2">"配信URL"</span> best
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--record</code>はテンプレート変数を受け付ける。公式ヘルプが例示している形式が<code class="language-plaintext highlighter-rouge">{author}</code> / <code class="language-plaintext highlighter-rouge">{category}</code> / <code class="language-plaintext highlighter-rouge">{id}</code> / <code class="language-plaintext highlighter-rouge">{time:%Y%m%d%H%M%S}</code>で、これは<code class="language-plaintext highlighter-rouge">--json</code>の<code class="language-plaintext highlighter-rouge">metadata</code>と対応している。<strong>取得したメタデータがそのままファイル名になる</strong>ので、後から探しやすい階層を最初に設計しておくとよい。</p>

<p>覚えておきたいオプションを整理する。</p>

<table>
  <thead>
    <tr>
      <th>オプション</th>
      <th>効果</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--retry-streams DELAY</code></td>
      <td>DELAY秒ごとに再試行して配信開始を待つ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--retry-max N</code></td>
      <td>再試行の上限回数。<code class="language-plaintext highlighter-rouge">0</code>だと無制限</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--record FILE</code></td>
      <td>再生しつつファイルへ録画する</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--stdout</code></td>
      <td>標準出力へ流す。ffmpegへパイプする用途</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--force</code></td>
      <td>既存ファイルがあっても上書きする</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--json</code></td>
      <td>解決結果をJSONで返す（録画はしない）</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">--record-and-pipe</code>は非推奨になっており、公式ヘルプが<code class="language-plaintext highlighter-rouge">--stdout --record=FILENAME</code>への置き換えを案内している。古い記事のコマンドをそのまま使うと将来動かなくなるので、ここは新しい書き方に寄せておきたい。</p>

<p>なお<code class="language-plaintext highlighter-rouge">.ts</code>のまま溜め続けるとファイルサイズが大きくなりやすい。配布や編集に回すなら、録画後にコンテナだけ差し替えるのが定石だ。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 再エンコードせずコンテナだけmp4へ変換する</span>
ffmpeg <span class="nt">-i</span> input.ts <span class="nt">-c</span> copy output.mp4
</code></pre></div></div>

<h2 id="どれを選ぶか静止画動画ライブ配信の分担">どれを選ぶか：静止画・動画・ライブ配信の分担</h2>

<p>ダウンローダー系ツールは「どれが最強か」ではなく、<strong>対象の性質で分担する</strong>のが正しい。</p>

<table>
  <thead>
    <tr>
      <th>対象</th>
      <th>使うツール</th>
      <th>理由</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>進行中のライブ配信</td>
      <td><strong>Streamlink</strong></td>
      <td>終端未定のストリームに追従・開始待ちができる</td>
    </tr>
    <tr>
      <td>完成済みの動画・音声</td>
      <td><a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a></td>
      <td>フォーマット選択・字幕・後処理が充実</td>
    </tr>
    <tr>
      <td>静止画ギャラリー</td>
      <td><a href="/tool/gallery-dl-image-gallery-downloader/">gallery-dl</a></td>
      <td>投稿者・タグ単位の集合を展開できる</td>
    </tr>
    <tr>
      <td>チームで共有したい</td>
      <td><a href="/tool/metube-selfhosted-yt-dlp-web-ui/">MeTube</a></td>
      <td>yt-dlpをWeb UI化してセルフホストできる</td>
    </tr>
  </tbody>
</table>

<p>補足として、<strong>アーカイブ済みの配信はyt-dlpのほうが素直</strong>なことが多い。Streamlinkが必要なのは「いま流れているもの」を掴む場面であり、終わったあとに取りに行くならyt-dlpで足りる。この線引きを持っておくと、ツール選定で迷わなくなる。</p>

<h2 id="まとめ">まとめ</h2>

<p>・Streamlinkは<strong>ストリーム解決器</strong>であって単純なダウンローダーではない。出力先をプレイヤー・録画・stdoutから選ぶ<br />
・v8.5.0の実測で<strong>同梱プラグイン135件</strong>、うち<strong>日本のサービス7件</strong>（ABEMA・ニコ生・OPENREC・radiko・ツイキャス・SHOWROOM・PIA ULIZA）<br />
・汎用の<code class="language-plaintext highlighter-rouge">hls</code> / <code class="language-plaintext highlighter-rouge">dash</code> / <code class="language-plaintext highlighter-rouge">http</code>プラグインがあるため、専用プラグインが無いサイトも扱える余地がある<br />
・<code class="language-plaintext highlighter-rouge">--retry-streams</code>＋<code class="language-plaintext highlighter-rouge">--record</code>のテンプレートで<strong>自動録画</strong>が組める。<code class="language-plaintext highlighter-rouge">--record-and-pipe</code>は非推奨<br />
・<code class="language-plaintext highlighter-rouge">--json</code>は機械可読で便利だが、<strong>署名つきURLに接続元IPが混ざる</strong>ことがあるので公開時は削る</p>

<h2 id="参照ソース">参照ソース</h2>

<ul>
  <li><a href="https://github.com/streamlink/streamlink">streamlink/streamlink — GitHub リポジトリ</a>（★11,696 / BSD-2-Clause / 2026-08-19時点）</li>
  <li><a href="https://streamlink.github.io/">Streamlink 公式ドキュメント</a></li>
  <li><a href="https://github.com/streamlink/streamlink/releases/tag/8.5.0">Streamlink 8.5.0 リリース</a>（2026-08-01公開）</li>
  <li>実測環境：streamlink 8.5.0 / Python 3.14.4 / macOS 14.5 arm64（<code class="language-plaintext highlighter-rouge">--plugins</code>・<code class="language-plaintext highlighter-rouge">--json</code>・<code class="language-plaintext highlighter-rouge">--help</code>の出力）</li>
</ul>

<!--
Distribution memo (2026-08-19)
- X: 「Streamlinkの同梱プラグイン135件を数えたら、日本のサービスが7つ標準で入ってた」＋日本サービス図。23時台JST枠
- セルフリプ: --retry-streams で配信開始を待つワンライナーを貼る
- 内部リンク: gallery-dl記事・MeTube記事と相互リンク済み
-->]]></content><author><name></name></author><category term="tool" /><category term="streamlink" /><category term="automation" /><category term="oss" /><category term="python" /><category term="cli" /><category term="live-streaming" /><category term="downloader" /><summary type="html"><![CDATA[Streamlinkのインストールから--record・--retry-streams・--jsonまで実測ベースで解説。v8.5.0を実際に入れて数えた同梱プラグインは135件で、ABEMA・ニコ生・radiko・ツイキャス等の日本のサービスが7件含まれる。yt-dlpとの使い分けを時間軸の違いから整理する。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_streamlink-live-stream-recorder.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_streamlink-live-stream-recorder.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">gallery-dl使い方完全ガイド2026｜269サイト対応の画像一括ダウンロードをyt-dlpと使い分ける</title><link href="https://ai-heartland.com/tool/gallery-dl-image-gallery-downloader/" rel="alternate" type="text/html" title="gallery-dl使い方完全ガイド2026｜269サイト対応の画像一括ダウンロードをyt-dlpと使い分ける" /><published>2026-08-19T10:00:00+09:00</published><updated>2026-08-19T10:00:00+09:00</updated><id>https://ai-heartland.com/tool/gallery-dl-image-gallery-downloader</id><content type="html" xml:base="https://ai-heartland.com/tool/gallery-dl-image-gallery-downloader/"><![CDATA[<p><strong>gallery-dl</strong>は、画像ギャラリー投稿サイトから作品を一括ダウンロードするPython製のコマンドラインツールだ。動画・音声の<a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a>に対して、こちらは<strong>静止画側の事実上の標準</strong>にあたる。本記事はv1.32.9を実際に手元へインストールし、<strong>対応サイト数・設定ファイルの探索順・pixivの認証エラー</strong>をすべて実測してから書いている。加えて、2026年4月に<strong>開発の本体がGitHubからCodebergへ移った</strong>という、日本語ではほとんど共有されていない変更も扱う。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/gallery-dl-image-gallery-downloader-fv.webp" width="1280" height="340" alt="gallery-dl v1.32.9 の実測値：対応サイト269件、extractor定義1,015件、pixivサブカテゴリ18件、ライセンスGPL-2.0" style="width:100%;height:auto;" />
  <figcaption>v1.32.9 を実際にインストールして <code>--list-extractors</code> を数えた結果。公式READMEは件数を書いていない</figcaption>
</figure>

<blockquote>
  <p>この記事は画像ギャラリー取得のCLI「gallery-dl」を解説します。自動化ツール全体の地図は<a href="/explain/ai-automation-tools-guide-2026/">AI自動化ツール｜ノーコードからコードまで2026年版の比較と選び方</a>をご覧ください。</p>
</blockquote>

<div class="point-box">

  <p><strong>30秒でわかる gallery-dl</strong></p>

  <p>・<strong>静止画のギャラリー一括取得</strong>が本業。動画・音声は<a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a>の担当で、守備範囲が重ならない<br />
・実測で<strong>対応サイト269件・extractor定義1,015件</strong>（v1.32.9）。pixivだけでサブカテゴリが18ある<br />
・<strong>開発の本体は2026年4月5日にCodebergへ移動</strong>。GitHubはCI/CD・nightly・Dockerイメージ用として残っている<br />
・設定ファイルはJSON。探索先は実測で3か所、<code class="language-plaintext highlighter-rouge">-v</code>を付ければどれを読んだか必ず出る<br />
・pixiv・fanbox等のログイン必須サイトは<strong>トークンが無いと必ず認証エラーで止まる</strong>。URLの問題ではない</p>

</div>

<div class="box-warning">

  <p><strong>法令遵守の前提</strong>：本記事は技術解説です。ダウンロードの可否は各サイトの利用規約と著作権法に従います。日本では2021年施行の改正著作権法により、違法配信と知りながら著作物をダウンロードする行為は刑事罰の対象です。取得したデータの再配布や学習データ化にも別途の権利処理が必要になる場合があります。</p>

</div>

<h2 id="gallery-dlとはyt-dlpと守備範囲が重ならない理由">gallery-dlとは：yt-dlpと守備範囲が重ならない理由</h2>

<p>gallery-dlは「画像ホスティングサイトから画像ギャラリーとコレクションをダウンロードするコマンドラインプログラム」と公式に説明されている。ポイントは<strong>ギャラリー単位</strong>で扱うことだ。1枚のURLを渡して1枚落とすのではなく、「この投稿者の全作品」「このタグの検索結果」といった<strong>集合</strong>を展開してまとめて取得する。</p>

<p>yt-dlpとの違いは、しばしば「動画か画像か」だけで語られるが、実務上はもう一段深い。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/gallery-dl-vs-yt-dlp-scope.webp" width="1280" height="430" alt="yt-dlpが得意な領域とgallery-dlが得意な領域の比較" style="width:100%;height:auto;" />
  <figcaption>「動画か画像か」より、扱う単位が「1ファイル」か「集合」かの違いが大きい</figcaption>
</figure>

<table>
  <thead>
    <tr>
      <th>観点</th>
      <th>gallery-dl</th>
      <th>yt-dlp</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>主対象</td>
      <td>静止画ギャラリー・コレクション</td>
      <td>動画・音声ファイル</td>
    </tr>
    <tr>
      <td>扱う単位</td>
      <td>投稿者／タグ／検索結果などの<strong>集合</strong></td>
      <td>個別URL（プレイリストも可）</td>
    </tr>
    <tr>
      <td>実測の対応規模</td>
      <td>269サイト／1,015 extractor（v1.32.9）</td>
      <td>1,400+サイト（公式表記）</td>
    </tr>
    <tr>
      <td>設定</td>
      <td>JSON設定ファイルが中心</td>
      <td>コマンドラインオプションが中心</td>
    </tr>
    <tr>
      <td>ライセンス</td>
      <td>GPL-2.0</td>
      <td>Unlicense</td>
    </tr>
    <tr>
      <td>相互関係</td>
      <td>HLS/DASHに当たると<strong>yt-dlpを呼ぶ</strong></td>
      <td>gallery-dlを呼ぶ機構は持たない</td>
    </tr>
  </tbody>
</table>

<p>最後の行が重要だ。gallery-dlは<code class="language-plaintext highlighter-rouge">ytdl</code>という名前のダウンローダーバックエンドを内蔵しており、その実装を読むと<strong>yt-dlpを優先し、無ければyoutube-dlにフォールバックする</strong>ようになっている。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># gallery_dl/ytdl.py — import_module（実際のソース）
</span><span class="k">def</span> <span class="nf">import_module</span><span class="p">(</span><span class="n">module_name</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">module_name</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">__import__</span><span class="p">(</span><span class="sh">"</span><span class="s">yt_dlp</span><span class="sh">"</span><span class="p">)</span>
        <span class="nf">except </span><span class="p">(</span><span class="nb">ImportError</span><span class="p">,</span> <span class="nb">SyntaxError</span><span class="p">):</span>
            <span class="k">return</span> <span class="nf">__import__</span><span class="p">(</span><span class="sh">"</span><span class="s">youtube_dl</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">util</span><span class="p">.</span><span class="nf">import_file</span><span class="p">(</span><span class="n">module_name</span><span class="p">)</span>
</code></pre></div></div>

<p>つまり「gallery-dlか、yt-dlpか」という二者択一ではなく、<strong>画像は自前で処理し、動画に当たったらyt-dlpへ委譲する</strong>という設計だ。両方入れておくのが正しい構成になる。手元の環境では、内蔵ダウンローダーは<code class="language-plaintext highlighter-rouge">common</code> / <code class="language-plaintext highlighter-rouge">http</code> / <code class="language-plaintext highlighter-rouge">text</code> / <code class="language-plaintext highlighter-rouge">ytdl</code>の4つが確認できた。</p>

<h2 id="2026年4月開発の本体がcodebergへ移った">2026年4月、開発の本体がCodebergへ移った</h2>

<p>日本語の解説記事でまったく触れられていない変更がある。<strong>gallery-dlの開発は2026年4月5日にCodebergへ移った</strong>。READMEの冒頭にも「Active development has moved to Codeberg」と明記されている。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/gallery-dl-codeberg-migration.webp" width="1280" height="430" alt="GitHubはCI/CD用として残り、コード本体はCodebergへ移った" style="width:100%;height:auto;" />
  <figcaption>GitHubリポジトリはアーカイブされていない。役割が変わった</figcaption>
</figure>

<p>アナウンス（GitHub issue #9374）の内容を整理すると、次のようになる。</p>

<p>・アナウンス本文は移行の理由を、<strong>DMCAテイクダウン通知</strong>と、GitHubに残るために求められた<strong>コミット履歴の書き換え・コード削除を受けて</strong>（in light of）と説明している<br />
・GitHubリポジトリは残るが、用途は<strong>CI/CD・自動化ワークフロー（テスト、nightlyバイナリ、Dockerイメージ）</strong>に限定される<br />
・公式の表現では「gallery-dlの実際のコードはもう入らない」<br />
・GitHubは<strong>アーカイブも凍結もされていない</strong>。★19,252・issueも開いたまま</p>

<p>実際にGitHub側の直近コミットを確認すると、<code class="language-plaintext highlighter-rouge">release version 1.32.9</code>のようなリリース同期のコミットだけが並んでいた。「更新が止まったリポジトリ」ではなく「役割が変わったリポジトリ」だと理解するのが正確だ。</p>

<p>実務上、これは次の3点に効いてくる。</p>

<div class="box-tip">

  <p><strong>Codeberg移行で実際に変わること</strong></p>

  <p>・<strong>スタンドアロン実行ファイルの配布元がcodeberg.orgになった</strong>。READMEのWindows用<code class="language-plaintext highlighter-rouge">gallery-dl.exe</code>・Linux用<code class="language-plaintext highlighter-rouge">gallery-dl.bin</code>のリンク先は<code class="language-plaintext highlighter-rouge">codeberg.org/mikf/gallery-dl/releases/...</code>を指している<br />
・<strong>開発版のインストール元URLも変わった</strong>。masterのtarballは<code class="language-plaintext highlighter-rouge">https://codeberg.org/mikf/gallery-dl/archive/master.tar.gz</code><br />
・一方で<strong>nightlyビルドはGitHub側に残っている</strong>（<code class="language-plaintext highlighter-rouge">github.com/gdl-org/builds/releases</code>）。「全部Codebergに移った」と単純化すると取り違える</p>

</div>

<p>なお<code class="language-plaintext highlighter-rouge">pip install gallery-dl</code>の参照先はPyPIなので<strong>変わらない</strong>。安定版をpipで入れている限り、この移行を意識する必要はない。影響を受けるのは、issueを追う人・ソースを読む人・実行ファイルを直接落としている人だ。</p>

<h2 id="インストールと最初の1コマンド">インストールと最初の1コマンド</h2>

<p>安定版はPyPIで配布されている。Python 3.8以上とRequestsがあれば動く。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 安定版をインストール（PyPI経由。Codeberg移行の影響を受けない）</span>
python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">-U</span> gallery-dl

<span class="c"># バージョンと動作確認</span>
gallery-dl <span class="nt">--version</span>

<span class="c"># 何もダウンロードせず、対象URLの中身だけ見る</span>
gallery-dl <span class="nt">--simulate</span> <span class="s2">"https://example.com/gallery/xxxx"</span>
</code></pre></div></div>

<p>Windowsでは<code class="language-plaintext highlighter-rouge">python</code>ではなく<code class="language-plaintext highlighter-rouge">py</code>を使う。pip以外にも、Homebrew（<code class="language-plaintext highlighter-rouge">brew install gallery-dl</code>）、Scoop、Chocolatey、MacPorts、Snap、Dockerイメージ（<code class="language-plaintext highlighter-rouge">ghcr.io/mikf/gallery-dl</code>）が用意されている。手元のmacOSでは<code class="language-plaintext highlighter-rouge">pip install</code>後に<code class="language-plaintext highlighter-rouge">gallery-dl --version</code>が<code class="language-plaintext highlighter-rouge">1.32.9</code>を返した。</p>

<p><code class="language-plaintext highlighter-rouge">--simulate</code>は最初に必ず覚えておきたい。<strong>ファイルを書かずにURLの解決結果だけを確認する</strong>モードで、対応していないURLを渡していないか、認証が必要かを消費なしで判定できる。</p>

<h2 id="対応サイトを実測で数える">対応サイトを実測で数える</h2>

<p>公式READMEは「several image hosting sites（いくつかの画像ホスティングサイト）」としか書いておらず、具体的な件数を示していない。そこで実際に数えた。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 対応サイト（Category）のユニーク件数を数える</span>
gallery-dl <span class="nt">--list-extractors</span> <span class="se">\</span>
  | <span class="nb">grep</span> <span class="s1">'^Category: '</span> <span class="se">\</span>
  | <span class="nb">sed</span> <span class="s1">'s/^Category: //; s/ - Subcategory.*//'</span> <span class="se">\</span>
  | <span class="nb">sort</span> <span class="nt">-u</span> | <span class="nb">wc</span> <span class="nt">-l</span>
</code></pre></div></div>

<p>v1.32.9での結果は次のとおり。</p>

<table>
  <thead>
    <tr>
      <th>数え方</th>
      <th style="text-align: right">件数</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ユニークなCategory（＝サイト単位）</td>
      <td style="text-align: right"><strong>269</strong></td>
    </tr>
    <tr>
      <td>extractor定義（サイト×サブカテゴリの組合せ）</td>
      <td style="text-align: right"><strong>1,015</strong></td>
    </tr>
    <tr>
      <td>pixivのサブカテゴリだけ</td>
      <td style="text-align: right"><strong>18</strong></td>
    </tr>
  </tbody>
</table>

<p>「対応サイト数」として意味があるのは269のほうだ。1,015は「pixivの<code class="language-plaintext highlighter-rouge">artworks</code>」「pixivの<code class="language-plaintext highlighter-rouge">ranking</code>」のように<strong>同じサイトの取得パターンを別々に数えた数</strong>なので、他ツールの公称値と横並びで比べると過大になる。</p>

<p>日本のユーザーに関係が深いところでは、<code class="language-plaintext highlighter-rouge">pixiv</code>・<code class="language-plaintext highlighter-rouge">fanbox</code>・<code class="language-plaintext highlighter-rouge">fantia</code>・<code class="language-plaintext highlighter-rouge">booth</code>・<code class="language-plaintext highlighter-rouge">skeb</code>・<code class="language-plaintext highlighter-rouge">seiga</code>・<code class="language-plaintext highlighter-rouge">kemono</code>が含まれていた。pixivのサブカテゴリは<code class="language-plaintext highlighter-rouge">artworks</code> / <code class="language-plaintext highlighter-rouge">user</code> / <code class="language-plaintext highlighter-rouge">ranking</code> / <code class="language-plaintext highlighter-rouge">search</code> / <code class="language-plaintext highlighter-rouge">series</code> / <code class="language-plaintext highlighter-rouge">favorite</code> / <code class="language-plaintext highlighter-rouge">followed</code> / <code class="language-plaintext highlighter-rouge">sketch</code> / <code class="language-plaintext highlighter-rouge">unlisted</code> / <code class="language-plaintext highlighter-rouge">pixivision</code>など18種類あり、さらに<code class="language-plaintext highlighter-rouge">pixiv-novel</code>が独立したCategoryとして小説（<code class="language-plaintext highlighter-rouge">novel</code> / <code class="language-plaintext highlighter-rouge">series</code> / <code class="language-plaintext highlighter-rouge">user</code> / <code class="language-plaintext highlighter-rouge">bookmark</code>）に対応している。「pixivの一括ダウンロード」でやりたいことのほとんどは、この時点で守備範囲に入っている。</p>

<h2 id="設定ファイルと認証ここで9割つまずく">設定ファイルと認証：ここで9割つまずく</h2>

<p>gallery-dlはコマンドラインオプションよりも<strong>JSON設定ファイル</strong>が主戦場になる。そして日本語の解説記事は、この探索順の説明が曖昧か、片方しか書いていないことが多い。実測すると3か所だった。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/gallery-dl-config-lookup.webp" width="1280" height="320" alt="設定ファイルの探索順：/etc/gallery-dl.conf、${HOME}/.config/gallery-dl/config.json、${HOME}/.gallery-dl.conf" style="width:100%;height:auto;" />
  <figcaption><code>-v</code> を付ければ、どのファイルを読んだかが必ず1行目付近に出る</figcaption>
</figure>

<p><code class="language-plaintext highlighter-rouge">-v</code>（verbose）で実行すると、起動時に読み込んだ設定ファイルが列挙される。手元では設定を置いていなかったため、次のように空リストが出た。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[gallery-dl][debug] Version 1.32.9
[gallery-dl][debug] Python 3.14.4 - macOS-14.5-arm64
[gallery-dl][debug] requests 2.34.2 - urllib3 2.7.0
[gallery-dl][debug] Configuration Files []
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">Configuration Files []</code>は「設定ファイルを1つも読んでいない」という意味</strong>だ。「設定を書いたのに効かない」ときは、まずここを見れば置き場所を間違えたのかどうかが一発でわかる。探索先は次の3か所である。</p>

<table>
  <thead>
    <tr>
      <th>優先順</th>
      <th>パス</th>
      <th>用途</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td><code class="language-plaintext highlighter-rouge">/etc/gallery-dl.conf</code></td>
      <td>システム全体の共通設定</td>
    </tr>
    <tr>
      <td>2</td>
      <td><code class="language-plaintext highlighter-rouge">${HOME}/.config/gallery-dl/config.json</code></td>
      <td>ユーザー標準（推奨）</td>
    </tr>
    <tr>
      <td>3</td>
      <td><code class="language-plaintext highlighter-rouge">${HOME}/.gallery-dl.conf</code></td>
      <td>ホーム直下の旧来形式</td>
    </tr>
  </tbody>
</table>

<p>PyYAMLやtomlを追加で入れればYAML・TOMLでも書けるが、素の状態ではJSONだけが確実に読める。</p>

<h3 id="pixivはurlが正しくても認証で止まる">pixivは「URLが正しくても」認証で止まる</h3>

<p>ログイン必須のサイトでは、URLの形式が完全に正しくても認証で停止する。手元でpixivの作品URLに<code class="language-plaintext highlighter-rouge">--simulate</code>をかけたところ、次のエラーになった。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[pixiv][error] AuthenticationError: 'refresh-token' required.
Run `gallery-dl oauth:pixiv` to get one.
</code></pre></div></div>

<p>これは不具合ではなく仕様だ。<strong>エラーメッセージが解決コマンドをそのまま提示している</strong>点が親切で、<code class="language-plaintext highlighter-rouge">gallery-dl oauth:pixiv</code>を実行してrefresh-tokenを取得し、設定ファイルに書き込めばよい。</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"extractor"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"base-directory"</span><span class="p">:</span><span class="w"> </span><span class="s2">"~/gallery-dl"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"pixiv"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"refresh-token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"取得したトークンを入れる"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"directory"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"pixiv"</span><span class="p">,</span><span class="w"> </span><span class="s2">"{user[account]}"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"filename"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{id}_{num}.{extension}"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Cookieを使う手もある。<code class="language-plaintext highlighter-rouge">--cookies-from-browser</code>でブラウザのCookieを渡せるが、GNOMEキーリングを使う環境ではSecretStorageの追加インストールが必要になる。「トークン方式のほうが再現性が高く、CIにも載せやすい」というのが実務上の結論だ。</p>

<div class="box-warning">

  <p><strong>トークンとCookieの取り扱い</strong>：refresh-tokenやCookieは<strong>アカウントそのものへのアクセス権</strong>に等しい。設定ファイルをリポジトリにコミットしない、CIでは秘密情報として注入する、共有マシンでは<code class="language-plaintext highlighter-rouge">${HOME}/.config</code>のパーミッションを確認する——このあたりはAPIキーと同じ扱いをする。</p>

</div>

<h2 id="実運用のパターン巡回重複回避メタデータ">実運用のパターン：巡回・重複回避・メタデータ</h2>

<p>gallery-dlが真価を発揮するのは、単発の取得ではなく<strong>繰り返し実行する巡回</strong>だ。処理の流れは次のようになる。</p>

<div class="mermaid">
flowchart LR
  A["URL / URLリスト"] --&gt; B["extractor 解決<br />269サイトから自動判定"]
  B --&gt; C{"認証が要る?"}
  C -- "要る" --&gt; D["config.json の<br />token / cookies"]
  C -- "不要" --&gt; E["集合を展開<br />投稿者・タグ・検索結果"]
  D --&gt; E
  E --&gt; F{"動画 (HLS/DASH)?"}
  F -- "はい" --&gt; G["ytdl ダウンローダー<br />yt-dlp へ委譲"]
  F -- "いいえ" --&gt; H["http ダウンローダー"]
  G --&gt; I["出力テンプレートで保存<br />+ archive DB に記録"]
  H --&gt; I
</div>

<p>繰り返し実行するときに効くのが<code class="language-plaintext highlighter-rouge">--download-archive</code>だ。取得済みIDをSQLiteに記録し、次回以降スキップする。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 取得済みを記録して差分だけ取る（2回目以降が速い）</span>
gallery-dl <span class="nt">--download-archive</span> ~/gallery-dl/archive.db <span class="s2">"対象URL"</span>

<span class="c"># メタデータをJSONで併記（あとで検索・分析に使える）</span>
gallery-dl <span class="nt">--write-metadata</span> <span class="s2">"対象URL"</span>

<span class="c"># 出力先の階層とファイル名を指定する</span>
gallery-dl <span class="nt">-D</span> ~/gallery-dl/pixiv <span class="nt">-f</span> <span class="s2">"{id}_{num}.{extension}"</span> <span class="s2">"対象URL"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--write-metadata</code>で吐き出されるJSONには、投稿者・タグ・投稿日時などサイト固有のフィールドがそのまま入る。<strong>画像そのものより、この構造化メタデータのほうが後工程で価値を持つ</strong>ことが多い。取得したタグ列をそのままデータセットのラベルにする、投稿日時で時系列の傾向を見る、といった使い方ができる。</p>

<p>なお<code class="language-plaintext highlighter-rouge">--download-archive</code>と<code class="language-plaintext highlighter-rouge">--write-metadata</code>は目的が違うので併用が前提だ。前者は「もう取ったか」の判定用、後者は「何を取ったか」の記録用である。</p>

<h2 id="どれを選ぶか静止画動画ライブ配信の分担">どれを選ぶか：静止画・動画・ライブ配信の分担</h2>

<p>ここまでを踏まえると、ダウンローダーの選定は「どれが最強か」ではなく<strong>対象の性質で分担する</strong>問題になる。</p>

<table>
  <thead>
    <tr>
      <th>対象</th>
      <th>使うツール</th>
      <th>理由</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>静止画ギャラリー（pixiv・fanbox等）</td>
      <td><strong>gallery-dl</strong></td>
      <td>集合単位の展開とサイト別メタデータに対応</td>
    </tr>
    <tr>
      <td>完成済みの動画・音声</td>
      <td><a href="/tool/yt-dlp-2026-complete-guide-cve-ai-integration/">yt-dlp</a></td>
      <td>フォーマット選択・字幕・後処理が充実</td>
    </tr>
    <tr>
      <td>進行中のライブ配信</td>
      <td><a href="/tool/streamlink-live-stream-recorder/">Streamlink</a></td>
      <td>終端未定のストリームに追従して録画できる</td>
    </tr>
    <tr>
      <td>チームで共有したい</td>
      <td><a href="/tool/metube-selfhosted-yt-dlp-web-ui/">MeTube</a></td>
      <td>yt-dlpをWeb UI化してセルフホストできる</td>
    </tr>
  </tbody>
</table>

<p>gallery-dlは<code class="language-plaintext highlighter-rouge">ytdl</code>バックエンド経由でyt-dlpを呼ぶので、<strong>両方入れておくのが実質的な標準構成</strong>になる。ライブ配信だけは時間軸の性質が違うため、<a href="/tool/streamlink-live-stream-recorder/">Streamlink</a>のような専用ツールが要る。</p>

<p>運用上の注意も1つ。gallery-dlは並列度やレート制限をデフォルトでは強くかけない。<strong>サイトへの負荷は自分で制御する必要がある</strong>ため、大量巡回するときは設定ファイルの<code class="language-plaintext highlighter-rouge">sleep</code>・<code class="language-plaintext highlighter-rouge">sleep-request</code>を明示して間隔を空けるのが礼儀であり、アカウント保全の観点でも合理的だ。</p>

<h2 id="まとめ">まとめ</h2>

<p>・gallery-dlは<strong>静止画ギャラリー側の標準ツール</strong>。yt-dlpと競合せず、内部で<strong>yt-dlpを呼ぶ</strong>（<code class="language-plaintext highlighter-rouge">yt_dlp</code>優先、無ければ<code class="language-plaintext highlighter-rouge">youtube_dl</code>）<br />
・v1.32.9の実測で<strong>対応サイト269件・extractor定義1,015件</strong>。公称値ではなく自分で数えられる<br />
・<strong>2026年4月5日に開発の本体がCodebergへ移行</strong>。GitHubはCI/CD・nightly・Dockerイメージ用として残り、アーカイブはされていない。pipユーザーは影響を受けない<br />
・設定ファイルの探索先は実測で3か所。<code class="language-plaintext highlighter-rouge">-v</code>の<code class="language-plaintext highlighter-rouge">Configuration Files []</code>で読み込み状況が確認できる<br />
・pixiv等は<strong>URLが正しくてもトークンが無ければ必ず止まる</strong>。<code class="language-plaintext highlighter-rouge">gallery-dl oauth:pixiv</code>で取得して設定に書く</p>

<h2 id="参照ソース">参照ソース</h2>

<ul>
  <li><a href="https://github.com/mikf/gallery-dl">mikf/gallery-dl — GitHub リポジトリ</a>（★19,252 / GPL-2.0 / 2026-08-19時点）</li>
  <li><a href="https://github.com/mikf/gallery-dl/issues/9374">[Announcement] Moving to Codeberg — GitHub issue #9374</a>（2026-04-05）</li>
  <li><a href="https://codeberg.org/mikf/gallery-dl">mikf/gallery-dl — Codeberg（移行後の開発本体）</a></li>
  <li><a href="https://gdl-org.github.io/docs/configuration.html">gallery-dl 公式ドキュメント（configuration / options / formatting）</a></li>
  <li>実測環境：gallery-dl 1.32.9 / Python 3.14.4 / macOS 14.5 arm64（<code class="language-plaintext highlighter-rouge">--list-extractors</code>・<code class="language-plaintext highlighter-rouge">-v --simulate</code>の出力）</li>
</ul>

<!--
Distribution memo (2026-08-19)
- X: 「gallery-dlの開発、2026年4月からCodebergに移ってます」＋Codeberg移行図（画像必須）。08時台JST枠
- セルフリプ: 実測269サイトの数え方コマンドを貼る
- 内部リンク: yt-dlpピラー記事から本記事へ導線を追加済み
-->]]></content><author><name></name></author><category term="tool" /><category term="gallery-dl" /><category term="automation" /><category term="oss" /><category term="python" /><category term="cli" /><category term="image" /><category term="pixiv" /><category term="downloader" /><summary type="html"><![CDATA[gallery-dlのインストールから設定ファイル・認証・出力テンプレートまで実測ベースで解説。v1.32.9を実際に入れて数えた対応サイトは269件・extractor定義1,015件。2026年4月にGitHubからCodebergへ開発が移った件と、pixivのrefresh-token必須エラーの直し方も扱う。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_gallery-dl-image-gallery-downloader.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_gallery-dl-image-gallery-downloader.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Context7 脆弱性ContextCrush（CVE-2026-75130）｜MCP配信悪用の手口と時系列</title><link href="https://ai-heartland.com/security/context7-contextcrush-vulnerability/" rel="alternate" type="text/html" title="Context7 脆弱性ContextCrush（CVE-2026-75130）｜MCP配信悪用の手口と時系列" /><published>2026-08-19T09:00:00+09:00</published><updated>2026-08-19T00:00:00+09:00</updated><id>https://ai-heartland.com/security/context7-contextcrush-vulnerability</id><content type="html" xml:base="https://ai-heartland.com/security/context7-contextcrush-vulnerability/"><![CDATA[<p><strong>Context7 脆弱性「ContextCrush」（CVE-2026-75130）</strong>が、2026年8月18日にNVDへ公開された。Context7は Upstash社が開発する MCP（Model Context Protocol）ドキュメント配信サーバーで、Claude Code・Cursor・Windsurfなど30以上のAIコーディングツールから利用される、いわば「MCPドキュメント配信の定番」だ。GitHub実測（2026-08-18時点）で★<strong>60,940</strong>・フォーク<strong>2,937</strong>、ライセンスはMIT。この記事では、その”正規の配信チャネル”そのものが攻撃経路になった仕組みを、公式アドバイザリとNVD一次情報から時系列で整理し、自分のMCP構成を点検する確認コマンドを提示する。</p>

<figure>
  <img loading="eager" src="/generated_images/sections/context7-contextcrush-attack-flow.webp" alt="ContextCrushの攻撃連鎖4段階。ライブラリ登録、Custom Rules埋め込み、MCP経由配信、AIエージェントによる実行" style="width:100%;height:auto;border-radius:.6rem;border:1px solid rgba(0,0,0,.08)" />
  <figcaption>ContextCrushの攻撃連鎖。攻撃者はContext7が持つ正規のライブラリ登録機能をそのまま使う（出典: Noma Security ブログの記述をもとに編集部作成）。</figcaption>
</figure>

<div class="point-box">
  <p><strong>30秒でわかる｜この記事のポイント</strong></p>

  <p>・<strong>正体</strong>：Upstash社製の<strong>MCPドキュメント配信サーバーContext7</strong>（★60,940）に見つかった脆弱性。CVE番号は<strong>CVE-2026-75130</strong>、通称<strong>ContextCrush</strong><br />
・<strong>核心</strong>：誰でも登録できるライブラリの「Custom Rules（AI Instructions）」欄が非サニタイズで、攻撃指示がMCP応答として正規のドキュメントに紛れ込みAIエージェントへ届く<br />
・<strong>時系列の特異点</strong>：発見・修正は2026年2月、公開開示は3月5日。NVDでのCVE公開は8月18日と、<strong>約5.5ヶ月のギャップ</strong>がある「後追い採番」の事案<br />
・<strong>深刻度</strong>：CVSS v3.1で<strong>9.0（Critical）</strong>、CVSS v4.0で<strong>6.4（Medium）</strong>と評価が分かれる<br />
・<strong>悪用実績</strong>：発見者Noma Securityは「in the wildでの悪用は観測されなかった」と明記</p>
</div>

<p>同種の「AI連携機能が攻撃者にとっての新しい配信経路になる」構図を体系的に押さえたい場合は、<a href="/security/supply-chain-security-guide-2026/">サプライチェーンセキュリティ2026｜攻撃手法・防御ツール・実践チェックリスト</a>を合わせて読んでほしい。ContextCrushは、MCPサーバー自体の実装欠陥（プロトコル層）ではなく、<strong>特定SaaSプラットフォームのアプリケーション層</strong>で起きたサプライチェーン型攻撃という点で、本記事末尾で比較する他のMCP関連脆弱性とレイヤーが異なる。</p>

<h2 id="context7とはaiコーディングツールが使うmcpドキュメント配信サーバー">Context7とは：AIコーディングツールが使うMCPドキュメント配信サーバー</h2>

<p>Context7（開発元 Upstash, Inc.・リポジトリ <code class="language-plaintext highlighter-rouge">upstash/context7</code>）は、サーバーレスRedis/DBで知られるUpstash社が公開する、ライブラリの最新ドキュメント・コード例をMCP経由でAIエージェントに配信するサービスだ。TypeScript製で、GitHub実測（2026-08-18時点）では★<strong>60,940</strong>・フォーク<strong>2,937</strong>、ライセンスはMIT（LICENSEファイルの記載と一致確認済み）。直近のpushも当日にあたり、npmパッケージ群（<code class="language-plaintext highlighter-rouge">@upstash/context7-mcp</code>・CLIの<code class="language-plaintext highlighter-rouge">ctx7</code>・エディタ連携の<code class="language-plaintext highlighter-rouge">@upstash/context7-opencode</code>等）も継続的にリリースされている、企業がホスティングも兼ねて運用する活発なプロジェクトだ。</p>

<p>Context7の基本的な役割は、AIコーディングアシスタントが「そのライブラリの最新の使い方」を参照する際に、古い学習データではなく最新のドキュメント・コード例をMCP経由で取得できるようにすることにある。ライブラリはContext7のレジストリに登録する形で追加でき、登録者はドキュメントに加えて「Custom Rules（AI Instructions）」という、そのライブラリを使う際にAIエージェントへ追加で伝えたい指示文を設定できる。この仕組み自体は、ライブラリ固有の注意点をAIに伝える便利な機能として設計されている。</p>

<div class="marker-yellow">ContextCrushの核心は「MCPサーバーの実装にバグがあった」ではなく、「<strong>誰でも書き込める入力（Custom Rules）が、十分な検証なしにAIエージェントへの"正規の指示"として配信される経路になっていた</strong>」という構造にある。</div>

<h2 id="context7-セキュリティ体制を実体面から見る企業運用か個人依存か">Context7 セキュリティ体制を実体面から見る：企業運用か個人依存か</h2>

<p>脆弱性の技術詳細に入る前に、Context7というプロジェクト自体の実体を確認しておく。読者が「Context7 セキュリティ」を調べる際に気になるのは、この種の欠陥が今後も繰り返されやすい構造かどうかだ。</p>

<p>・<strong>企業サポート</strong>：あり。Upstash社が開発・運用するホスティング型サービス（context7.com）と、OSSのクライアント/MCPサーバーを組み合わせたハイブリッド構成で、個人メンテナー1人に依存するプロジェクトではない<br />
・<strong>pre-1.0か</strong>：いいえ。★60,940・数年規模の運用実績があり、npmでも<code class="language-plaintext highlighter-rouge">context7-mcp</code>・<code class="language-plaintext highlighter-rouge">ctx7</code>（CLI）・<code class="language-plaintext highlighter-rouge">context7-sdk</code>など複数パッケージを継続リリース中<br />
・<strong>star数と実体の乖離</strong>：乖離なし。2026-08-18時点でもコミット・npm公開が続いており、開発は現在も活発<br />
・<strong>バス係数（コミット比率）</strong>：本記事では未確認。企業製品である点から個人依存の懸念は薄いと推測できるが、断定はしない</p>

<p>このプロファイル自体は、一般的な「メンテナー1人が燃え尽きて放置されたOSS」型のリスクとは異なる。ContextCrushが示しているのは、企業がしっかり運用しているサービスであっても、<strong>ユーザー生成コンテンツを扱う機能（Custom Rules）の検証設計</strong>という別の切り口で欠陥が生まれうる、という点だ。</p>

<h2 id="context7-脆弱性-contextcrush-の時系列発見から55ヶ月遅れたcve公開">Context7 脆弱性 ContextCrush の時系列：発見から5.5ヶ月遅れたCVE公開</h2>

<p>発見者Noma Security（研究者Eli Ainhorn）のブログと、NVDのCVE公開日を突き合わせると、時系列に大きなギャップがあることがわかる。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/context7-contextcrush-stat.webp" alt="CVE-2026-75130を数字で示す図。CVSS v3.1は9.0でCritical、CVSS v4.0は6.4でMedium、発見からCVE公開までの時差は約5.5ヶ月" style="width:100%;height:auto;border-radius:.6rem;border:1px solid rgba(0,0,0,.08)" />
  <figcaption>CVE-2026-75130の主要な数字。CVSSスコアはNVD記載値、時差は発見者ブログの日付とNVD公開日の突き合わせ（出典は本文・参照ソース参照）。</figcaption>
</figure>

<table>
  <thead>
    <tr>
      <th>日付</th>
      <th>出来事</th>
      <th>出典</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-02-18</td>
      <td>Noma Securityが脆弱性を発見</td>
      <td>Noma Security ブログ</td>
    </tr>
    <tr>
      <td>2026-02-19</td>
      <td>Upstashが報告を受理</td>
      <td>同上</td>
    </tr>
    <tr>
      <td>2026-02-23</td>
      <td>本番環境で修正が完了</td>
      <td>同上</td>
    </tr>
    <tr>
      <td>2026-03-05</td>
      <td>Noma Securityがブログで公開開示</td>
      <td>同上</td>
    </tr>
    <tr>
      <td>2026-08-18</td>
      <td>NVDがCVE-2026-75130を正式公開</td>
      <td>NVD</td>
    </tr>
  </tbody>
</table>

<p>この表が示す通り、<strong>発見から本番修正までは実質5日、公開開示までは約2週間</strong>というスピードで対応が完了している一方、NVDでの正式なCVE公開は約5.5ヶ月後になっている。この記事は「本日発覚した新規のゼロデイ」ではなく、「<strong>今年2月に発見・修正済みだった脆弱性に、本日ようやく正式なCVE番号が採番・公開された</strong>」という性質の事案として扱う。速報的な煽り方はせず、このギャップ自体を時系列として明示しておく。</p>

<p>なお、この種の「修正済みだが後日CVE採番」というパターンは珍しいものではない。CNA（CVE採番機関）への申請・NVDでの精査には時間がかかることがあり、開発元による迅速な修正と、公的なCVEデータベースへの反映は必ずしも同時進行しない。読者にとって重要なのは、CVE番号の公開日そのものを「事案の発生日」と混同しないことだ。</p>

<h3 id="cvss-v31とv40でスコアが割れる理由">CVSS v3.1とv4.0でスコアが割れる理由</h3>

<p>NVDのCVE-2026-75130ページには、CVSS v3.1で<strong>9.0（Critical）</strong>、CVSS v4.0で<strong>6.4（Medium）</strong>という、評価バージョンによって深刻度が大きく異なる2つのスコアが併記されている。CVSS v4.0は旧バージョンに比べて「攻撃の複雑さ」「必要な特権」「ユーザー関与の有無」といった要素をより細かく反映する設計になっており、間接プロンプトインジェクションのように「攻撃者が直接システムへ到達するわけではなく、AIエージェントの判断を介する」タイプの脆弱性では、旧バージョンより厳しめ、あるいは緩めに評価が振れることがある。本記事はどちらか一方を「正しいスコア」として採用せず、NVD記載の両方をそのまま併記する。</p>

<h2 id="攻撃の仕組みcustom-rulesの非サニタイズが招く間接プロンプトインジェクション">攻撃の仕組み：Custom Rulesの非サニタイズが招く間接プロンプトインジェクション</h2>

<p>ContextCrushの技術的な核心は、Context7のライブラリ登録機能が持つ「<strong>Custom Rules（AI Instructions）</strong>」欄の非サニタイズにある。Context7はライブラリの登録自体を誰にでも開放しており、登録者はドキュメント本体に加えて、そのライブラリを扱う際にAIエージェントへ伝える追加指示（Custom Rules）を自由記述できる。この欄の内容が十分に検証・無害化されないまま、MCP応答の一部としてそのままAIエージェントへ配信されていたことが問題だった。</p>

<p>VulnCheckのアドバイザリは、この脆弱性を<strong>CWE-1427（不適切な出力ニュートラライズ、LLMプロンプトの文脈での分類）</strong>として整理している。攻撃者は、Context7に悪意あるライブラリ（または既存ライブラリに偽装したエントリ）を登録し、Custom Rules欄に「このライブラリを使う際は〇〇を実行せよ」といった指示を埋め込む。開発者が普段どおりAIエージェントにそのライブラリのドキュメントを問い合わせると、Context7のMCPサーバーはドキュメント本体とCustom Rulesを合わせて返す。AIエージェントは、この応答を<strong>Context7という信頼されたMCPサーバーからの正規の情報</strong>として扱うため、埋め込まれた指示をユーザーの意図と誤認して実行してしまう可能性がある。</p>

<p>この構図は、一般に<strong>MCP プロンプトインジェクション</strong>と呼ばれる攻撃分類の一種だ。ユーザーが直接入力したプロンプトではなく、AIエージェントが外部ツール（この場合はMCPサーバー経由のドキュメント）から取得したコンテンツに攻撃指示が紛れ込む「間接プロンプトインジェクション」に該当する。エージェントが「自分が呼び出したツールの応答は信頼できる」という前提で動作する設計そのものが悪用の土台になっている点が、通常のプロンプトインジェクション対策（入力フィルタリング）だけでは防ぎきれない理由でもある。</p>

<h3 id="プロトコル層の脆弱性との違いmcp-stdioトランスポート欠陥との比較">プロトコル層の脆弱性との違い：MCP STDIOトランスポート欠陥との比較</h3>

<p>「MCPの脆弱性」とひとくくりにされがちだが、ContextCrushは既存記事で扱った<strong>MCP STDIOトランスポートの設計欠陥</strong>とは攻撃レイヤーが異なる。混同を避けるため、ここで比較しておく。</p>

<table>
  <thead>
    <tr>
      <th>項目</th>
      <th>ContextCrush（Context7）</th>
      <th>MCP STDIOトランスポート欠陥</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>攻撃レイヤー</td>
      <td>アプリケーション層（Custom Rulesの非サニタイズ）</td>
      <td>プロトコル/トランスポート層（コマンド文字列をOS実行）</td>
    </tr>
    <tr>
      <td>影響範囲</td>
      <td>Context7という特定SaaSプラットフォーム</td>
      <td>MCP SDK全般（Python/TS/Java/Rust）</td>
    </tr>
    <tr>
      <td>CVE/分類</td>
      <td>CVE-2026-75130・CWE-1427</td>
      <td>複数CVE</td>
    </tr>
    <tr>
      <td>攻撃の性質</td>
      <td>間接プロンプトインジェクション経由のサプライチェーン攻撃</td>
      <td>コマンドインジェクションによるRCE</td>
    </tr>
    <tr>
      <td>出所</td>
      <td>Noma Security・VulnCheck・NVD</td>
      <td><a href="/security/anthropic-mcp-stdio-rce-vulnerability/">MCP脆弱性！STDIOトランスポートの設計欠陥で20万台のサーバーがRCEの危険に——OX Securityが警告</a></td>
    </tr>
  </tbody>
</table>

<p>ContextCrushは「MCPサーバーの実装そのもの」に穴があったわけではなく、「<strong>特定のMCPサーバー（Context7）が配信するコンテンツの検証が甘かった</strong>」という、いわばアプリケーションレベルの問題だ。MCPを実装する側だけでなく、MCPサーバーを<strong>運用する側</strong>（ユーザー生成コンテンツを扱うレジストリ型サービス）にも同種のリスクがあることを示す事例といえる。</p>

<div class="box-tip">
<strong>読者の3つの問いへの答え</strong><br />
① <strong>何が起きた</strong>：誰でも書き込めるCustom Rules欄が非サニタイズのままMCP応答に含まれ、攻撃指示が正規のドキュメントとしてAIエージェントに届く経路が存在した。② <strong>何が原因</strong>：ユーザー生成コンテンツ（Custom Rules）を、既存の信頼された配信チャネル（MCPレスポンス）に無検証で混ぜ込んでいたこと。③ <strong>何をすればよいか</strong>：自分の環境がContext7を利用しているかを確認した上で、公式のアナウンスと修正状況を追う。
</div>

<h2 id="自分の環境を確認するcontext7をmcpサーバーとして使っているか">自分の環境を確認する：Context7をMCPサーバーとして使っているか</h2>

<p>このCVEは既に修正済みと報じられているため、攻撃の再現手順は扱わない。ここでは、読者が自分のMCP構成にContext7が含まれているかを確認する手順のみを示す。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Claude Code の場合、登録済みMCPサーバーの一覧にContext7が含まれるか確認</span>
claude mcp list 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-i</span> context7

<span class="c"># 各種エディタ・エージェントのMCP設定ファイルにcontext7の記載が無いか横断的に確認</span>
<span class="nb">grep</span> <span class="nt">-ril</span> <span class="s2">"context7"</span> ~/.claude ~/.cursor ~/.codeium 2&gt;/dev/null
</code></pre></div></div>

<p>Context7が設定に含まれている場合でも、この脆弱性の核心はContext7の<strong>サーバー側（バックエンド・パーシングエンジン・クローリングエンジン）</strong>にあり、これらのコンポーネントは<code class="language-plaintext highlighter-rouge">upstash/context7</code>のGitHubリポジトリには含まれていない非公開部分だ(README「Disclaimer」に明記)。つまり、読者側でクライアントのnpmパッケージ（<code class="language-plaintext highlighter-rouge">@upstash/context7-mcp</code>等）のバージョンを上げる対応が必要かどうかは、この記事の執筆時点では判断できない。</p>

<p>Context7をローカルサーバーとして自前でホストしている場合と、Upstashが運用するホスティング型サービス（context7.com経由）を利用している場合とでも、確認すべきポイントは異なる。ホスティング型を素直に使っているだけであれば、サーバー側の修正は既に反映されている可能性が高い。一方、フォークして独自にホストしている、あるいはバックエンドの一部を自前実装に置き換えているような環境では、Custom Rulesのサニタイズ処理が自環境にも反映されているかを個別に確認する必要がある。この判断材料についても、本記事が扱えるのはここまでで、最終的な要否は公式ドキュメント・アナウンスを確認してほしい。</p>

<div class="box-warning">
<strong>注意</strong>：NVDが記載する影響バージョン範囲は「0〜2.1.2」だが、現行のnpmパッケージ`@upstash/context7-mcp`は4.0.2まで進んでおり、バージョン体系がそのまま対応するかは不明。VulnCheckアドバイザリにも修正バージョンの明記は無い。「サーバー側で修正済み」と報じられている一方、クライアント側の対応要否については、断定せず公式のアナウンスを確認することを推奨する。
</div>

<table>
  <thead>
    <tr>
      <th>確認項目</th>
      <th>状態</th>
      <th>判定</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Context7をMCP設定に登録している</td>
      <td>該当する</td>
      <td>以下のCustom Rules関連の対応方針を確認</td>
    </tr>
    <tr>
      <td>Context7をMCP設定に登録している</td>
      <td>該当しない</td>
      <td>本脆弱性の直接的な影響対象外</td>
    </tr>
    <tr>
      <td>クライアント側npmパッケージの対応要否</td>
      <td>未確認</td>
      <td>公式アナウンスの確認を推奨（本記事では断定しない）</td>
    </tr>
  </tbody>
</table>

<h2 id="context7-脆弱性-の対策現時点で言えることと言い切れないこと">Context7 脆弱性 の対策：現時点で言えることと、言い切れないこと</h2>

<p>Noma Securityのブログによれば、Upstash側の<strong>本番環境での修正は2026年2月23日に完了済み</strong>と報じられている。この修正はContext7のサーバー側（Custom Rulesの検証・サニタイズ処理）に対して行われたとみられ、GitHubリポジトリで公開されているクライアント側のコード自体に、この脆弱性に対応する明示的な修正コミットがあるかどうかは、本記事では特定していない。</p>

<p>読者側で現実的に取れる対応は以下の通りだ。</p>

<p>・<strong>自分のMCP構成にContext7が含まれるか確認する</strong>（上記コマンド参照）<br />
・<strong>AIエージェントがMCP経由で取得した指示を無条件に実行しない運用にする</strong>：ドキュメント取得ツールからの応答であっても、ファイル削除・外部送信を伴う操作は人間の承認を挟む<br />
・<strong>公式のセキュリティアナウンスを継続的に確認する</strong>：クライアント側の対応要否が明記されるまでは、憶測で「対応済み」「対応不要」と判断しない<br />
・<strong>MCPレジストリ型サービスを自分で運用・実装している場合</strong>は、ユーザー生成コンテンツ（今回のCustom Rulesに相当する自由記述欄）が、既存の信頼された配信経路（MCPレスポンス）にそのまま混入していないかを棚卸しする</p>

<p>自分でMCPサーバーを実装・運用している場合、ContextCrushの事案は棚卸しの材料になる。確認しておきたい観点は、<strong>①ユーザーが自由記述できる欄（Custom Rules相当）が、他の信頼された情報と無検証で同じ応答に混ざっていないか</strong>、<strong>②AIエージェント側が、MCP応答内のどの部分が「ドキュメント」でどの部分が「指示」なのかを区別できる設計になっているか</strong>の2点だ。</p>

<p>MCPクライアント側（Claude Code・Cursor等のエージェント実装）の観点でも、対応の余地はある。多くのエージェントは、MCPツールの実行結果を追加のシステムプロンプトのように扱わず、あくまで「参照情報」として提示した上でユーザーの判断を挟む設計を選べる。ファイル削除・外部への送信・シェルコマンド実行など、取り返しのつかない操作については、MCP応答由来の指示だけを根拠に自動実行しない設定・レビューフローを維持することが、個別のCVE対応よりも汎用的な防御線になる。</p>

<h2 id="まとめ正規の配信チャネルが攻撃経路になったサプライチェーン攻撃">まとめ：正規の配信チャネルが攻撃経路になったサプライチェーン攻撃</h2>

<div class="conclusion-box">
  <p>Context7 脆弱性ContextCrush（CVE-2026-75130）は、★60,940という広く使われるMCPドキュメント配信サーバーで見つかった、Custom Rules（AI Instructions）の非サニタイズに起因する間接プロンプトインジェクションだ。攻撃者は特別な侵入手段を必要とせず、Context7が公開しているライブラリ登録機能をそのまま使うだけで、正規の配信チャネル経由でAIエージェントに攻撃指示を届けられた。</p>

  <p>発見から本番修正までは実質5日という迅速な対応が取られていた一方、NVDでのCVE公開は約5.5ヶ月遅れており、「速報」として煽らず時系列を正しく理解することが重要だ。in the wildでの悪用は観測されていないが、クライアント側の対応要否が公式に明記されていない点は、読者自身が今後の公式アナウンスを確認する必要がある。</p>

  <p>MCPを介した信頼チャネルの悪用という構図は、<a href="/security/anthropic-mcp-stdio-rce-vulnerability/">MCP脆弱性！STDIOトランスポートの設計欠陥で20万台のサーバーがRCEの危険に——OX Securityが警告</a>や<a href="/security/nginx-ui-mcp-rce-vulnerability-chain/">nginx-ui 脆弱性 CVE-2026-33032｜MCP機能起点の認証バイパスから半年で25件超のRCE連鎖</a>、<a href="/security/aws-kiro-cve-2026-10591-mcp-config-rce/">AWS Kiro 脆弱性CVE-2026-10591とは｜MCP設定書き換えRCEを2系統の報告から検証</a>でも、レイヤーは異なるがそれぞれ扱っている。MCPを利用・実装しているなら、どのレイヤーの信頼が悪用されうるかを横断的に把握しておく価値がある。</p>
</div>

<div class="mermaid">
flowchart TD
    A["攻撃者がContext7へ<br />悪意ライブラリを登録"] --&gt; B["Custom Rules欄に<br />攻撃指示を埋め込み（非サニタイズ）"]
    B --&gt; C["開発者がAIエージェント経由で<br />そのライブラリのドキュメントを問い合わせ"]
    C --&gt; D["Context7のMCPサーバーが<br />ドキュメント＋Custom Rulesを応答"]
    D --&gt; E{"AIエージェントは<br />正規の指示と誤認するか？"}
    E -- はい --&gt; F["埋め込まれた指示を実行<br />CVE-2026-75130"]
    E -- いいえ 人間承認等の防御 --&gt; G["実行前に検知・停止"]
    style F fill:#ffe3e3,stroke:#c92a2a,stroke-width:2px
    style G fill:#d9f5ec,stroke:#0b7a63,stroke-width:2px
</div>

<h2 id="参照ソース">参照ソース</h2>

<p>・<a href="https://nvd.nist.gov/vuln/detail/CVE-2026-75130">NVD - CVE-2026-75130</a> — CVSSスコア（v3.1/v4.0）・影響バージョン・公開日を取得<br />
・<a href="https://noma.security/blog/contextcrush-context7-the-mcp-server-vulnerability/">ContextCrush: The Context7 MCP Server Vulnerability Hiding in Plain Sight（Noma Security, 2026-03-05）</a> — 発見経緯・攻撃メカニズム・開示タイムラインを取得<br />
・<a href="https://www.vulncheck.com/advisories/context7-prompt-injection-via-custom-ai-instructions">VulnCheck Advisory: Context7 Prompt Injection via Custom AI Instructions</a> — CWE-1427分類・CVSS v4ベクターを取得<br />
・<a href="https://github.com/upstash/context7">upstash/context7（公式リポジトリ）</a> — star数・ライセンス・開発状況を実測</p>

<!--
Distribution memo（Step 8）
- X（08時台JST・画像必須）: 「MCPドキュメント配信の定番Context7に、Custom Rules非サニタイズの脆弱性。発見は2月、CVE公開は本日」の一点突破。攻撃連鎖図を添付。
  セルフリプ（30分以内）: 「発見〜修正は5日、CVE公開まで5.5ヶ月というギャップも」で本文への誘導。
- はてブ: 「正規の配信チャネルがそのまま攻撃経路になる」構図がMCP実装者・利用者双方に刺さる想定。
- 内部リンク提案（受け側）: /security/anthropic-mcp-stdio-rce-vulnerability/ や /security/nginx-ui-mcp-rce-vulnerability-chain/ から本記事へ「同種のMCP信頼チャネル悪用の実例」として参照可能。
- 既報状況と差別化: WebSearch実測時点（2026-08-19）で本CVE単体を扱う日本語記事は0件（Zenn/Qiitaも同様）。英語圏はNoma Security・SC Media・Infosecurity Magazine等で既報。
-->]]></content><author><name>編集部</name></author><category term="security" /><category term="security" /><category term="セキュリティ" /><category term="cve" /><category term="MCP" /><category term="プロンプトインジェクション" /><category term="サプライチェーン" /><summary type="html"><![CDATA[Context7 脆弱性ContextCrush（CVE-2026-75130、CVSS v3.1で9.0）は、誰でも登録できるMCPドキュメント配信を悪用し、Custom Rulesに仕込んだ指示をAIエージェントへ届ける間接プロンプトインジェクションです。発見から公開までの時系列と自環境の確認コマンドを整理します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_context7-contextcrush-vulnerability.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_context7-contextcrush-vulnerability.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AIコーディングエージェント 脆弱性｜GitHub Issue起点でCI/CDシークレットが漏れる攻撃チェーン</title><link href="https://ai-heartland.com/security/ai-coding-agent-cicd-secrets-github-issue-attack/" rel="alternate" type="text/html" title="AIコーディングエージェント 脆弱性｜GitHub Issue起点でCI/CDシークレットが漏れる攻撃チェーン" /><published>2026-08-18T09:00:00+09:00</published><updated>2026-08-18T00:00:00+09:00</updated><id>https://ai-heartland.com/security/ai-coding-agent-cicd-secrets-github-issue-attack</id><content type="html" xml:base="https://ai-heartland.com/security/ai-coding-agent-cicd-secrets-github-issue-attack/"><![CDATA[<p><strong>AIコーディングエージェント 脆弱性</strong>は、Claude Code・Gemini CLI・OpenAI Codexという主要3社の製品で、独立に、しかし同じ形で見つかった。Black Hat USA 2026で開示された研究によれば、攻撃の起点は「誰でも作成できるGitHub Issue」で、そこからCI/CDワークフローが握るシークレット（APIキーやトークン）へ到達する経路が、3社それぞれのエージェントで成立していたという。単発のCVEではなく、AIエージェントとシステムを仲介する「ハーネス」の設計そのものが攻撃面になっていることを示す事例だ。</p>

<figure>
  <img loading="eager" src="/generated_images/sections/ai-coding-agent-cicd-secrets-attack-chain.webp" alt="攻撃チェーンの図解。GitHub Issueを作成（誰でも投稿できる入力）→CI/CDワークフローが起動（on: issues トリガー）→エージェントがIssue本文を処理（プロンプトインジェクション）→シークレットを外部へ持ち出し（許可済み通信路を悪用）。Claude Code・Gemini CLI・OpenAI Codexの3社で独立に成立した" style="width:100%;height:auto;border-radius:.6rem;border:1px solid rgba(0,0,0,.08)" />
  <figcaption>GitHub Issueを起点にCI/CDのシークレットへ到達する攻撃チェーン（出典: The Hacker News・Cloud Security Alliance research note）。</figcaption>
</figure>

<div class="point-box"><strong>30秒でわかる AIコーディングエージェント 脆弱性（2026年8月時点）</strong><ul>
<li>・<strong>何が起きた</strong>：<strong>Claude Code・Gemini CLI・OpenAI Codex</strong>の3社で、GitHub Issueを起点にCI/CDのシークレットへ到達する攻撃チェーンが独立に成立していた。</li>
<li>・<strong>Claude Code</strong>：CVE-2026-54316。修正版<strong>2.1.163</strong>。CVSSはNVD 9.1・Anthropic自己申告6.0と評価が割れている。</li>
<li>・<strong>Gemini CLI</strong>：CVE-2026-12537（GHSA-wpqr-6v78-jr5g）。CVSS<strong>10.0</strong>。修正版はgemini-cli 0.39.1／0.40.0-preview.3・run-gemini-cli 0.1.22。</li>
<li>・<strong>OpenAI Codex</strong>：CVE不採番。「仕様通り」としジョブ分離・read-onlyサンドボックスで対応。</li>
<li>・<strong>やること</strong>：バージョン確認とCIワークフローの `on: issues` ＋シークレット同居チェック（本文にコマンドあり）。</li>
</ul></div>

<blockquote>
  <p>エージェント型ツール・サプライチェーン攻撃全般の防御チェックリストは <a href="/security/supply-chain-security-guide-2026/">サプライチェーンセキュリティ2026｜攻撃手法・防御ツール・実践チェックリスト</a> をご覧ください。</p>
</blockquote>

<h2 id="aiコーディングエージェント-脆弱性の正体github-issueを起点にcicdシークレットへ到達する仕組みとタイムライン">AIコーディングエージェント 脆弱性の正体：GitHub Issueを起点にCI/CDシークレットへ到達する仕組みとタイムライン</h2>

<p>3社のAIコーディングエージェントは、いずれもGitHub上のリポジトリと連携し、Issueへのコメントやラベル付けをトリガーにCI/CDワークフロー上でエージェントを起動できる。この設計自体は便利な自動化機能だが、今回報告された攻撃チェーンはその設計の裏側を突く。</p>

<p>流れは次の通りだ。まず攻撃者は、対象リポジトリに誰でも投稿できるGitHub Issueを作成する。リポジトリ側のCI/CDワークフローが <code class="language-plaintext highlighter-rouge">on: issues</code> のようなIssue関連のイベントをトリガーにしていると、Issue作成だけでワークフローが起動する。このワークフロー内でAIコーディングエージェントが呼び出され、Issue本文を読み込んで処理する際、エージェントはその本文を「信頼できる指示」として扱ってしまう（プロンプトインジェクション）。ワークフローがCI環境のシークレット（APIキー・デプロイトークン等）にアクセスできる状態であれば、エージェントは細工されたIssue本文の指示に従い、そのシークレットを何らかの通信路経由で外部へ持ち出せてしまう。</p>

<p>タイムラインとして確認できているのは以下の通りだ。</p>

<p>・<strong>2026-08-07</strong>：The Hacker Newsが、Claude Code・Gemini CLIの脆弱性がGitHub Issueを起点にCI Workflow Secretsへ到達することを報じる<br />
・<strong>2026-08-08</strong>：Cloud Security Allianceがresearch noteを公開し、3社（OpenAI Codexを含む）の技術詳細・CVE番号・修正版をまとめる<br />
・<strong>Black Hat USA 2026</strong>：本件を含む攻撃チェーンが統合的に開示される（開示の正確な日付は各社アドバイザリ側では明示されておらず、上記2媒体の報道を通じて確認できる範囲にとどまる）</p>

<p>同種の設計欠陥がプロトコルレベルで報告された例としては、<a href="/security/anthropic-mcp-stdio-rce-vulnerability/">MCP脆弱性！STDIOトランスポートの設計欠陥で20万台のサーバーがRCEの危険に——OX Securityが警告</a> がある。今回の3社横断の事例は、プロトコルではなく「エージェントが外部入力をどこまで信頼するか」というハーネス側の設計判断が焦点になっている点が異なる。</p>

<h2 id="claude-codegemini-cliopenai-codex3社のaiコーディングエージェント-脆弱性を比較する">Claude Code・Gemini CLI・OpenAI Codex：3社のAIコーディングエージェント 脆弱性を比較する</h2>

<p>3社の脆弱性は攻撃の起点こそ共通するが、具体的な欠陥の型とベンダーの対応は大きく異なる。</p>

<table>
  <thead>
    <tr>
      <th>製品</th>
      <th>脆弱性の型</th>
      <th>CVSS</th>
      <th>修正版</th>
      <th>ベンダーの立場</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Claude Code</td>
      <td>事前許可ドメイン経由の帯域外データ持ち出し（HuggingFaceダウンロードカウンタを悪用しAPIキーを1文字ずつ漏洩）</td>
      <td>NVD 9.1／Anthropic自己申告 6.0</td>
      <td>2.1.163</td>
      <td>Moderate認定（NVDと評価が乖離）</td>
    </tr>
    <tr>
      <td>Gemini CLI</td>
      <td>ヘッドレスモードのワークスペース自動信頼＋<code class="language-plaintext highlighter-rouge">--yolo</code>でのallowlistバイパス</td>
      <td>10.0</td>
      <td>0.39.1／0.40.0-preview.3／run-gemini-cli 0.1.22</td>
      <td>Critical認定・即修正</td>
    </tr>
    <tr>
      <td>OpenAI Codex</td>
      <td>マルチパス構成で前段の出力が後段の信頼されたシステムプロンプト文脈になる設計</td>
      <td>未採番</td>
      <td>パッチではなくワークフロー変更（ジョブ分離・read-onlyサンドボックス）</td>
      <td>「仕様通り」として脆弱性非認定</td>
    </tr>
  </tbody>
</table>

<figure>
  <img loading="lazy" src="/generated_images/sections/ai-coding-agent-cicd-secrets-vendor-stack.webp" alt="3社のCVSS評価とベンダー対応。Gemini CLIはCVSS10.0でCritical認定・即修正。Claude CodeはCVSS9.1（NVD）だがAnthropic自己申告は6.0でModerate。OpenAI CodexはCVE未採番で仕様通りとしジョブ分離で対応" style="width:100%;height:auto;border-radius:.6rem;border:1px solid rgba(0,0,0,.08)" />
  <figcaption>3社のCVSS評価とベンダー対応の温度差（出典: NVD・GitHub Advisory Database・GitLab Advisory Database）。</figcaption>
</figure>

<p>もっとも評価が割れているのがClaude Codeだ。NVDはCVSS v3.1で9.1というCritical寄りのスコアを付けているのに対し、Anthropicは自己申告のCVSS v4で6.0（Moderate）としている。両者の評価がなぜ食い違うのか、その根拠を明示した一次ソースの記述は確認できておらず、攻撃の前提条件（HuggingFaceという事前許可済みドメインが必要になる等）の評価の重み付けが異なる可能性はあるが、断定はできない。<strong>この食い違いの理由自体は本記事執筆時点で未確認</strong>として扱う。</p>

<p>なお、脆弱性の対象が「Claude Code本体（npmパッケージ <code class="language-plaintext highlighter-rouge">@anthropic-ai/claude-code</code>）」なのか「CI連携で使われることが多いGitHub Action版（<code class="language-plaintext highlighter-rouge">anthropics/claude-code-action</code>）」なのかは、参照した記事群の間でも表記が揺れている。GitLab Advisory Databaseの記載はnpmパッケージを対象としているが、CI/CDでの実運用ではGitHub Action経由の利用が主流であるため、どちらが一次責任箇所かは公式アドバイザリの原文を個別に確認するのが確実だ。</p>

<p>Gemini CLI側は、CVE-2026-12537という番号とGHSA-wpqr-6v78-jr5gというGitHub Advisory IDの両方が資料上に現れる。GHSA-wpqr-6v78-jr5g自体は2026-04-24公開だが、2026-08のBlack Hat開示に関する報道でCVE-2026-12537という番号も併記されている。この2つが同一脆弱性への後日のCVE採番なのか、別物を指すのかは、本記事の調査範囲では確定できていない。</p>

<p>Gemini CLI固有の技術的な欠陥は2つある。1つはヘッドレスモードでワークスペースを自動的に信頼してしまう挙動、もう1つは <code class="language-plaintext highlighter-rouge">--yolo</code> フラグを使った場合にツール実行のallowlist（許可リスト）チェックがバイパスされてしまう挙動だ。Googleはこの2つの設計欠陥を1本のアドバイザリで同時に修正している。</p>

<p>OpenAI Codexは唯一、今回の件をCVEとして採番していない。OpenAIの立場は「マルチパス構成において前段の出力が後段の信頼されたシステムプロンプト文脈として扱われるのは設計上の挙動であり、脆弱性ではない」というものだ。対応としてはパッチではなく、ジョブの分離とread-onlyサンドボックスの徹底、そして公式ドキュメントへの「instruction files（Issue本文のような外部入力ファイル）は信頼できない入力として扱う」という明記に留めている。脆弱性認定の基準がベンダーごとに異なることを、この3社の対応差が端的に示している。</p>

<p>Claude Code単体の脆弱性を広く一覧したい場合は、<a href="/security/claude-code-security-guide/">Claude Code セキュリティ｜サンドボックスの守備範囲と公開アドバイザリで見る攻撃面・確認コマンド</a> も参照してほしい。本記事はそのうちCVE-2026-54316の1件を、Gemini CLI・OpenAI Codexとの横断比較という切り口で深掘りする専用記事という位置づけだ。</p>

<p>自律型ペネトレーションテストの標準化という別角度からAIエージェントのセキュリティを見るなら、<a href="/security/owasp-apts-autonomous-pentesting-standard/">OWASP APTS｜AIエージェント時代の自律型ペネトレーションテスト基準を読む</a> も関連する。</p>

<h2 id="自分の環境が影響を受けるか確認する方法">自分の環境が影響を受けるか確認する方法</h2>

<p>実際に確認すべきポイントは、①各エージェントのバージョンが修正版以降か、②CI/CDワークフローに「Issueトリガー」と「シークレット参照」が同居していないか、③（Claude Codeの場合）WebFetchの許可リストにパス制限のないベアホスト名が残っていないか、の3点になる。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># インストール済みバージョンの確認（各社の修正版と比較する）</span>
npm <span class="nb">ls</span> <span class="nt">-g</span> @anthropic-ai/claude-code 2&gt;/dev/null <span class="o">||</span> claude <span class="nt">--version</span>
npm <span class="nb">ls</span> <span class="nt">-g</span> @google/gemini-cli 2&gt;/dev/null <span class="o">||</span> gemini <span class="nt">--version</span>
</code></pre></div></div>

<p>本記事の検証環境で実際に上記コマンドを実行したところ、<code class="language-plaintext highlighter-rouge">claude --version</code> は <code class="language-plaintext highlighter-rouge">2.1.234 (Claude Code)</code> を返した。これはClaude Codeの修正版である2.1.163より新しく、この環境のClaude Codeは修正済みバージョンだと確認できる。一方でGemini CLIはこの検証環境に導入されておらず、<code class="language-plaintext highlighter-rouge">npm ls -g @google/gemini-cli</code> は空、<code class="language-plaintext highlighter-rouge">gemini --version</code> は「コマンドが見つからない」というエラーになった。両方のコマンドを併記しているのは、npm経由の導入かバイナリ配布かで確認方法が変わりうるためだ。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># CIワークフローで、Issueトリガーとシークレット利用が同居していないか確認</span>
<span class="nb">grep</span> <span class="nt">-rln</span> <span class="s2">"on:"</span> .github/workflows/<span class="k">*</span>.yml 2&gt;/dev/null | xargs <span class="nb">grep</span> <span class="nt">-l</span> <span class="s2">"issues:"</span> 2&gt;/dev/null
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"run-gemini-cli@"</span> .github/workflows/<span class="k">*</span>.yml 2&gt;/dev/null
</code></pre></div></div>

<p>このコマンドを当サイト自身のリポジトリで実際に実行したところ、<code class="language-plaintext highlighter-rouge">dreaming.yml</code> が1件ヒットした。ただし中身を確認すると、これは <code class="language-plaintext highlighter-rouge">on: issues</code> というIssueトリガーではなく、<code class="language-plaintext highlighter-rouge">permissions: issues: read</code>（closed issueの収集に使う読み取り権限）が偶然 <code class="language-plaintext highlighter-rouge">issues:</code> という文字列にマッチしていた誤検出だった。実際のトリガーは <code class="language-plaintext highlighter-rouge">schedule</code> と <code class="language-plaintext highlighter-rouge">workflow_dispatch</code> のみで、Issue作成では起動しない。<strong>このコマンドは足がかりの一覧を出すだけで、ヒットしたファイルは必ず <code class="language-plaintext highlighter-rouge">on:</code> ブロックの中身を目視確認する必要がある</strong>——これは実際にコマンドを動かして初めて分かった注意点だ。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Claude CodeのWebFetch許可リストにベアホスト名（パス制限なし）が残っていないか確認</span>
<span class="nb">grep</span> <span class="nt">-n</span> <span class="s2">"huggingface.co"</span> .claude/settings.json 2&gt;/dev/null
</code></pre></div></div>

<p>こちらも当リポジトリで実行したが該当行は無かった（本サイトはClaude CodeのWebFetch許可リストにHuggingFaceドメインを登録していないため）。読者は自分のプロジェクトの <code class="language-plaintext highlighter-rouge">.claude/settings.json</code> や、CI上のワークフロー定義ファイルに対して同じコマンドを実行し、Issueトリガーとシークレットの同居、パス制限のないベアホスト名の許可が無いかを確認してほしい。</p>

<div class="mermaid">
flowchart TD
    A["対象エージェントは<br />修正版以降か？"] -- いいえ --&gt; B["まずアップデート"]
    A -- はい --&gt; C{"CIワークフローに<br />Issueトリガーとシークレットが<br />同居しているか？"}
    C -- いいえ --&gt; D["この攻撃チェーンの<br />対象外"]
    C -- はい --&gt; E{"Issue本文を処理する<br />ステップとシークレットに<br />アクセスするステップが<br />同一ジョブか？"}
    E -- いいえ（分離済み） --&gt; F["リスクは限定的"]
    E -- はい --&gt; G["ジョブ分離・許可リストの<br />見直しが必要"]
</div>

<div class="box-tip">
<strong>読者の3つの問いへの答え</strong><br />
① <strong>このAIコーディングエージェント 脆弱性は結局何なのか</strong>：GitHub Issueという誰でも作れる入力から、CI/CDのシークレットに到達する攻撃チェーンが3社で独立に成立していた事例。 ② <strong>何を解決する記事か</strong>：3社の脆弱性の型・CVSS・ベンダー対応の違いを比較表で整理し、自分の環境で今すぐ実行できる確認コマンドを示す。 ③ <strong>何を代替できるか</strong>：この記事自体は対策を代替しない。バージョンアップデートとCIワークフローの点検が唯一の実効的な対応になる。
</div>

<h2 id="なぜハーネスが脆弱性の主戦場になるのか">なぜハーネスが脆弱性の主戦場になるのか</h2>

<p>3社の脆弱性を並べると、共通する構造が見えてくる。AIコーディングエージェント本体（LLM）が持つ「指示に従う」という性質自体は変えられない。だとすれば、エージェントとシステムを仲介する「ハーネス」——どの入力を信頼するか、どのツール呼び出しをどこまで許可するか、シークレットへのアクセス範囲をどう限定するか——の設計が、事実上の防御境界になる。</p>

<p>Claude Codeの事例は、WebFetchの許可リストが「ドメイン単位」で緩く設定されていたことが起点だった。事前に許可されたHuggingFaceドメインへの通信自体は正当な機能だが、パスやリクエストの内容までは検証されておらず、その正当な通信路がAPIキーを1文字ずつ持ち出す帯域外チャネルに転用された。Gemini CLIの事例は、ヘッドレスモード（人間の確認を挟まない自動実行モード）でワークスペースを自動信頼する設計と、<code class="language-plaintext highlighter-rouge">--yolo</code> フラグによる許可リストバイパスという、どちらも「利便性のために追加された緩和機構」が攻撃面になっていた。OpenAI Codexは、マルチパス構成において前段の処理結果が後段では「信頼されたシステムプロンプト文脈」に昇格してしまう設計自体をOpenAIが仕様と認めており、これも広義には同じ構造の問題だ。</p>

<div class="marker-yellow">3社に共通するのは、「便利にするための自動信頼・許可リスト・多段処理」という設計要素そのものが、外部からの入力を扱う経路になった瞬間に攻撃面へ転化する、という構図だ。</div>

<p>言い換えると、今回の一連の脆弱性は個別のバグというより、「AIエージェントに何をどこまで自動で信頼させるか」というハーネス設計の根本的な難しさを、3社が独立に踏み抜いた結果と見ることができる。CI/CD環境はシークレットへのアクセスとエージェントの自動実行が両立する場所であるため、この種の設計判断が特に高い代償を伴う。</p>

<h2 id="対策とアップデートの進め方">対策とアップデートの進め方</h2>

<p>対応は各社の修正版へのアップデートが前提になるが、それだけでは今回の構造的な問題を完全には解消しない。</p>

<p>・<strong>バージョンアップデート</strong>：Claude Codeは2.1.163以上、Gemini CLIは0.39.1／0.40.0-preview.3以上（run-gemini-cliは0.1.22以上）へ更新する。OpenAI Codexは対応するパッチが無いため、後述のワークフロー変更を自分の運用に反映する<br />
・<strong>CIワークフローの点検</strong>：<code class="language-plaintext highlighter-rouge">on: issues</code> のようなIssueトリガーと、シークレットへのアクセスが同一ジョブ内に同居していないかを確認する。同居している場合は、Issue本文を処理するジョブとシークレットを使うジョブを分離し、前者の出力を後者が無条件に信頼しないようにする<br />
・<strong>許可リストの粒度を見直す</strong>：WebFetch等の許可リストを「ドメイン単位」ではなく、必要なパス・リクエスト形式まで絞り込めないか確認する。ドメインが正当でも、パス制限が無ければ帯域外持ち出しの通信路になりうる<br />
・<strong>ヘッドレスモード・自動実行フラグの扱いを見直す</strong>：Gemini CLIの <code class="language-plaintext highlighter-rouge">--yolo</code> のような、確認を省略するフラグをCI環境で常用していないか確認する<br />
・<strong>「instruction filesは信頼できない入力」という前提を運用に反映する</strong>：OpenAIが採用したこの原則は、他社のエージェントを使う場合にも応用できる。Issue本文・PRコメント等、外部から書き込まれるテキストをエージェントに読ませる箇所では、そのテキストを実行可能な指示として扱わない設計・レビュー運用を検討する</p>

<p>これらのワークアラウンドは、パッチ適用と違って自分のCI設定側の変更が必要になる。特にIssueトリガーからAIエージェントを起動する自動化を既に組んでいるプロジェクトは、ジョブ分離の見直しを優先度高く扱う価値がある。</p>

<h2 id="まとめaiコーディングエージェント-脆弱性から読み取れること">まとめ：AIコーディングエージェント 脆弱性から読み取れること</h2>

<p>GitHub Issueという「誰でも作れる入力」から、CI/CDが持つシークレットへ到達する攻撃チェーンが、Claude Code・Gemini CLI・OpenAI Codexという3社のAIコーディングエージェントで独立に成立していた。ベンダーの対応はAnthropicが自己申告でModerate評価、GoogleがCriticalとして即修正、OpenAIが脆弱性非認定という三者三様で、脆弱性認定の基準そのものがベンダーごとに異なることが分かる事例でもある。</p>

<div class="conclusion-box">
<strong>この記事のポイント</strong><br />
・Claude Code（CVE-2026-54316）・Gemini CLI（CVE-2026-12537／GHSA-wpqr-6v78-jr5g）・OpenAI Codex（未採番）で、GitHub Issue起点のCI/CDシークレット到達チェーンが独立に成立<br />
・Claude CodeはCVSSがNVD 9.1・Anthropic自己申告6.0で評価が乖離（食い違いの理由は未確認）<br />
・Gemini CLIはCVSS10.0でCritical認定・即修正。OpenAI CodexはCVE不採番で「仕様通り」の立場<br />
・自分の環境ではバージョン確認とCIワークフローの `on: issues`＋シークレット同居チェックを実行する<br />
・ハーネス（AIとシステムを仲介する設計）が事実上の防御境界であり、今後も同種の脆弱性の主戦場になりうる
</div>

<p>読者自身のプロジェクトでAIコーディングエージェントをCI/CDに組み込んでいる場合は、まずバージョンを確認し、次に本記事のコマンドでIssueトリガーとシークレットの同居を点検してほしい。</p>

<h2 id="参照ソース">参照ソース</h2>

<p>・<a href="https://thehackernews.com/2026/08/claude-code-and-gemini-cli-flaws-let.html">Claude Code and Gemini CLI Flaws Let a GitHub Issue Reach CI Workflow Secrets（The Hacker News, 2026-08-07）</a> — 3社横断の攻撃チェーンの概要・タイムライン<br />
・<a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-ai-coding-agent-cicd-secrets-20260808-csa/">Three AI Coding Agents, One GitHub Issue: CI/CD Secrets Exposed（Cloud Security Alliance, 2026-08-08）</a> — 技術詳細・CVE番号・修正版の一次情報<br />
・<a href="https://github.com/advisories/GHSA-wpqr-6v78-jr5g">Gemini CLI: Remote Code Execution via workspace trust and tool allowlisting bypasses（GHSA-wpqr-6v78-jr5g, GitHub Advisory Database）</a> — Gemini CLI側の公式アドバイザリ<br />
・<a href="https://advisories.gitlab.com/npm/@anthropic-ai/claude-code/CVE-2026-54316/">CVE-2026-54316: Claude Code Out-of-Band Data Exfiltration via Pre-Approved HuggingFace Domain in WebFetch（GitLab Advisory Database）</a> — Claude Code側の公式アドバイザリ</p>

<!--
Distribution memo（Step 8）
- X（08時台JST・画像必須）: 「Claude Code・Gemini CLI・OpenAI Codexの3社で、GitHub Issueを起点にCI/CDのシークレットへ到達する攻撃チェーンが独立に成立していた。ベンダーの対応温度差も三者三様」。添付画像: cover_ai-coding-agent-cicd-secrets-github-issue-attack.webp。
  セルフリプ（30分以内）: 「自分のCIワークフローの確認コマンドはこちら」＋ai-coding-agent-cicd-secrets-attack-chain.webpを添付。
- はてブ: 「CVSSの評価がNVDとAnthropicで9.1 vs 6.0に割れている」という温度差の切り口が刺さりそう。
- 内部リンク提案（受け側）: /security/claude-code-security-guide/ から本記事へ「CVE-2026-54316の深掘り記事」として1本追加可。
- 既報状況と差別化: 日本語では本件（3社横断＋GitHub Issue起点）を扱った記事は確認できず。英語一次資料（The Hacker News・CSA）のみの状態で、3社比較表と自環境確認コマンドを日本語で提供する点が差別化。
-->]]></content><author><name>編集部</name></author><category term="security" /><category term="security" /><category term="セキュリティ" /><category term="CVE" /><category term="Claude Code" /><category term="Gemini CLI" /><category term="OpenAI Codex" /><category term="CI/CD" /><summary type="html"><![CDATA[AIコーディングエージェント 脆弱性はClaude Code・Gemini CLI・OpenAI Codexの3社で共通していた。GitHub Issueを起点にCI/CDのシークレットへ到達する攻撃チェーンをタイムラインと自環境確認コマンドで解説する。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_ai-coding-agent-cicd-secrets-github-issue-attack.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_ai-coding-agent-cicd-secrets-github-issue-attack.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Anthropic Cybersecurity Skillsとは｜817個のセキュリティスキルをAIエージェントに</title><link href="https://ai-heartland.com/security/anthropic-cybersecurity-skills-library/" rel="alternate" type="text/html" title="Anthropic Cybersecurity Skillsとは｜817個のセキュリティスキルをAIエージェントに" /><published>2026-08-18T09:00:00+09:00</published><updated>2026-08-18T09:00:00+09:00</updated><id>https://ai-heartland.com/security/anthropic-cybersecurity-skills-library</id><content type="html" xml:base="https://ai-heartland.com/security/anthropic-cybersecurity-skills-library/"><![CDATA[<p>AIコーディングエージェントに侵入テストやフォレンジックの手順を聞いても、汎用LLMの知識では「どのツールを、どういう手順で、何を確認しながら使うか」までは踏み込めないことが多い。<strong>Anthropic Cybersecurity Skills</strong>は、この空白を817個の構造化スキルで埋めるコミュニティ製OSSだ。AIエージェント セキュリティ スキルという交差領域に特化し、MITRE ATT&amp;CKなど6つの業界フレームワークへスキルをマッピングしている点が最大の特徴になる。</p>

<figure>
  <img loading="eager" src="/generated_images/readme/anthropic-cybersecurity-skills-banner.webp" alt="Anthropic Cybersecurity Skills 公式バナー。盾のアイコンとセキュリティ関連のアイコン群" style="width:100%;height:auto;border-radius:.6rem;border:1px solid rgba(0,0,0,.08)" />
  <figcaption>Anthropic Cybersecurity Skills の公式バナー（出典: mukul975/Anthropic-Cybersecurity-Skills 公式README）</figcaption>
</figure>

<div class="point-box"><strong>30秒でわかる Anthropic Cybersecurity Skills（2026年8月時点）</strong><ul>
<li>・<strong>正体</strong>：mukul975氏によるコミュニティ製OSS。<strong>Anthropic PBC非公式</strong>（README末尾に明記）</li>
<li>・<strong>何ができる</strong>：29のセキュリティドメイン・817個のスキル（Markdown+YAML）をAIエージェントに読み込ませ、MITRE ATT&amp;CK等6フレームワークに沿った実務ワークフローを実行させる</li>
<li>・<strong>何を解決する</strong>：AIエージェントが「どのツールを、いつ、どう使うか」という現場の暗黙知を持たない問題</li>
<li>・<strong>実測</strong>：<code>git clone</code>後に<code>ls skills/ | wc -l</code>を実行し、README記載の「817スキル」がmain・最新タグv1.3.0の両方と一致することを確認</li>
<li>・<strong>注意</strong>：レッドチームC2やフィッシング模擬など攻撃的技術を含むため、認可された環境でのみ使用する</li>
</ul></div>

<p>サプライチェーン攻撃への防御スキルも29ドメインの1つとして含まれるが、対策の全体像は <a href="/security/supply-chain-security-guide-2026/">サプライチェーンセキュリティ2026｜攻撃手法・防御ツール・実践チェックリスト</a> を参照してほしい。本記事はAIエージェント向けスキル集という切り口に絞って解説する。</p>

<h2 id="anthropic-cybersecurity-skillsとは817個のセキュリティスキルをaiエージェントに渡すコミュニティ製oss">Anthropic Cybersecurity Skillsとは｜817個のセキュリティスキルをAIエージェントに渡すコミュニティ製OSS</h2>

<p>Anthropic Cybersecurity Skills（GitHub: <code class="language-plaintext highlighter-rouge">mukul975/Anthropic-Cybersecurity-Skills</code>）は、★28,361・フォーク3,445（実測時点）のリポジトリで、2026-02-25の作成から5.5ヶ月弱でこの規模まで成長した。ライセンスはApache-2.0で、READMEバッジの宣言とLICENSEファイル原文が一致していることを確認済みだ。</p>

<div class="box-warning">
<strong>⚠️ Anthropic公式ではない</strong><br />
プロジェクト名に「Anthropic」を含むため誤解されやすいが、README末尾に "Community project by @mukul975. Not affiliated with Anthropic PBC." と明記されている。Anthropic社が開発・保証しているツールではない。
</div>

<p>正体は実行可能なコードベースではなく、<strong>agentskills.io標準</strong>に準拠したMarkdown+YAMLのナレッジ／プロンプト資産集だ。1スキルは<code class="language-plaintext highlighter-rouge">SKILL.md</code>（YAML frontmatter＋手順本文）と、<code class="language-plaintext highlighter-rouge">references/</code>（標準マッピング・詳細手順）・<code class="language-plaintext highlighter-rouge">scripts/</code>（補助スクリプト）・<code class="language-plaintext highlighter-rouge">assets/</code>（チェックリストのテンプレート）で構成される。README曰く、1スキルはfrontmatterのスキャンだけなら約30トークン、全文読み込みでも500〜2,000トークンで済むよう設計されており、817スキル全体を一度にスキャンしてもコンテキストを圧迫しにくい「progressive disclosure」構造になっている。</p>

<div class="box-tip">
<strong>読者の3つの問いへの答え</strong><br />
① <strong>何ができる</strong>：AIエージェントにセキュリティ実務のプレイブックを817本渡す ② <strong>何を解決する</strong>：AIエージェントが「どのツールを・いつ・どう使うか」という現場の暗黙知を持たない問題 ③ <strong>何を代替できる</strong>：汎用LLMの当てずっぽうな手順提案を、フレームワークに裏付けられた構造化ワークフローに置き換える
</div>

<p>対応プラットフォームも幅広い。AIコーディングアシスタントではClaude Code・GitHub Copilot・Cursor・Windsurf・Cline・Aider・Continue・Roo Code・Amazon Q Developer・Tabnine・Sourcegraph Cody・JetBrains AIが、CLIエージェントではOpenAI Codex CLI・Gemini CLIが、自律型エージェントではDevin・Replit Agent・SWE-agent・OpenHandsが、エージェントフレームワークではLangChain・CrewAI・AutoGen・Semantic Kernel・Haystack・Vercel AI SDKが対象で、agentskills.io標準に対応する任意のプラットフォームで動く（README「Compatible platforms」記載）。</p>

<h2 id="インストールと使い方実機で検証した2つの方法">インストールと使い方——実機で検証した2つの方法</h2>

<p>READMEの「Quick start」に載っている手順は2つ。実際にcloneして中身と手順を検証した。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 方法1: npx（README推奨）</span>
npx skills add mukul975/Anthropic-Cybersecurity-Skills

<span class="c"># 方法2: git clone（確実な代替手段）</span>
git clone https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
<span class="nb">cd </span>Anthropic-Cybersecurity-Skills
<span class="nb">ls </span>skills/ | <span class="nb">wc</span> <span class="nt">-l</span>   <span class="c"># → 817（README記載の数値と一致することを実測）</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">npx skills add</code>は、本記事の検証環境（非対話シェル）では<strong>入力待ちでハングし、タイムアウトした</strong>。npmレジストリ自体への到達は<code class="language-plaintext highlighter-rouge">npm view skills version</code>で正常（<code class="language-plaintext highlighter-rouge">1.5.22</code>が返る）ことを確認しているため、レジストリの問題ではなく、<code class="language-plaintext highlighter-rouge">skills</code>CLI（agentskills.io系の別プロジェクト）がインタラクティブなプロンプトを出す設計に起因すると見られる。CI・スクリプト等の非対話環境では、<strong><code class="language-plaintext highlighter-rouge">git clone</code>方式を確実なフォールバックとして使う</strong>のが安全だ。</p>

<p>もう一つ、README本文には明記されていないが、リポジトリには<code class="language-plaintext highlighter-rouge">.claude-plugin/marketplace.json</code>・<code class="language-plaintext highlighter-rouge">plugin.json</code>が同梱されており、Claude Codeのプラグイン形式でも追加できる設定になっている。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Claude Codeのプラグイン形式（設定ファイルから確認できる導入経路。README本文には未記載）</span>
/plugin marketplace add mukul975/Anthropic-Cybersecurity-Skills
/plugin <span class="nb">install </span>cybersecurity-skills@anthropic-cybersecurity-skills
</code></pre></div></div>

<p>Claude Codeでこのcybersecurity skillsを運用する場合、claude code セキュリティの観点では「どのスキルが読み込まれ、どのコマンドを実行しようとしているか」を都度確認できる対話的な承認フローを維持したまま使うのが安全だ。攻撃的なワークフローを含むスキル集である以上、自動承認モードで無人実行させるのは避け、Workflowセクションのステップごとに人間が確認する運用が現実的になる。</p>

<p>インストール後、AIエージェントがスキルをどう選んで実行するかはREADMEに具体例がある。「メモリダンプから資格情報窃取の痕跡を分析して」というプロンプトに対し、エージェントは817個のfrontmatterを走査して関連スキルを絞り込み、上位候補を全文ロードして<code class="language-plaintext highlighter-rouge">Workflow</code>セクションの手順を実行し、<code class="language-plaintext highlighter-rouge">Verification</code>セクションで結果を検証する、という流れだ。</p>

<div class="mermaid">
flowchart TD
    A["ユーザーのプロンプト<br />例：メモリダンプを分析して"] --&gt; B["817個のfrontmatterを走査<br />(~30トークン/件)"]
    B --&gt; C["関連スキルを数件に絞り込み"]
    C --&gt; D["上位スキルを全文ロード<br />(500〜2,000トークン/件)"]
    D --&gt; E["Workflowセクションを<br />ステップ実行"]
    E --&gt; F["Verificationセクションで<br />結果を検証・ATT&amp;CK等へマッピング"]
</div>

<h2 id="6フレームワーク29ドメイン817スキルの中身">6フレームワーク・29ドメイン——817スキルの中身</h2>

<p>817スキルは29のセキュリティドメインに分かれ、クラウドセキュリティ（66）・脅威ハンティング（58）・脅威インテリジェンス（52）が上位を占める。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/anthropic-cybersecurity-skills-domains.webp" alt="29ドメインのうちスキル数が多い上位7ドメインの棒グラフ。クラウドセキュリティ66・脅威ハンティング58・脅威インテリジェンス52など" />
  <figcaption>スキル数が多い上位7ドメイン（出典: 公式README「What's inside — 29 security domains」表の実数値）</figcaption>
</figure>

<p>各スキルは、該当する範囲だけ6つのフレームワークにマッピングされる。フォレンジック系のスキルはMITRE ATT&amp;CK＋NIST CSFの2つで足りる一方、AIセキュリティ系のスキルはそこにMITRE ATLAS・NIST AI RMFも加わる、という設計だ。817スキル全体でのフレームワーク別カバレッジはMITRE ATT&amp;CK 805件・NIST CSF 2.0 804件・MITRE D3FEND 139件・NIST AI RMF 97件・MITRE F3 94件・MITRE ATLAS 93件（いずれもREADME記載、v19.1/2.0/v1.4.0/1.0/v1.1/2026.07という各フレームワークのバージョン時点の数値）。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/anthropic-cybersecurity-skills-frameworks.webp" alt="6つのフレームワーク（MITRE ATT&amp;CK・NIST CSF 2.0・MITRE D3FEND・NIST AI RMF・MITRE F3・MITRE ATLAS）ごとのマッピング数" />
  <figcaption>817スキルが対応する6フレームワークとマッピング数（出典: 公式README「Six frameworks, one skill library」）</figcaption>
</figure>

<p>上位7ドメイン以外の内訳も含め、29ドメイン全体の実数値を一覧にしておく（出典はいずれも公式README「What’s inside」表）。</p>

<table>
  <thead>
    <tr>
      <th>ドメイン</th>
      <th>スキル数</th>
      <th>ドメイン</th>
      <th>スキル数</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>クラウドセキュリティ</td>
      <td>66</td>
      <td>インシデントレスポンス</td>
      <td>26</td>
    </tr>
    <tr>
      <td>脅威ハンティング</td>
      <td>58</td>
      <td>脆弱性管理</td>
      <td>25</td>
    </tr>
    <tr>
      <td>脅威インテリジェンス</td>
      <td>52</td>
      <td>ペネトレーションテスト</td>
      <td>21</td>
    </tr>
    <tr>
      <td>ネットワークセキュリティ</td>
      <td>43</td>
      <td>DevSecOps</td>
      <td>18</td>
    </tr>
    <tr>
      <td>Webアプリセキュリティ</td>
      <td>42</td>
      <td>ゼロトラストアーキテクチャ</td>
      <td>17</td>
    </tr>
    <tr>
      <td>デジタルフォレンジック</td>
      <td>41</td>
      <td>エンドポイントセキュリティ</td>
      <td>17</td>
    </tr>
    <tr>
      <td>マルウェア解析</td>
      <td>39</td>
      <td>暗号技術</td>
      <td>16</td>
    </tr>
    <tr>
      <td>ID・アクセス管理</td>
      <td>37</td>
      <td>フィッシング防御</td>
      <td>15</td>
    </tr>
    <tr>
      <td>SOC運用</td>
      <td>35</td>
      <td>AIセキュリティ</td>
      <td>14</td>
    </tr>
    <tr>
      <td>レッドチーミング</td>
      <td>33</td>
      <td>モバイルセキュリティ</td>
      <td>13</td>
    </tr>
    <tr>
      <td>コンテナセキュリティ</td>
      <td>33</td>
      <td>ランサムウェア対策</td>
      <td>13</td>
    </tr>
    <tr>
      <td>セキュリティオペレーション</td>
      <td>28</td>
      <td>コンプライアンス・ガバナンス</td>
      <td>9</td>
    </tr>
    <tr>
      <td>OT/ICSセキュリティ</td>
      <td>28</td>
      <td>サプライチェーンセキュリティ</td>
      <td>8</td>
    </tr>
    <tr>
      <td>APIセキュリティ</td>
      <td>28</td>
      <td>欺瞞技術（Deception）</td>
      <td>6</td>
    </tr>
    <tr>
      <td>—</td>
      <td>—</td>
      <td>ハードウェア・ファームウェア</td>
      <td>4</td>
    </tr>
  </tbody>
</table>

<p>MITRE ATT&amp;CKの戦術別では、Initial Access（467）・Privilege Escalation（464）・Persistence（444）・Stealth（旧Defense Evasion、442）にスキルが集中しており、侵入後の権限昇格・永続化・防御回避という「攻撃が長期化する局面」を手厚くカバーしていることが分かる。</p>

<p>1スキルの実体は、READMEに掲載されている実例のYAML frontmatterで具体的にイメージできる。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">performing-memory-forensics-with-volatility3</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;-</span>
  <span class="s">Analyze memory dumps to extract running processes, network connections,</span>
  <span class="s">injected code, and malware artifacts using the Volatility3 framework.</span>
<span class="na">domain</span><span class="pi">:</span> <span class="s">cybersecurity</span>
<span class="na">subdomain</span><span class="pi">:</span> <span class="s">digital-forensics</span>
<span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">forensics</span><span class="pi">,</span> <span class="nv">memory-analysis</span><span class="pi">,</span> <span class="nv">volatility3</span><span class="pi">,</span> <span class="nv">incident-response</span><span class="pi">,</span> <span class="nv">dfir</span><span class="pi">]</span>
<span class="na">atlas_techniques</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">AML.T0047</span><span class="pi">]</span>
<span class="na">d3fend_techniques</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">D3-MA</span><span class="pi">,</span> <span class="nv">D3-PSMD</span><span class="pi">]</span>
<span class="na">nist_ai_rmf</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">MEASURE-2.6</span><span class="pi">]</span>
<span class="na">nist_csf</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">DE.CM-01</span><span class="pi">,</span> <span class="nv">RS.AN-03</span><span class="pi">]</span>
<span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1.2"</span>
<span class="na">author</span><span class="pi">:</span> <span class="s">mukul975</span>
<span class="na">license</span><span class="pi">:</span> <span class="s">Apache-2.0</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>本文側は<code class="language-plaintext highlighter-rouge">## When to Use</code>（発動条件）・<code class="language-plaintext highlighter-rouge">## Prerequisites</code>（必要ツール・権限）・<code class="language-plaintext highlighter-rouge">## Workflow</code>（実行手順）・<code class="language-plaintext highlighter-rouge">## Verification</code>（成功確認）という4セクションで統一されている。</p>

<p>開発のリリースタイムラインも実測しておく。リポジトリ作成は2026-02-25で、v1.0.0（2026-03-11公開・734スキル／26ドメイン）→v1.1.0（2026-03-21公開・753スキル）→v1.2.0（2026-04-06公開・5フレームワーク対応）→v1.3.0（2026-06-22公開・現行の6フレームワーク体制）という4リリースを経ている。最新タグv1.3.0時点のスキル数をGitHub API（<code class="language-plaintext highlighter-rouge">git/trees</code>）で数えたところ817件で、mainブランチの<code class="language-plaintext highlighter-rouge">git clone</code>実測値と一致した——READMEの「817」という数値はmain・最新タグのどちらでも裏付けが取れている。</p>

<h2 id="aiエージェント自身のセキュリティを守るスキルもmcpサーバー監査を例に">AIエージェント自身のセキュリティを守るスキルも——MCPサーバー監査を例に</h2>

<p>29ドメインには「AIセキュリティ」（14スキル）という区分があり、AIエージェント自身の攻撃対象領域（プロンプトインジェクション・MCPサーバーの脆弱性など）を扱うスキルも含まれる。代表例が<code class="language-plaintext highlighter-rouge">auditing-mcp-servers-for-tool-poisoning</code>だ。</p>

<p>このスキルは、MCPサーバーが提供するツールの「description」フィールドに悪意ある指示を仕込む<strong>tool poisoning攻撃</strong>（OWASP MCP03:2025）や、ツールの挙動を後から差し替える<strong>tool shadowing</strong>、承認後に説明文を変更する<strong>rug pull</strong>、SSRFや未認証公開といったMCPサーバー特有のリスクを、Invariant Labsの<code class="language-plaintext highlighter-rouge">mcp-scan</code>による静的・動的解析と手動チェックで監査する手順をまとめている。frontmatterには<code class="language-plaintext highlighter-rouge">nist_ai_rmf: [MANAGE-2.2]</code>・<code class="language-plaintext highlighter-rouge">atlas_techniques: [AML.T0010]</code>が付与されており、MCPサーバーをエージェントスタックに追加する前・内部MCPサーバーのレビュー時・rug pull検知時・CI/CDゲートとしての利用を想定している。</p>

<p>MCPのトランスポート層自体の設計欠陥については <a href="/security/anthropic-mcp-stdio-rce-vulnerability/">MCP脆弱性！STDIOトランスポートの設計欠陥で20万台のサーバーがRCEの危険に——OX Securityが警告</a> で詳しく扱っているので、あわせて参照してほしい。AIエージェントに攻撃側のペンテスト実務を持たせる方向のOSSとしては、実際にPoCで裏取りしながら攻撃するタイプの <a href="/security/strix-ai-penetration-testing/">Strixとは｜AIが実際に攻めてPoCで裏取りするOSSペンテスターを実測で解説</a> との組み合わせも相性がよい。</p>

<h2 id="汎用claude-skillsカタログとの違いと実体評価">汎用Claude Skillsカタログとの違いと実体評価</h2>

<p>当サイトには汎用的なClaude Skillsのカタログ・解説記事が複数あるが、それらとAnthropic Cybersecurity Skillsは主題が明確に異なる。汎用カタログは「スキルという仕組み自体の解説・幅広い収集」が主眼で、セキュリティは数あるジャンルの1つに過ぎない。対してAnthropic Cybersecurity Skillsは、817スキル全てをセキュリティ29ドメインと6フレームワークに体系化した専門特化型だ。「スキル」という仕組みそのものの解説は <a href="/explain/claude-skills-explained/">Claude Skillsとは｜「スキル=フォルダ」の仕組みと作り方・使い方を徹底解説</a> を参照してほしい。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/anthropic-cybersecurity-skills-vs-generic.webp" alt="汎用Claude Skillsカタログ記事とAnthropic Cybersecurity Skillsの違いを対比した図" />
  <figcaption>汎用スキルカタログとの違い。セキュリティ29ドメイン特化・6フレームワークマッピング・実務ワークフローが差別化点</figcaption>
</figure>

<p>セキュリティ領域の類似プロジェクトとの比較は次の通り（既存記事から転記可能な情報のみ・裏取りできていない項目は「未確認」と明記する）。</p>

<table>
  <thead>
    <tr>
      <th>比較対象</th>
      <th>ライセンス</th>
      <th>⭐</th>
      <th>特徴・本OSSとの違い</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>composiohqawesome-claude-skills</td>
      <td>未確認（既存記事から転記時に要確認）</td>
      <td>未確認</td>
      <td>汎用スキルのキュレーションリスト。セキュリティ特化ではない</td>
    </tr>
    <tr>
      <td>open-skills</td>
      <td>未確認（既存記事から転記時に要確認）</td>
      <td>未確認</td>
      <td>汎用スキルの収集・管理ツール</td>
    </tr>
    <tr>
      <td>Anthropic Cybersecurity Skills（本記事）</td>
      <td>Apache-2.0</td>
      <td>28,361</td>
      <td>セキュリティ29ドメイン・817スキルを6フレームワークにマッピング。Claude Code・GitHub Copilot・Codex CLI・Cursor・Gemini CLI等26以上のプラットフォームに対応</td>
    </tr>
  </tbody>
</table>

<p>実体評価として、star数の裏側も見ておく。contributor数はGitHub API実測で10人だが、開発者mukul975氏が167コミット、2位のvalorisa氏でも18コミットに留まり、<strong>実質的なバス係数は1</strong>だ。スキル定義集という性質上、コード規模の割にコミットが1人に偏るのは珍しくないが、企業サポートが無い個人プロジェクトである点は正直に書いておく。一方で、star数だけの「バイラル」ではないことを裏付ける材料もある。awesome-agent-skills・awesome-ai-security・SkillsLLM・NeverSight skills_feedなど複数の外部スキルカタログ・キュレーションサイトに実際に掲載されており（README「Featured in」記載）、単発の話題性で終わっていない。</p>

<div class="marker-yellow">実質的な開発者は1人（バス係数1）だが、複数の外部カタログサイトに横断的に採用されている点は、単なるバイラルとは違う実体の裏付けとして評価できる。</div>

<p>README「What people are saying」には第三者からの評価も掲載されている。AI/技術系クリエイターのHasan Toor氏（@hasantoxr）は「AIエージェントがそのまま使える、本物の・整理されたセキュリティスキルのデータベース。チュートリアルでもブログ記事でもない」とコメントし、Medium執筆者のfazal-sec氏も「セキュリティスクリプトの寄せ集めではなく、AI駆動のセキュリティワークフロー向けに設計された構造化ナレッジベースだ」と評している（いずれもREADME掲載の引用）。コントリビューションは活発で、CONTRIBUTING.mdによれば新規スキルの追加はドメインテンプレートに沿ってPRを送る形式で受け付けており、Deception Technology（当時2スキル）・Compliance &amp; Governance（当時5スキル）など手薄なドメインへの貢献を呼びかけている。PRは48時間以内にレビューされる運用で、実質1人が主導しつつもコミュニティからの追加を取り込む体制はある。</p>

<p>導入前に確認すべき対策も明記しておく。このリポジトリはレッドチームC2・フィッシング模擬・エクスプロイトなど攻撃的・デュアルユース技術を含むため、開発元は「認可された環境（自己所有システムまたは書面での許可があるシステム）でのみ使用し、適用法令を遵守すること」を明記している。影響範囲・利用対象は認可されたペネトレーションテスト・セキュリティ研究・教育に限られ、無許可の第三者システムへ向けて使うことは想定されていない。導入時はまずCTF環境や自社所有の検証環境で試し、本番のインシデント対応やレッドチーム演習に組み込む前に、社内の利用規程・法務確認を挟むのが妥当な対策になる。</p>

<p>自律型ペネトレーションテストの標準化という文脈では <a href="/security/owasp-apts-autonomous-pentesting-standard/">OWASP APTS｜AIエージェント時代の自律型ペネトレーションテスト基準を読む</a> も参考になる。AIエージェントにセキュリティ実務のスキルを持たせるという方向性は共通しており、標準化の動きと個別OSSの実装を突き合わせて読むと理解が深まる。</p>

<h2 id="まとめ導入前に確認すべきこと">まとめ｜導入前に確認すべきこと</h2>

<div class="conclusion-box">
Anthropic Cybersecurity Skillsは、817個のセキュリティスキルをMITRE ATT&amp;CKなど6フレームワークに体系的にマッピングし、Claude Codeを含む26以上のAIエージェント環境へコマンド一発で追加できるコミュニティ製OSSだ。<br />
・導入は<code>git clone</code>が確実（<code>npx skills add</code>は非対話環境でハングする挙動を実機確認）<br />
・817という数値はmain・最新タグv1.3.0の両方で実測一致<br />
・攻撃的技術を含むため、認可された環境限定で使う前提を忘れない<br />
・開発者は実質1人（バス係数1）で、Anthropic非公式であることも導入判断の材料にしてほしい
</div>

<h2 id="参照ソース">参照ソース</h2>

<p>・<a href="https://github.com/mukul975/Anthropic-Cybersecurity-Skills">mukul975/Anthropic-Cybersecurity-Skills（公式リポジトリ・README）</a> — スキル構成・フレームワークマッピング・インストール手順・ライセンスを取得<br />
・<a href="https://mahipal.engineer/Anthropic-Cybersecurity-Skills/">Anthropic-Cybersecurity-Skills 公式サイト</a> — プロジェクト概要の一次情報</p>

<!--
Distribution memo（Step 8）
- X（08時台JST・画像必須）: 「AIエージェントにMITRE ATT&CKなど6フレームワーク×817個のセキュリティスキルを渡すOSS」を主フックに。添付画像: cover_anthropic-cybersecurity-skills-library.webp。
  セルフリプ（30分以内）: 「npx skills add は非対話環境でハングしたのでgit clone推奨、という実機検証結果」を補足。
- はてブ: 「Anthropic非公式」「バス係数1」という実体評価の角度が刺さりそう。
- 内部リンク提案（受け側）: /security/strix-ai-penetration-testing/ や /security/anthropic-mcp-stdio-rce-vulnerability/ から本記事へ「関連スキル集」として1本ずつリンクを検討。
- 既報状況と差別化: 日本語記事は未確認（2026-08-18時点、検索1-10位はtrendshift.io等の英語カタログサイトのみ）。本記事が国内初の日本語解説と見られる。
-->]]></content><author><name>編集部</name></author><category term="security" /><category term="セキュリティ" /><category term="ペンテスト" /><category term="MITRE ATT&amp;CK" /><category term="MCP" /><category term="Claude Skills" /><summary type="html"><![CDATA[Anthropic Cybersecurity Skills（mukul975・★28,361）は、AIエージェント セキュリティ スキルをMITRE ATT&CKなど6フレームワークに体系的にマッピングして渡すOSS。817スキルの中身とインストール手順を実機検証で解説する非公式解説記事。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_anthropic-cybersecurity-skills-library.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_anthropic-cybersecurity-skills-library.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">さくらのレンタルサーバ不正アクセスとは｜583アカウントの意味と、利用者が今すぐ確認すべきこと</title><link href="https://ai-heartland.com/security/sakura-rental-server-unauthorized-access/" rel="alternate" type="text/html" title="さくらのレンタルサーバ不正アクセスとは｜583アカウントの意味と、利用者が今すぐ確認すべきこと" /><published>2026-08-18T09:00:00+09:00</published><updated>2026-08-18T00:00:00+09:00</updated><id>https://ai-heartland.com/security/sakura-rental-server-unauthorized-access</id><content type="html" xml:base="https://ai-heartland.com/security/sakura-rental-server-unauthorized-access/"><![CDATA[<p>さくらのレンタルサーバ不正アクセス——さくらインターネットが2026年8月17日に公表したこの事案は、報道の見出しだけを追うと「583アカウントが不正ログインされた」という数字の話に見える。だが公式発表を読むと、この事案の性質を決めているのは583という数ではなく、<strong>第三者が「当社管理環境を経由して」顧客環境に到達したという一文</strong>のほうだ。利用者が自分の側で鍵を固くしていても防げない層で起きている。本記事では公式発表と総務省・個人情報保護委員会の一次資料だけを使って、①この数字が何を数えたものなのか、②影響なしと書かれたサービスとの境界はどこか、③FreeBSDである「さくらのレンタルサーバ」で実際に通る確認コマンドは何か、④この1件に対して並走している2つの報告義務は何か、を整理する。</p>

<figure>
<img src="/generated_images/sections/sakura-incident-timeline.png" alt="さくらのレンタルサーバ不正アクセスの時系列：2026年8月9日の異常検知から8月17日の公表まで8日間、不正ログイン583アカウント、マルウェア設置確認、侵入経路は調査中" loading="lazy" width="1280" height="320" />
<figcaption>公式発表に明記された日付だけで引いた時系列。侵入経路と発生時期は「調査を進めております」の段階で、確定していない。出典: <a href="https://www.sakura.ad.jp/corporate/information/newsreleases/2026/08/17/1968225614/">当社レンタルサーバーサービスの一部環境に対する不正なアクセスについて（さくらインターネット）</a></figcaption>
</figure>

<div class="point-box"><strong>30秒でわかる さくらのレンタルサーバ不正アクセス（2026年8月17日発表時点）</strong><ul>
<li>・<strong>何が起きた</strong>：第三者が<strong>さくらインターネットの管理環境を経由して</strong>「さくらのレンタルサーバ」の一部顧客環境へ不正アクセス。一部サーバーに<strong>マルウェアの設置</strong>を確認。</li>
<li>・<strong>583という数字</strong>：不正ログインが判明したアカウント数。<strong>破られたパスワードの数ではない</strong>。影響を受けた可能性のある範囲は調査継続中。</li>
<li>・<strong>漏えいの可能性</strong>：「顧客領域内に保存された情報」と「利用者識別子」。加えて<strong>通信の秘密に該当する情報</strong>へアクセス可能な状態だったと明記。</li>
<li>・<strong>影響が確認されていない</strong>：さくらのVPS／さくらのクラウド／さくらの専用サーバ PHY／高火力 PHY。ただし「確認を進めております」であって安全確定ではない。</li>
<li>・<strong>利用者への要求</strong>：現時点で<strong>一律のパスワード変更要請は出ていない</strong>。対象者には個別メール通知。便乗フィッシングに注意。</li>
<li>・<strong>公表されていないもの</strong>：侵入経路、発生時期、漏えいした情報の具体的内容。いずれも調査中。</li>
</ul></div>

<blockquote>
  <p>開発者を狙う供給網まわりの攻撃と防御の全体像は <a href="/security/supply-chain-security-guide-2026/">サプライチェーンセキュリティ2026｜攻撃手法・防御ツール・実践チェックリスト</a> をご覧ください。</p>
</blockquote>

<h2 id="さくらのレンタルサーバ不正アクセスの時系列検知から公表まで8日">さくらのレンタルサーバ不正アクセスの時系列——検知から公表まで8日</h2>

<p>公式発表から日付を拾うと、確定しているのは2点しかない。<strong>2026年8月9日（日）に同社が管理するサーバー環境で異常を検知し調査を開始</strong>したこと、そして<strong>8月17日（月）に公表</strong>したことだ。この間隔は8日である。</p>

<p>その間に何が判明したかは発表に順序立てて書かれている。異常検知から始まった調査によって、第三者が同社管理環境を経由して「さくらのレンタルサーバ」の一部顧客環境へ不正アクセスを行っていたことが確認された。さらに一部のサーバーにマルウェアが設置されていたこと、攻撃者が顧客情報および通信の秘密に該当する情報へアクセス可能な状態であったことが確認されている。同社は確認後直ちに認証情報の無効化とアクセス遮断、その他の封じ込め対応を実施したと記載している。</p>

<p>ここで注意しておきたいのは、<strong>8月9日は「異常を検知した日」であって、「不正アクセスが始まった日」ではない</strong>という点だ。発表は発生時期についても調査中としている。攻撃者がいつから同社管理環境にいたのかは、現時点で公表されていない。時系列を読むときにこの2つを混同すると、被害期間を実際より短く見積もることになる。</p>

<p>・<strong>確定している日付</strong>：8月9日（異常検知・調査開始）、8月17日（公表）<br />
・<strong>確定していない日付</strong>：侵入の開始時期、マルウェアが設置された時期、顧客環境へ到達した時期<br />
・<strong>判明済みの事実</strong>：管理環境経由の侵入、マルウェア設置、583アカウントへの不正ログイン、通信の秘密該当情報へのアクセス可能状態</p>

<h2 id="当社管理環境を経由して583アカウントはパスワードが弱かった話ではない">「当社管理環境を経由して」——583アカウントはパスワードが弱かった話ではない</h2>

<p>この事案でもっとも読み違えられやすいのが583という数字だ。レンタルサーバーで「N件の不正ログイン」と聞くと、通常は利用者側の認証情報が破られた話——パスワードリスト攻撃や総当たり、あるいは利用者のPCからの認証情報窃取——を思い浮かべる。その図式なら、対策は利用者側にある。強いパスワードにする、二要素認証を入れる、という話になる。</p>

<p>しかし公式発表の書き方はそうなっていない。<strong>「第三者が当社管理環境を経由して『さくらのレンタルサーバ』の一部お客さま環境へ不正アクセスを行っていたことを確認しました」</strong>と書かれている。侵入の起点は利用者のアカウントではなく、事業者側の管理環境だ。</p>

<figure>
<img src="/generated_images/sections/sakura-intrusion-model.png" alt="583アカウントの誤読されやすい図式と公式発表が書いている図式の比較。利用者のパスワードが破られたのではなく、当社管理環境を経由して顧客環境へ到達した" loading="lazy" width="1280" height="430" />
<figcaption>左が見出しから読まれがちな図式、右が公式発表の文言に沿った図式。侵入経路そのものは「調査を継続」とされ公表されていない。出典: <a href="https://www.sakura.ad.jp/corporate/information/newsreleases/2026/08/17/1968225614/">さくらインターネット公式発表</a></figcaption>
</figure>

<p>この違いは、利用者にとって実務的な意味を持つ。共有ホスティングでは、利用者が触れる領域の外側に、事業者だけが触れる管理層がある。プラン変更、データベースの作成や移行、バックアップ、アカウントの発行と停止——こうした操作は、個々の顧客の権限では実行できない。裏返せば、その層は<strong>すべての顧客環境に対して横断的な権限を持っている</strong>。管理層が侵害された場合、そこから見える顧客環境の数は、利用者側の設定の堅牢さとは独立に決まる。</p>

<div class="mermaid">
flowchart TB
  A["攻撃者"] --&gt; B["さくらインターネットの管理環境<br />（利用者からは操作できない層）"]
  B --&gt; C["顧客環境 A"]
  B --&gt; D["顧客環境 B<br />（583アカウントに不正ログイン）"]
  B --&gt; E["顧客環境 C"]
  F["利用者が管理できる範囲<br />パスワード・公開鍵・ファイル・CMS"] -.-&gt;|"この層の強化では<br />上流の侵害を防げない"| C
  F -.-&gt; D
  F -.-&gt; E
  style B fill:#dc2626,color:#fff
  style D fill:#f59e0b,color:#fff
  style F fill:#0070f3,color:#fff
</div>

<p>だからこの事案において、583は「583人が弱いパスワードを使っていた」ことを意味しない。<strong>不正ログインが判明したアカウントの数</strong>であり、しかも同社は「影響を受けた可能性のあるお客さま環境について調査を継続しております」と注記している。確定値ではなく、現時点で判明している数だ。</p>

<p>同時に、これは「利用者にできることは何もない」という意味でもない。攻撃者が到達したあとに何を置いていったか——マルウェア、追加された管理者アカウント、書き換えられた設定——は利用者側の領域に残る。設置されたマルウェアの除去は同社が実施しているが、公式発表が利用者に対して「身に覚えのないファイルや管理者アカウントが追加されていないか」「ウェブサイトやアプリケーションに不審な変更が行われていないか」の確認を求めているのは、この残留物のためだ。後述する確認コマンドは、まさにこの層を見るためのものになる。</p>

<h2 id="影響が確認されていないサービスの境界線vpsクラウド高火力phyはなぜ分かれたか">影響が確認されていないサービスの境界線——VPS・クラウド・高火力PHYはなぜ分かれたか</h2>

<p>発表は、影響が確認されていないサービスを名指しで4つ挙げている。さくらのVPS、さくらのクラウド、さくらの専用サーバ PHY、高火力 PHY だ。ここには同社のGPUクラウド／AI基盤事業である高火力 PHY も含まれている。</p>

<figure>
<img src="/generated_images/sections/sakura-service-scope.png" alt="影響が確認されたさくらのレンタルサーバと、現時点で影響が確認されていないさくらのVPS・さくらのクラウド・専用サーバPHY・高火力PHYの一覧" loading="lazy" width="1280" height="430" />
<figcaption>公式発表に名指しで挙げられたサービスのみを記載。「影響が確認されていない」と「安全が確定した」は別の状態である点に注意。出典: <a href="https://www.sakura.ad.jp/corporate/information/newsreleases/2026/08/17/1968225614/">さくらインターネット公式発表</a></figcaption>
</figure>

<p>この境界には技術的な読み筋がある。挙げられた4サービスはいずれも、<strong>利用者が自分でOSを持つ</strong>タイプのサービスだ。VPSとクラウドは仮想マシンを、専用サーバ PHY と高火力 PHY は物理サーバーを、それぞれ利用者が管理する。事業者の管理層が触るのは、その外側——ハイパーバイザー、電源、ネットワーク、コントロールパネル——であって、OSの内側に日常的に手を入れる仕組みは持たない。</p>

<p>対してレンタルサーバーは共有型で、OSは事業者のものだ。利用者は与えられたディレクトリとデータベースの中で作業する。プラン変更もデータベース作成も、事業者の自動化が利用者の領域に踏み込んで実行する。<strong>事業者の管理層と顧客データの距離が、構造的に近い</strong>。</p>

<p>ただし、ここで踏み込みすぎないでおきたい。この読み筋は「なぜレンタルサーバーだけが影響を受けたのか」の説明としてはもっともらしいが、<strong>同社は侵入経路を公表していない</strong>。管理層の性質の違いが今回の分岐の原因だと断定することはできないし、発表もそう書いていない。確実に言えるのは次の2点だ。</p>

<p>・発表時点で影響が確認されているのは「さくらのレンタルサーバ」のみである<br />
・上記4サービスは「現時点において影響は確認されておりません」であり、これは調査完了の宣言ではない。同じ発表に「その他サービスについても必要に応じて影響有無の確認を進めております」と併記されている</p>

<div class="box-warning"><strong>「影響が確認されていない」の読み方</strong><br />
インシデント発表における「現時点で影響は確認されていない」は、「調べた結果いずれのサービスも安全だった」ではなく「現在までに得られた情報の範囲では影響を示す証拠が出ていない」という意味で使われる。調査が継続している段階では、後続の発表で範囲が広がることがある。他社サービスも含め、この語を安全宣言として受け取らないほうがよい。</div>

<h2 id="さくらのレンタルサーバ不正アクセス後に自分の環境を確認するコマンドosはfreebsd">さくらのレンタルサーバ不正アクセス後に自分の環境を確認するコマンド——OSはFreeBSD</h2>

<p>ここからが、公式発表を読むだけでは埋まらない部分だ。発表の「お客さまへのお願い」は散文で書かれている——「身に覚えのないファイルや管理者アカウントが追加されていないか」「ウェブサイトやアプリケーションに不審な変更が行われていないか」「心当たりのないログインやメール送信等が発生していないか」。何を見るべきかは分かるが、<strong>どう見るかは書かれていない</strong>。</p>

<h3 id="前提プランによって打てる手が違う">前提：プランによって打てる手が違う</h3>

<p>まず自分のプランを確認する必要がある。さくらインターネットの公式仕様によれば、<strong>ライトプランはSSH・データベース・cronのいずれも提供対象外</strong>だ。つまりライトプランの利用者は、以下のコマンドを1つも実行できない。</p>

<figure>
<img src="/generated_images/sections/sakura-plan-matrix.png" alt="ライトプランはSSH・データベース・cronいずれも利用不可、スタンダード以上はSSH利用可・データベース50個以上・cron10個までという比較" loading="lazy" width="1280" height="430" />
<figcaption>プランごとの確認手段の差。ライトプランはコマンドによる確認経路を持たない。出典: <a href="https://help.sakura.ad.jp/rs/2251/">基本仕様を知りたい（さくらのレンタルサーバ）</a></figcaption>
</figure>

<p>ライトプランの場合は、コントロールパネルのファイルマネージャー、またはFTPクライアントで接続し、<strong>ファイル一覧を更新日時の降順に並べ替えて</strong>見慣れないファイルがないかを目視する経路になる。ライトはデータベースを持たないためWordPressなどのCMSも動いておらず、確認対象は静的ファイルとメール周りに限られる。</p>

<h3 id="freebsdであることの落とし穴">FreeBSDであることの落とし穴</h3>

<p>スタンダード以上でSSHが使える場合も、そのままLinux向けの調査コマンドを貼り付けると動かない。<strong>さくらのレンタルサーバのOSはFreeBSD</strong>であり、<code class="language-plaintext highlighter-rouge">find</code>・<code class="language-plaintext highlighter-rouge">stat</code>・<code class="language-plaintext highlighter-rouge">grep</code>・チェックサム系のコマンドがGNU版とは別実装だからだ。インシデント対応の記事やチートシートはLinux（GNU coreutils）前提で書かれていることが多く、コピー＆ペーストすると次のように弾かれる。</p>

<figure>
<img src="/generated_images/sections/sakura-bsd-vs-gnu.png" alt="Linux前提のmd5sum・stat -c・find -printf・grep -P・tacがFreeBSDでは動かず、md5・stat -f・find -ls・grep -E・tail -rに置き換わることを示す比較表" loading="lazy" width="1280" height="430" />
<figcaption>左のGNU由来のコマンド・オプションはBSD userlandでは通らない。右が置き換え先。出典: 各コマンドのBSD実装（macOS/Darwin のBSD userlandで構文を確認）</figcaption>
</figure>

<div class="box-tip"><strong>この対応表の検証方法について</strong><br />
筆者はさくらのレンタルサーバの契約を持たないため、<strong>以下のコマンドを同サービス上で実行してはいない</strong>。左右の対応は、FreeBSDと同じBSD userlandを持つmacOS（Darwin）上で構文と終了コードを確認したものだ。<code>md5sum</code>・<code>tac</code> は command not found（exit 127）、<code>stat -c</code> は illegal option、<code>find -printf</code> は unknown primary、<code>grep -P</code> は invalid option で失敗し、右列はいずれも exit 0 で通ることを確認している。FreeBSDとDarwinで挙動が分かれる可能性が残る点は明記しておく。</div>

<h3 id="1-最近書き換えられたファイルを洗い出す">1. 最近書き換えられたファイルを洗い出す</h3>

<p>最初に見るのは更新日時だ。異常検知は8月9日だが、侵入時期は不明なので、期間は広めに取って絞り込む。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># www 配下で、指定日以降に「内容が」変更されたファイル（mtime）</span>
find ~/www <span class="nt">-type</span> f <span class="nt">-newermt</span> <span class="s2">"2026-07-01"</span> <span class="nt">-ls</span>

<span class="c"># 内容ではなく「メタデータが」変更されたファイル（ctime）。</span>
<span class="c"># 攻撃者が touch で mtime を偽装しても ctime は巻き戻せないため、</span>
<span class="c"># mtime と ctime の結果が食い違うファイルは優先して見る</span>
find ~/www <span class="nt">-type</span> f <span class="nt">-newerct</span> <span class="s2">"2026-07-01"</span> <span class="nt">-ls</span>

<span class="c"># 日付ではなく相対日数で見る場合（-mtime は GNU/BSD 共通で使える）</span>
find ~/www <span class="nt">-type</span> f <span class="nt">-mtime</span> <span class="nt">-30</span> <span class="nt">-exec</span> <span class="nb">ls</span> <span class="nt">-lT</span> <span class="o">{}</span> <span class="se">\;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-newermt</code> / <code class="language-plaintext highlighter-rouge">-newerct</code> はBSDの <code class="language-plaintext highlighter-rouge">find</code> が持つ <code class="language-plaintext highlighter-rouge">-newerXY</code> 形式で、<code class="language-plaintext highlighter-rouge">X</code> に比較する時刻の種類（m=更新、c=メタデータ変更）、<code class="language-plaintext highlighter-rouge">Y</code> に <code class="language-plaintext highlighter-rouge">t</code>（引数を時刻文字列として解釈）を指定する書き方だ。GNU由来の <code class="language-plaintext highlighter-rouge">-printf</code> は使えないので、整形が必要なら <code class="language-plaintext highlighter-rouge">-ls</code> か <code class="language-plaintext highlighter-rouge">-exec ls -lT</code> を使う。<code class="language-plaintext highlighter-rouge">ls -lT</code> はBSDの <code class="language-plaintext highlighter-rouge">ls</code> で年まで含む完全なタイムスタンプを表示するオプションになる。</p>

<h3 id="2-webシェルの典型パターンを探す">2. Webシェルの典型パターンを探す</h3>

<p>マルウェア設置が確認されている以上、PHPファイルに実行系の関数が仕込まれていないかを見る。BSDの <code class="language-plaintext highlighter-rouge">grep</code> はPCRE（<code class="language-plaintext highlighter-rouge">-P</code>）を持たないため、拡張正規表現 <code class="language-plaintext highlighter-rouge">-E</code> と POSIX 文字クラスで書く。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 難読化・動的実行の典型キーワードを含む PHP を列挙</span>
<span class="nb">grep</span> <span class="nt">-rl</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.php"</span> <span class="nt">-E</span> <span class="s2">"eval[[:space:]]*</span><span class="se">\(</span><span class="s2">|base64_decode|assert[[:space:]]*</span><span class="se">\(</span><span class="s2">|gzinflate|str_rot13|preg_replace[[:space:]]*</span><span class="se">\(</span><span class="s2">.*/e"</span> ~/www

<span class="c"># 画像ディレクトリに PHP が置かれていないか（正規の構成ではまず存在しない）</span>
find ~/www <span class="nt">-type</span> f <span class="nt">-name</span> <span class="s2">"*.php"</span> <span class="nt">-path</span> <span class="s2">"*upload*"</span>
find ~/www <span class="nt">-type</span> f <span class="nt">-name</span> <span class="s2">"*.php"</span> <span class="nt">-path</span> <span class="s2">"*image*"</span>

<span class="c"># .htaccess による拡張子の乗っ取りがないか</span>
find ~/www <span class="nt">-name</span> <span class="s2">".htaccess"</span> <span class="nt">-exec</span> <span class="nb">grep</span> <span class="nt">-l</span> <span class="s2">"AddType</span><span class="se">\|</span><span class="s2">AddHandler</span><span class="se">\|</span><span class="s2">php_value</span><span class="se">\|</span><span class="s2">auto_prepend_file"</span> <span class="o">{}</span> <span class="se">\;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.htaccess</code> の確認を入れているのは、<code class="language-plaintext highlighter-rouge">AddType application/x-httpd-php .png</code> のような1行で、画像に見えるファイルをPHPとして実行させる細工が成立するためだ。ファイル名だけを見ていると見落とす。</p>

<h3 id="3-永続化の足がかりを確認する">3. 永続化の足がかりを確認する</h3>

<p>侵入されたあと再侵入されないためには、置き土産のほうが重要になる。公開鍵とcronは代表的な永続化ポイントだ。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 自分が登録した覚えのない公開鍵が混ざっていないか（1行=1鍵）</span>
<span class="nb">cat</span> ~/.ssh/authorized_keys 2&gt;/dev/null

<span class="c"># 鍵ファイル自体がいつ変更されたか</span>
<span class="nb">ls</span> <span class="nt">-lT</span> ~/.ssh/ 2&gt;/dev/null

<span class="c"># cron に不審なエントリがないか（スタンダード以上・最大10個）</span>
crontab <span class="nt">-l</span> 2&gt;/dev/null
</code></pre></div></div>

<p>もうひとつ、コマンドではなく<strong>コントロールパネルで確認すべき残留物</strong>がある。<strong>メールの転送設定</strong>だ。さくらのレンタルサーバでは転送先の設定はコントロールパネル側（対象メールアドレスの「設定」→「詳細設定」→転送先アドレス）で管理されるため、シェルから見に行くのではなくパネル上で各アドレスの転送先を1つずつ確認する。</p>

<p>これを挙げているのは、発表が「通信の秘密に該当する情報」に言及しているためだ。転送設定は、一度仕込まれると以後の受信メールを継続的に外部へ流し続ける。パスワードを変えても消えない種類の残留物であり、しかもメール本文は通信の秘密そのものにあたる。優先度は高い。</p>

<h3 id="4-cmsの管理者アカウントと認証情報を洗う">4. CMSの管理者アカウントと認証情報を洗う</h3>

<p>WordPressなどを動かしている場合、追加された管理者アカウントの確認が要る。発表が明示的に「管理者アカウントが追加されていないか」を挙げている項目だ。データベースはスタンダード以上で利用できる。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># WordPress の管理者ユーザーを一覧（DB名・ユーザー名・接頭辞は wp-config.php に合わせる）</span>
mysql <span class="nt">-h</span> &lt;DBサーバ名&gt; <span class="nt">-u</span> &lt;DBユーザー&gt; <span class="nt">-p</span> &lt;DB名&gt; <span class="nt">-e</span> <span class="se">\</span>
  <span class="s2">"SELECT u.ID, u.user_login, u.user_email, u.user_registered
   FROM wp_users u
   JOIN wp_usermeta m ON u.ID = m.user_id
   WHERE m.meta_key = 'wp_capabilities' AND m.meta_value LIKE '%administrator%';"</span>

<span class="c"># サーバー上に平文で置かれている認証情報の在処を洗い出す</span>
find ~/www <span class="nt">-type</span> f <span class="se">\(</span> <span class="nt">-name</span> <span class="s2">"wp-config.php"</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s2">".env"</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s2">"*.ini"</span> <span class="se">\)</span> <span class="nt">-exec</span> <span class="nb">ls</span> <span class="nt">-lT</span> <span class="o">{}</span> <span class="se">\;</span>
</code></pre></div></div>

<p>最後の1行が、この事案でとくに重要になる。発表は漏えいの可能性がある情報として「<strong>顧客領域内に保存された情報</strong>」を挙げている。サーバー上のファイルに平文で書かれた認証情報は、まさにこの「顧客領域内に保存された情報」だ。<code class="language-plaintext highlighter-rouge">wp-config.php</code> のデータベースパスワード、<code class="language-plaintext highlighter-rouge">.env</code> の外部APIキー、決済やメール配信のトークン——これらはサーバーのログインパスワードを変えても無効化されない。<a href="/security/news-fake-tanstack-npm-env-steal/">偽TanStackパッケージが.envを狙ったnpmサプライチェーン攻撃</a> と同様に、狙われるのは常に「そこに置いてある鍵」のほうだ。外部サービスのキーについては、<a href="/security/gemini-api-key-abuse-prevention/">Gemini APIキーの不正利用を防ぐ管理と監視の実践</a> で扱ったような発行元側でのローテーションと利用状況の確認が必要になる。</p>

<h3 id="5-ログを見るただし残っていない可能性がある">5. ログを見る——ただし残っていない可能性がある</h3>

<p>公式発表の確認項目には「心当たりのないログインやメール送信等が発生していないか」が含まれている。これに対応する手段は2つある。</p>

<p>ひとつはコントロールパネルだ。さくらのレンタルサーバでは、<strong>サーバーコントロールパネルの「セキュリティ」→「サーバーログイン履歴」</strong>からログイン履歴を確認できる。SSHが使えないライトプランでも参照できる経路なので、プランを問わず最初に見るべき場所になる。なお公式ヘルプは、メールアドレスとパスワードでログインするとメール設定しか表示されず履歴に到達できない、と注意している。初期ドメインまたは追加ドメインでログインする必要がある。</p>

<p>もうひとつがアクセスログだが、ここには重要な前提がある。<strong>さくらのレンタルサーバのアクセスログは、利用者が「保存する」設定にしていなければ蓄積されない。</strong> 保存設定をしている場合は <code class="language-plaintext highlighter-rouge">/home/アカウント名/log/access_log_[日付]</code> に置かれ、当日分はテキスト、前日以前はgzip圧縮される。保存期間は最大24か月まで選べる。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ログが実際に存在するか（保存設定をしていなければ空か存在しない）</span>
<span class="nb">ls</span> <span class="nt">-lT</span> ~/log/ 2&gt;/dev/null | <span class="nb">head</span> <span class="nt">-20</span>

<span class="c"># 当日分（非圧縮）— 認証まわりへのPOSTを抜く</span>
<span class="nb">grep</span> <span class="nt">-h</span> <span class="s1">'"POST '</span> ~/log/access_log_<span class="k">*</span> 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"</span><span class="se">\.</span><span class="s2">gz"</span> | <span class="nb">tail</span> <span class="nt">-50</span>

<span class="c"># 前日以前（gzip圧縮済み）— BSDでは zcat ではなく zgrep / gzcat を使う</span>
zgrep <span class="nt">-h</span> <span class="s2">"wp-login</span><span class="se">\|</span><span class="s2">xmlrpc</span><span class="se">\|</span><span class="s2">/uploads/.*</span><span class="se">\.</span><span class="s2">php"</span> ~/log/access_log_<span class="k">*</span>.gz 2&gt;/dev/null | <span class="nb">tail</span> <span class="nt">-50</span>

<span class="c"># アクセス元IPの多い順（awk / sort / uniq は BSD でもそのまま使える）</span>
zgrep <span class="nt">-h</span> <span class="s2">""</span> ~/log/access_log_<span class="k">*</span>.gz 2&gt;/dev/null | <span class="nb">awk</span> <span class="s1">'{print $1}'</span> | <span class="nb">sort</span> | <span class="nb">uniq</span> <span class="nt">-c</span> | <span class="nb">sort</span> <span class="nt">-rn</span> | <span class="nb">head</span> <span class="nt">-20</span>
</code></pre></div></div>

<div class="box-tip"><strong><code>zcat</code> はFreeBSDでは使えるが、macOSでは失敗する</strong><br />
圧縮ログの展開で使う <code>zcat</code> は、環境によって挙動が分かれる。<strong>FreeBSDの <code>zcat</code> はbase同梱のgzipのエイリアス</strong>で、gzip(1) のNAME欄に <code>gzip, gunzip, zcat</code> と併記され「zcat または gzcat として起動された場合は <code>-c</code> と <code>-d</code> が有効になる」と定義されている。つまりさくらのレンタルサーバ上では <code>zcat access_log_*.gz</code> はそのまま通る。<br />
一方 <strong>macOS（Darwin）の <code>zcat</code> は旧compress形式を前提</strong>としており、<code>file.gz.Z</code> を探しに行って <code>can't stat</code> で終わる（手元で確認）。手元のMacで予行演習してから本番に臨む場合、ここだけ挙動が逆転する。両方で確実に動かしたいなら <code>gzcat</code> か <code>zgrep</code> を使うのが無難だ。</div>

<p>ログについては、<strong>「見た結果なにも無かった」と「そもそも記録が無い」を混同しない</strong>ことが重要だ。アクセスログを保存する設定にしていなければ、侵害の痕跡が無いのではなく観測手段が無い。さらに公式ヘルプは、ディスク使用量が80%を超えるとログの保存機能が停止すると明記している。設定していたつもりでも、容量逼迫で途中から記録が止まっているケースがありうる。ログが薄いこと自体を、今後に向けた設定の見直し対象として扱うのが妥当だ。</p>

<h3 id="影響範囲マトリクス自分はどこまで確認すべきか">影響範囲マトリクス——自分はどこまで確認すべきか</h3>

<p>ここまでの確認手順は、利用形態によって重みが変わる。公式発表が挙げた確認項目（ファイル・管理者アカウント・不審な変更・心当たりのないログインやメール送信）を、実際の使われ方に対応させると次のようになる。影響を受ける可能性の欄は、発表時点で判明している情報に基づく整理であり、調査継続中である点は全行に共通する。</p>

<table>
  <thead>
    <tr>
      <th>利用形態</th>
      <th>影響を受ける可能性</th>
      <th>優先して確認すること</th>
      <th>鍵の再発行</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ライトプラン（静的サイト・メール）</td>
      <td>あり（レンタルサーバのため）</td>
      <td>ファイルマネージャー/FTPで更新日時順に目視。コントロールパネルのメール転送設定</td>
      <td>メールパスワード</td>
    </tr>
    <tr>
      <td>スタンダード以上＋WordPress等CMS</td>
      <td>あり。<strong>最も確認項目が多い</strong></td>
      <td>追加された管理者アカウント、<code class="language-plaintext highlighter-rouge">wp-config.php</code>、<code class="language-plaintext highlighter-rouge">uploads</code> 配下のPHP、<code class="language-plaintext highlighter-rouge">.htaccess</code></td>
      <td>DBパスワード、CMS管理者、認証プラグインの鍵</td>
    </tr>
    <tr>
      <td>スタンダード以上＋自作アプリ</td>
      <td>あり。<strong>鍵の露出が最大の論点</strong></td>
      <td><code class="language-plaintext highlighter-rouge">.env</code>・設定ファイルに平文で置いた外部APIキー、決済・メール配信トークン</td>
      <td>外部サービス側で全キーをローテーション</td>
    </tr>
    <tr>
      <td>メール中心の利用</td>
      <td>あり</td>
      <td>心当たりのない送信履歴、転送設定の追加、自動応答の書き換え</td>
      <td>メールパスワード、SMTP認証情報</td>
    </tr>
    <tr>
      <td>さくらのVPS／クラウド／専用サーバ PHY／高火力 PHY</td>
      <td>発表時点で<strong>確認されていない</strong></td>
      <td>続報の確認。現時点で個別対応の指示は出ていない</td>
      <td>不要（現時点）</td>
    </tr>
  </tbody>
</table>

<p>この表で一貫しているのは、<strong>サーバーのログインパスワードを変えても無効化されないもの</strong>——サーバー上のファイルに平文で置かれた外部サービスの鍵——が、どの行でも再発行対象に入る点だ。顧客領域内に保存された情報が閲覧された可能性がある、という発表の一文が効いてくるのはここになる。</p>

<h2 id="制限された3機能の共通点読み取れることと読み取れないこと">制限された3機能の共通点——読み取れることと、読み取れないこと</h2>

<p>発表の「現在実施している対応」に、ひとつ具体的な記載がある。<strong>「さくらのレンタルサーバ」のデータベースアップグレード機能、プラン変更、移行ツールなどの一部機能の制限</strong>だ。</p>

<p>この3つには機能上の共通点がある。いずれも<strong>利用者の権限では完結せず、事業者側の自動化が顧客データを読み書きしたり別の場所へ移したりする操作</strong>だ。データベースアップグレードはDBの中身を読んで新バージョンへ書き戻す。プラン変更は収容先を変える。移行ツールはデータを別環境へ複製する。通常の閲覧や更新と違い、顧客領域の境界をまたいで動く系統の機能が並んでいる。</p>

<p>封じ込めの一般論として、侵害の可能性がある経路や、調査中に状態を変えられると困る機能を止めるのは定石だ。上記3機能がその条件に当てはまることは、機能の性質から言える。</p>

<p><strong>ただし、ここから侵入経路を逆算することはできない。</strong> 同社は侵入経路を調査中と明言しており、経路を特定した旨の発表はしていない。機能が制限された理由は、その機能が侵入に使われたからかもしれないし、フォレンジック調査中にデータが動くのを避けるためかもしれないし、単に安全側に倒した予防措置かもしれない。発表はどれとも書いていない。本記事はこの点について推定を置かない。</p>

<p>・<strong>事実</strong>：データベースアップグレード機能・プラン変更・移行ツールなどが制限された<br />
・<strong>機能から言えること</strong>：いずれも事業者側の自動化が顧客データに触れる操作である<br />
・<strong>言えないこと</strong>：これらが侵入経路だったかどうか。制限の理由。公式は経路を調査中としている</p>

<h2 id="通信の秘密と個人データは別レジーム1つの事案に走る2つのクロック">「通信の秘密」と「個人データ」は別レジーム——1つの事案に走る2つのクロック</h2>

<p>ここが、多くの報道が触れていない部分だ。発表には次の一文がある——<strong>「攻撃者がお客さま情報および通信の秘密に該当する情報へアクセス可能な状態であったことを確認しております」</strong>。</p>

<p>「通信の秘密」は日常語ではなく、電気通信事業法上の法的概念だ。さくらインターネットは電気通信事業者であり、この語を使った時点で、個人情報保護法とは<strong>別系統の報告義務</strong>が発生する。実際、発表の対応欄には「総務省、個人情報保護委員会等の関係機関への報告および情報共有」と、2つの官庁が併記されている。</p>

<figure>
<img src="/generated_images/sections/sakura-two-clocks.png" alt="通信の秘密の漏えいは詳報が認知日から30日以内、個人データの漏えい等は確報が30日以内で不正アクセス等は60日以内、特定利用者情報の漏えいは指定電気通信事業者のみ対象という3レジームの比較" loading="lazy" width="1280" height="430" />
<figcaption>1つの事案が複数に該当する場合、それぞれ別個に報告が必要になる。提出先はいずれも本社所在地を管轄する総合通信局等。出典: <a href="https://www.soumu.go.jp/main_sosiki/joho_tsusin/d_syohi/rouei_houkoku.html">電気通信事業における通信の秘密の漏えい事案の報告（総務省）</a>・<a href="https://www.soumu.go.jp/soutsu/kanto/com/rouei.html">報告の手続（総務省 関東総合通信局）</a></figcaption>
</figure>

<p>総務省の手引きは、この重複を明示的に注意している。<strong>「一の漏えい等事案が、『通信の秘密の漏えい』『特定利用者情報の漏えい』『個人データの漏えい等』の複数に該当する場合、電気通信事業者はそれぞれ所要の報告を行う必要があります」</strong>。1つのインシデントに対して、様式も期限も別々の報告が並走するということだ。</p>

<table>
  <thead>
    <tr>
      <th>報告の種類</th>
      <th>根拠法</th>
      <th>第一報／速報</th>
      <th>詳報／確報の期限</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>通信の秘密の漏えい</td>
      <td>電気通信事業法</td>
      <td>発覚後すみやかに</td>
      <td>漏えいを<strong>認知した日から30日以内</strong>（詳報）</td>
    </tr>
    <tr>
      <td>個人データの漏えい等</td>
      <td>個人情報保護法</td>
      <td>発覚後すみやかに</td>
      <td>知った日を1日目として<strong>30日以内</strong>、ただし<strong>不正アクセス等は60日以内</strong>（確報）</td>
    </tr>
    <tr>
      <td>特定利用者情報の漏えい</td>
      <td>電気通信事業法</td>
      <td>発覚後すみやかに</td>
      <td>認知した日から30日以内（詳報）</td>
    </tr>
  </tbody>
</table>

<p>注目すべきは<strong>期限が揃っていない</strong>ことだ。通信の秘密の詳報は30日以内で固定される。一方、個人データの確報は原則30日だが、個人情報保護法施行規則第7条第3号——「不正の目的をもって行われたおそれがある行為による漏えい等」——に該当する場合は60日以内に延びる。<strong>不正アクセスはまさにこの3号の典型例</strong>として挙げられている類型だ。</p>

<p>つまり同じ1つの事案について、<strong>（両トラックの認知日が同じであれば）通信の秘密のトラックのほうが先に期限を迎える</strong>構造になっている。詳細な続報がどのタイミングで出るかを考えるうえで、この非対称性は実務的な意味を持つ。</p>

<div class="box-warning"><strong>ただし起算日は確定できない</strong><br />
どちらの期限も起算点は「認知した日」「知った日」であって、公表日ではない。発表では8月9日に「異常を検知」したとあるが、通信の秘密に該当する情報へのアクセスが確認されたのは「その後の調査により」とされている。つまり<strong>各トラックの起算日となる認知日は、公式発表からは特定できない</strong>。本記事で具体的な期限日を計算していないのはこのためだ。8月9日を起算日と仮定して逆算した日付が出回った場合、それは前提が置かれた推計であって公式の期限ではない。</div>

<p>なお3本目の「特定利用者情報の漏えい」は、利用者の利益に及ぼす影響が大きい電気通信役務を提供する者として指定された<strong>指定電気通信事業者のみ</strong>が対象となる。さくらインターネットがこの指定を受けているかどうかは本記事では確認できていないため、該当するかは判断を保留する。</p>

<p>なお提出先は、いずれのトラックでも「本社所在地を管轄する総合通信局等」と定められている。さくらインターネットの本社は大阪市北区にあるため、窓口は近畿総合通信局が該当することになる。個人データの漏えい等についても、電気通信事業者の場合は個人情報保護委員会の権限が総務省へ委任されているため、提出先は個人情報保護委員会ではなく同じく総合通信局等になる。発表に「総務省、個人情報保護委員会等の関係機関への報告および情報共有」と両方が併記されているのは、この委任構造と整合する。</p>

<h2 id="今後の見通しと備え続報で見るべき点と便乗フィッシング">今後の見通しと備え——続報で見るべき点と、便乗フィッシング</h2>

<p>発表は「本件については現在も調査を継続しており、新たに公表すべき事実が判明した場合には速やかにお知らせいたします」で締めくくられている。続報は前提とされている。次の発表で確認すべき点を整理しておく。</p>

<p>・<strong>583という数字が動くか</strong>：現在は「判明した」数であり、調査継続中と注記されている。増える可能性がある<br />
・<strong>侵入経路と発生時期</strong>：現在は完全に未公表。ここが出ると、被害期間の見積もりが初めて可能になる<br />
・<strong>漏えいした情報の具体的内容</strong>：現在は「顧客領域内に保存された情報」「利用者識別子」という抽象的な粒度にとどまる<br />
・<strong>他サービスへの影響の確定</strong>：VPS・クラウド・専用サーバ PHY・高火力 PHY は「現時点で確認されず」の段階<br />
・<strong>制限された機能の復旧</strong>：データベースアップグレード機能・プラン変更・移行ツールの制限がいつ解けるか<br />
・<strong>一律のパスワード変更要請の有無</strong>：現時点では出ていないが、「必要な事項が判明した場合は個別案内とウェブサイトで告知」とされている</p>

<p>そして利用者側で今すぐ効くのは、<strong>便乗フィッシングへの警戒</strong>だ。発表は「本件に便乗したフィッシングメールや不審な連絡にも十分ご注意ください」と明記し、続けて「当社から、パスワード、認証情報、クレジットカード情報等をメールや電話でお伺いすることはありません」と断言している。</p>

<p>これは実務的に重要な条件だ。インシデント公表後は、その事業者を騙る「緊急のパスワード変更のお願い」型のメールが出回りやすい。本件では<strong>同社が対象者へ個別にメール通知を行っている</strong>ため、正規の通知と偽の通知が同じ受信箱に並ぶ状況が生まれる。判別の基準は単純にできる。</p>

<div class="box-tip"><strong>正規通知と便乗フィッシングの切り分け</strong><br />
・メール内のリンクを踏まず、<strong>会員メニューのURLを自分で入力して</strong>ログインし直し、告知を確認する<br />
・パスワードや認証情報の入力をメール経由で求められたら、その時点で偽物と判断してよい（同社が明示的に否定している）<br />
・急かす文面・期限を切る文面ほど疑う。公式発表は一律のパスワード変更を求めておらず、期限も設けていない<br />
・不審な事象を見つけた場合の正規窓口は、同社カスタマーセンター（<code>support@sakura.ad.jp</code>、問い合わせ時は会員IDを添える）</div>

<p>最後に、この事案から持ち帰れる一般則を1つ挙げておく。共有ホスティングを使うということは、<strong>自分の環境の安全性の一部を事業者の管理層に委ねる</strong>ということだ。委ねた部分は利用者側の努力では固くできない。だからこそ、委ねなくて済むもの——サーバー上のファイルに平文で置いた外部サービスの鍵——を減らしておくことの価値が上がる。サーバーが侵害されたときに失われるものの大きさは、そこに何を置いていたかで決まる。</p>

<h2 id="参照ソース">参照ソース</h2>

<p>・<a href="https://www.sakura.ad.jp/corporate/information/newsreleases/2026/08/17/1968225614/">当社レンタルサーバーサービスの一部環境に対する不正なアクセスについて（さくらインターネット公式・2026年8月17日）</a><br />
・<a href="https://help.sakura.ad.jp/rs/2251/">基本仕様を知りたい（さくらのレンタルサーバ）｜さくらのサポート情報</a><br />
・<a href="https://help.sakura.ad.jp/rs/2239/">ログイン履歴を確認したい｜さくらのサポート情報</a><br />
・<a href="https://help.sakura.ad.jp/rs/2228/">アクセスログを設定し管理したい（Webalizer）｜さくらのサポート情報</a><br />
・<a href="https://www.soumu.go.jp/main_sosiki/joho_tsusin/d_syohi/rouei_houkoku.html">電気通信事業における通信の秘密の漏えい事案の報告（総務省）</a><br />
・<a href="https://www.soumu.go.jp/soutsu/kanto/com/rouei.html">電気通信業における「通信の秘密の漏えい」・「特定利用者情報の漏えい」・「個人データの漏えい等」報告の手続（総務省 関東総合通信局）</a><br />
・<a href="https://www.soumu.go.jp/main_sosiki/joho_tsusin/d_syohi/denkitsushin_rouei.html">電気通信業における個人データの漏えい等事案の報告（総務省）</a><br />
・<a href="https://www.ppc.go.jp/personalinfo/legal/leakAction/">漏えい等の対応とお役立ち資料（個人情報保護委員会）</a><br />
・<a href="https://man.freebsd.org/cgi/man.cgi?query=gzip&amp;sektion=1">gzip(1) — FreeBSD Manual Pages（zcat がgzipのエイリアスである根拠）</a></p>

<!--
配信メモ（Distribution）
- X: 「583アカウント」の数字ではなく「当社管理環境を経由して」の一文が本質、という角度で。画像は sakura-intrusion-model.png。
  セルフリプで「通信の秘密と個人データで報告期限が30日/60日と別々に走る」を追加（two-clocks図）。
- はてブ: FreeBSD gotcha（md5sum/stat -c/find -printf/grep -P が通らない）が技術者に刺さる想定。
- 内部リンク提案: 今後ホスティング事業者インシデントの記事が増えたら相互リンク。
-->]]></content><author><name>編集部</name></author><category term="security" /><category term="security" /><category term="セキュリティ" /><category term="インシデント" /><category term="ホスティング" /><category term="さくらインターネット" /><category term="FreeBSD" /><summary type="html"><![CDATA[さくらのレンタルサーバ不正アクセスは「583人のパスワードが破られた」話ではない。公式発表が書いた『当社管理環境を経由して』の意味、影響なしとされたサービスの境界、FreeBSD環境で実際に通る確認コマンド、そして1つの事案に走る2つの報告期限を一次ソースだけで整理する。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_sakura-rental-server-unauthorized-access.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_sakura-rental-server-unauthorized-access.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">DeepSeek Harness徹底解説｜「すべてがプラグイン」の実体を129行の構成ダンプと起動ゲートで測る</title><link href="https://ai-heartland.com/agent/deepseek-harness/" rel="alternate" type="text/html" title="DeepSeek Harness徹底解説｜「すべてがプラグイン」の実体を129行の構成ダンプと起動ゲートで測る" /><published>2026-08-16T10:30:00+09:00</published><updated>2026-08-16T10:30:00+09:00</updated><id>https://ai-heartland.com/agent/deepseek-harness</id><content type="html" xml:base="https://ai-heartland.com/agent/deepseek-harness/"><![CDATA[<p>2026年8月13日、DeepSeek AIが<strong>DeepSeek Harness（<code class="language-plaintext highlighter-rouge">dsh</code>）</strong>をMITライセンスで公開した。GitHubの <a href="https://github.com/deepseek-ai/deepseek-harness">deepseek-ai/deepseek-harness</a> は公開から3日で115,000スターを超え、Hacker Newsのスレッドも本稿執筆時点で729ポイントに達している。掲げられた設計思想はただ一つ、<strong>「Everything is a Plugin（すべてがプラグイン）」</strong>だ。</p>

<p>ただし、この手の標語は紹介記事で何度も繰り返されるうちに検証されないまま定着する。幸い「すべてがプラグイン」は<strong>数えられる主張</strong>である。本記事では宣伝文とREADMEをいったん脇に置き、npm公開版（<code class="language-plaintext highlighter-rouge">@deepseek-ai/dsh</code> 0.1.0-rc.6）を実際にインストールして、構成をダンプし、起動を止められ、サンドボックスの実装を1つずつ確かめた結果をまとめる。読者が自分の環境で再現できるコマンドも併記した。</p>

<figure>
  <img loading="lazy" src="/generated_images/deepseek-harness-presets.png" width="1440" height="880" alt="DeepSeek HarnessのWeb UI。エージェント・プリセットの選択メニューにStandard / Code / Minimal / Creatorの4モードが並ぶ" />
  <figcaption>本記事で実際に起動したDeepSeek Harness 0.1.0-rc.6のWeb UI（<code>npx @deepseek-ai/dsh web</code> → http://127.0.0.1:3080）。APIキー未設定でもUIは起動し、4つのエージェント・プリセットが選択できる</figcaption>
</figure>

<p>エージェント基盤そのものの全体像や、他フレームワークとの位置づけを先に押さえたい方は、まず<a href="/explain/ai-agent-framework-comparison-2026/">AIエージェントフレームワーク比較2026｜LangGraph・CrewAI・Dify等9種をStar数・実コードで検証</a>を参照してほしい。本記事はその中で「ハーネス層」に相当するDeepSeek Harnessを、実測に絞って掘り下げる。</p>

<div class="point-box">
  <p><strong>30秒でわかるDeepSeek Harness</strong></p>

  <p>・<strong>正体</strong>：モデルではなく「body」側。ツール・権限・セッション・サンドボックス・UIをすべてプラグインとして差し替える実行基盤<br />
・<strong>実測した規模</strong>：標準<code class="language-plaintext highlighter-rouge">web</code>プロファイルの構成ダンプは<strong>129行のプラグイン行</strong>、うち<strong>25行が既定で無効</strong>。npmには<code class="language-plaintext highlighter-rouge">@deepseek-ai/*</code>スコープで<strong>195パッケージ</strong>が入る<br />
・<strong>プリセットは4つ</strong>：Standard / Code / Minimal / Creator。うち<strong>CodeはStandardとの差分がプラグイン1枚だけ</strong><br />
・<strong>起動ゲートは2段</strong>：Node <strong>22.15.0以上</strong>（<code class="language-plaintext highlighter-rouge">engines</code>未宣言のため警告なしで落ちる）→ <code class="language-plaintext highlighter-rouge">DEEPSEEK_API_KEY</code><br />
・<strong>土台はCordis</strong>：Koishi系エコシステム由来のメタフレームワークを<code class="language-plaintext highlighter-rouge">vendor/</code>に取り込み、<code class="language-plaintext highlighter-rouge">@deepseek-ai</code>スコープで再公開している<br />
・<strong>ステータス</strong>：開発者プレビュー（0.1.0-rc.6）。互換性を壊す変更が入ると公式に明言。本記事の数値も動く前提で読んでほしい</p>
</div>

<h2 id="deepseek-harnessとはモデルではなくbodyを配る">DeepSeek Harnessとは——モデルではなく「body」を配る</h2>

<p>DeepSeekはこれまでモデル（V3、R1、V4系）で知られてきた。DeepSeek Harnessが配っているのはその逆側、<strong>モデルに手足を与える実行環境</strong>である。READMEの表現を借りれば、モデルが脳なら、ハーネスは身体であり、その身体のあらゆる部位がプラグインとして交換可能になっている。</p>

<p>対象になるのは以下だ。モデル接続、ツール（bash・ファイル編集・Web検索）、スキル、セッション永続化、サンドボックス、ファイルシステム、エージェントループ、サブエージェント、ワークフロー、そしてWeb UIそのもの。<strong>UIまでプラグイン</strong>という点が、Claude CodeやCodex CLIのような「完成品のCLI」とは根本的に発想が違う。</p>

<p>この設計を可能にしているのが <strong><a href="https://github.com/cordiverse/cordis">Cordis</a></strong> というメタフレームワークだ。READMEは「<em>A Programming Paradigm for Spatiotemporal Composability</em>（時空間的合成可能性のためのプログラミングパラダイム）」という論文を参照している。抽象的に聞こえるが、実装まで降りると意味は具体的で、後述する<code class="language-plaintext highlighter-rouge">isolate</code>レルムとホットリロードの2点に集約される。</p>

<h3 id="押さえておきたい前提これは開発者プレビューである">押さえておきたい前提：これは開発者プレビューである</h3>

<p>READMEは大文字で「<strong>互換性を壊す変更が入る</strong>」と警告している。実際、GitHubの公開リポジトリの最終コミットは2026年8月13日 13:00（UTC）で、リリースタグもGitHub Releasesも作られていない。バージョン管理はnpm側で行われており、本記事が測ったのは npm の <strong>0.1.0-rc.6</strong>（2026年8月13日 12:35 UTC公開）である。</p>

<div class="box-warning">
  <p><strong>本記事の数値の有効範囲と検証環境</strong></p>

  <p>以下で示すプラグイン数・プリセット構成・既定値は、すべて <strong>npm版 <code class="language-plaintext highlighter-rouge">@deepseek-ai/dsh</code> 0.1.0-rc.6</strong> を実測したものだ。GitHubの公開コミットは<code class="language-plaintext highlighter-rouge">0.1.0-rc.5</code>までしか含まないため、GitHubのツリーを読んだ数値とは一致しない可能性がある。プレビュー版であり、数値は今後動く。再現用のコマンドを各節に併記したので、読む時点の版で取り直してほしい。</p>

  <p>・<strong>検証日</strong>：2026年8月16日（JST）<br />
・<strong>OS</strong>：macOS（darwin arm64）<br />
・<strong>Runtime</strong>：Node.js 22.13.1 / 22.14.0 / 22.15.0（公式tarballを並べて比較）<br />
・<strong>対象</strong>：<code class="language-plaintext highlighter-rouge">@deepseek-ai/dsh</code> 0.1.0-rc.6（npm、2026年8月13日公開）<br />
・<strong>未実施</strong>：<code class="language-plaintext highlighter-rouge">isolate</code>レルム違反によるマウント拒否のライブ再現（APIキーを要する経路のため、実装ソースの読解に留めた）</p>
</div>

<h2 id="すべてがプラグインを数える構成ダンプ129行の内訳">「すべてがプラグイン」を数える——構成ダンプ129行の内訳</h2>

<p><code class="language-plaintext highlighter-rouge">dsh</code>には<code class="language-plaintext highlighter-rouge">--dump-default-config</code>という、<strong>主張をそのまま検証できるフラグ</strong>が用意されている。ユーザー設定や<code class="language-plaintext highlighter-rouge">--patch</code>オーバーレイを除いた、そのプロファイル本来の構成ツリーを出力するものだ。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx @deepseek-ai/dsh <span class="nt">--profile</span> web <span class="nt">--dump-default-config</span>
</code></pre></div></div>

<p>出力は490行のYAMLで、そのうちプラグイン行（<code class="language-plaintext highlighter-rouge">name:</code>）は<strong>129行</strong>あった。内訳は以下のとおりである。</p>

<div class="box-tip">
  <p><strong>標準<code class="language-plaintext highlighter-rouge">web</code>プロファイルの実測値（0.1.0-rc.6）</strong></p>

  <p>・プラグイン行：<strong>129</strong><br />
・うち<code class="language-plaintext highlighter-rouge">disabled: true</code>（既定で無効）：<strong>25</strong><br />
・実際に起動する行：<strong>104</strong><br />
・構成を提供するバンドル：<strong>2つ</strong>（<code class="language-plaintext highlighter-rouge">dsh-base</code> と <code class="language-plaintext highlighter-rouge">dsh-web-app</code>）<br />
・<code class="language-plaintext highlighter-rouge">dsh-web-app</code>が<code class="language-plaintext highlighter-rouge">dsh-base</code>にパッチを当てている箇所：<strong>11</strong></p>
</div>

<p>数だけでなく、<strong>出力のコメントが出所を示している</strong>点が有用だ。各行の上に <code class="language-plaintext highlighter-rouge"># == @deepseek-ai/dsh-base</code> や <code class="language-plaintext highlighter-rouge"># == @deepseek-ai/dsh-base, patched by @deepseek-ai/dsh-web-app</code> という注記が付き、「この設定は誰が置き、誰が上書きしたか」がダンプを読むだけで追える。プラグイン数が3桁に達する構成で、これは実務上かなり効く。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/dsh-scale.png" width="1280" height="340" alt="DeepSeek Harnessの規模を示す実測値。プラグイン行129・既定無効25・npmパッケージ195・プリセット4" />
  <figcaption><code>--dump-default-config</code>とnpmインストール結果から取得した実測値</figcaption>
</figure>

<h3 id="既定で無効な25行が意味すること">既定で無効な25行が意味すること</h3>

<p>無効化されている25行を並べると、この構成の設計が見えてくる。</p>

<p><code class="language-plaintext highlighter-rouge">dsh-tool-bash</code> / <code class="language-plaintext highlighter-rouge">dsh-tool-fs</code> / <code class="language-plaintext highlighter-rouge">dsh-tool-fs-search</code> / <code class="language-plaintext highlighter-rouge">dsh-tool-web</code> / <code class="language-plaintext highlighter-rouge">dsh-tool-todo</code> / <code class="language-plaintext highlighter-rouge">dsh-tool-goal</code> / <code class="language-plaintext highlighter-rouge">dsh-tool-workflow</code> / <code class="language-plaintext highlighter-rouge">dsh-tool-str-replace-editor</code> / <code class="language-plaintext highlighter-rouge">dsh-skill</code> / <code class="language-plaintext highlighter-rouge">dsh-skill-filesystem</code> / <code class="language-plaintext highlighter-rouge">dsh-compaction-basic</code> ——つまり<strong>エージェントが使うツール類がほぼ全部、ホスト起動時点では無効</strong>なのだ。</p>

<p>これは欠陥ではなく分業である。ツールを有効にするのは後述する<strong>エージェント・プリセット</strong>の役割で、ホスト側の構成はレジストリ・サンドボックス・永続化・モデルルートといった「セッションが所有してはいけないもの」だけを持つ。同梱の構成ファイルはこれを<strong>ホストプレーン／エージェントプレーン</strong>と呼び分けている。</p>

<div class="mermaid">
graph TB
  subgraph HOST["ホストプレーン（プロセス全体で1つ）"]
    A["base.cordis.yml + web.cordis.yml"]
    B["レジストリ・永続化・モデルルート"]
    C["サンドボックス／承認スタック"]
  end
  subgraph AGENT["エージェントプレーン（セッションごと）"]
    D["agent.cordis.yml（プリセット）"]
    E["ツール登録・プロンプト断片"]
    F["isolate レルム内のサービス"]
  end
  A --&gt; B
  A --&gt; C
  D --&gt; E
  D --&gt; F
  B -.-&gt;|"サービスを提供"| D
  C -.-&gt;|"ポリシーを提供"| D
  style HOST fill:#e3f2fd,stroke:#0070f3
  style AGENT fill:#e0f7f2,stroke:#19c2a8
</div>

<p>この2プレーン分離は、実装上<strong>強制されている</strong>。プリセット側の構成でサービスを公開する行は、必ず<code class="language-plaintext highlighter-rouge">isolate</code>レルムを持つグループの中に置かなければならない。置かなければそのサービスはルートレルム（=プロセス全体で共有）に公開されてしまい、2つ目のセッションが同じプリセットを読み込んだ瞬間に1つ目と衝突する。</p>

<p><code class="language-plaintext highlighter-rouge">dsh-agent-presets</code>はこれをマウント時に検査し、違反していれば<strong>マウント自体を拒否</strong>する。エラーメッセージは「プリセットのサービスは<code class="language-plaintext highlighter-rouge">isolate</code>レルムの背後に置くか、ホスト構成へ移せ」と具体的に指示する形になっている。加えて、プラグインが後からタイマーや非同期継続でサービスを公開した場合に備え、サービス登録が変化するたびに全マウントを再検査する仕組みまで用意されている。</p>

<p>これが「時空間的合成可能性」の<strong>空間</strong>側の正体だ。抽象論ではなく、マウント時に落とされる具体的な制約である。</p>

<h2 id="4つのプリセットcodeはstandardとの差分がプラグイン1枚しかない">4つのプリセット——CodeはStandardとの差分がプラグイン1枚しかない</h2>

<p>エージェントの性格を決めるのがプリセットだ。UIのメニューには Standard / Code / Minimal / Creator の4つが並ぶ。ディスク上の実体は<code class="language-plaintext highlighter-rouge">config/agent-presets/</code>配下の4ディレクトリで、それぞれ<code class="language-plaintext highlighter-rouge">preset.yml</code>（表示名）と<code class="language-plaintext highlighter-rouge">agent.cordis.yml</code>（構成本体）を持つ。</p>

<p>なお<strong>ディレクトリ名と表示名は一致しない</strong>。Creatorモードのディレクトリ名は<code class="language-plaintext highlighter-rouge">creator</code>ではなく <strong><code class="language-plaintext highlighter-rouge">cordis</code></strong> である。</p>

<table>
  <thead>
    <tr>
      <th>プリセット</th>
      <th>ディレクトリ名</th>
      <th>中国語表示名</th>
      <th>構成行数</th>
      <th>位置づけ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Standard</td>
      <td><code class="language-plaintext highlighter-rouge">standard</code></td>
      <td>标准模式</td>
      <td>251行</td>
      <td>ファイル編集・Shell・検索・Skills・計画・目標・サブエージェント・ワークフローを備えた全部入り</td>
    </tr>
    <tr>
      <td>Code</td>
      <td><code class="language-plaintext highlighter-rouge">code</code></td>
      <td>PTC 模式</td>
      <td>262行</td>
      <td>Standardの全能力＋Code Mode SDK。1操作1ツール呼び出しでなく、TypeScriptプログラムを書いて<code class="language-plaintext highlighter-rouge">run_code</code>で実行</td>
    </tr>
    <tr>
      <td>Minimal</td>
      <td><code class="language-plaintext highlighter-rouge">minimal</code></td>
      <td>极简模式</td>
      <td>62行</td>
      <td>永続bashと<code class="language-plaintext highlighter-rouge">str_replace_editor</code>の2ツールのみ。ベンチマーク用</td>
    </tr>
    <tr>
      <td>Creator</td>
      <td><code class="language-plaintext highlighter-rouge">cordis</code></td>
      <td>创造模式</td>
      <td>262行</td>
      <td>Standardの全能力＋ランタイム自己参照ツール。プリセットを作るためのプリセット</td>
    </tr>
  </tbody>
</table>

<p>この表で最も情報量が多いのは行数だ。<code class="language-plaintext highlighter-rouge">standard</code>と<code class="language-plaintext highlighter-rouge">code</code>の構成ファイルをdiffすると、<strong>コメントを除いた実質的な差分はプラグイン1枚（YAMLでは4行）だけ</strong>である。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">tool-presentation</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">@deepseek-ai/dsh-agent-tool-presentation'</span>
  <span class="na">config</span><span class="pi">:</span>
    <span class="na">mode</span><span class="pi">:</span> <span class="s">code</span>
</code></pre></div></div>

<p>「Code Mode」という独立したモードがあるのではなく、<strong>Standardに提示層プラグインを1枚重ねただけ</strong>。5回のツール往復になるはずの操作列が、TypeScriptプログラム1本にまとまる——この挙動の差が、構成ファイル上ではプラグイン1行の追加として表現されている。「すべてがプラグイン」という標語が、ここで実際に機能しているのが確認できる。</p>

<h3 id="minimalモードはホストのサンドボックスをわざと迂回する">Minimalモードはホストのサンドボックスをわざと迂回する</h3>

<p>Minimalプリセットの62行は短いぶん、設計の意図が読み取りやすい。ペルソナは<code class="language-plaintext highlighter-rouge">You are a helpful software engineer assistant.</code>の一文だけで、<code class="language-plaintext highlighter-rouge">complete: true</code>が指定されている。これは<strong>この文字列がシステムプロンプトの全体であり、以降のどの仕組みもプロンプトを追記できない</strong>という宣言だ。ランタイム情報のスナップショットも抑止され、コンテキスト圧縮も入らない。ベンチマークで条件を固定するための構成としては筋が通っている。</p>

<p>一方で注意すべき記述もある。Minimalプリセットのファイルシステムは<code class="language-plaintext highlighter-rouge">isolate</code>されたグループの中で<code class="language-plaintext highlighter-rouge">dsh-fs-local</code>（素のローカルFS）を使い、同梱コメントは<strong>このプリセットに限りホスト側のサンドボックス済みプロバイダを覆い隠す</strong>と明記している。ベンチマーク用途では妥当だが、日常の作業用として選ぶモードではない。</p>

<h3 id="creator创造模式は自らをシェルアクセス相当と宣言している">Creator（创造模式）は自らを「シェルアクセス相当」と宣言している</h3>

<p>セキュリティ面で最も重要なのはCreatorモードだ。<code class="language-plaintext highlighter-rouge">cordis/agent.cordis.yml</code>の冒頭コメントには、<strong>TRUST</strong>という見出しで警告が書かれている。要点は2つある。</p>

<p>・<code class="language-plaintext highlighter-rouge">cordis_mount</code>は<strong>モデルが書いたJavaScriptを稼働中のランタイムに対して評価する</strong><br />
・このエージェントが書いた構成は、他のセッションがマウントするプリセットになる</p>

<p>そのうえで、<strong>このプリセットのセッションはシェルアクセスと同等に扱え</strong>と結論している。ベンダーが自ら「これは事実上のRCEである」と同梱ファイルに書いているケースは珍しく、評価に値する誠実さだ。ただし当然ながら、<strong>信頼できない入力を扱うタスクや共有環境での常用には向かない</strong>。用途はカスタムプリセットの作成に限定するのが設計意図に沿う。</p>

<div class="box-warning">
  <p><strong>Creatorモードを使う前に</strong></p>

  <p>ベンダー自身が同梱ファイルで「シェルアクセス相当として扱え」と明記しているモードである。Web上のコンテンツを読ませるタスク、他人から受け取ったプロンプト、CI上での自動実行といった用途では選ばないこと。プリセット作成が終わったらStandardへ戻すのが安全側の運用になる。</p>
</div>

<p>なお、プリセットの信頼レベル（<code class="language-plaintext highlighter-rouge">trust</code>）は<code class="language-plaintext highlighter-rouge">preset.yml</code>に書いても効かない。実装上<code class="language-plaintext highlighter-rouge">trust</code>は<strong>そのプリセットが発見されたルートディレクトリから決まる</strong>設計で、表示メタデータのファイルからは<code class="language-plaintext highlighter-rouge">name</code> / <code class="language-plaintext highlighter-rouge">description</code> / <code class="language-plaintext highlighter-rouge">order</code>しか読まれない。ローカルで作ったプリセットが「同梱プリセットである」と自称できないようにするための作りで、こうした細部の詰め方は全体的に丁寧である。</p>

<h3 id="自作プリセットの作られ方書き込み経路はまるごとコピー1本だけ">自作プリセットの作られ方——書き込み経路は「まるごとコピー」1本だけ</h3>

<p>Creatorモードが書く先、つまりユーザーが自分でプリセットを作る仕組みにも、いくつか意図的な制限が入っている。実装を読むと次のようになっていた。</p>

<p>・<strong>書き込み操作はディレクトリのまるごとコピーだけ</strong>。構成テキストそのものをAPI越しに渡す経路が無い。コピー元は既存プリセットのIDで指定するため、コピーによって新しい権限が生まれることがない<br />
・<strong>プリセットIDは<code class="language-plaintext highlighter-rouge">^[a-z0-9][a-z0-9-]*$</code>に限定</strong>される。IDがそのままディレクトリ名になるため、これはスタイル規約ではなく<code class="language-plaintext highlighter-rouge">..</code>やパス区切りでルート外へ出るのを防ぐ封じ込め境界として書かれている<br />
・<strong>コピー先は<code class="language-plaintext highlighter-rouge">user</code>ルート（<code class="language-plaintext highlighter-rouge">$DSH_HOME/.agent-presets</code>）のみ</strong>。同梱プリセットは「デプロイの一部」として扱われ、上書きも削除もできない<br />
・<strong>コピー後にパーミッションを絞る</strong>。同梱プリセットはワールドリーダブルなことがあるため、コピー後にディレクトリ0700・ファイル0600へ落とし直す<br />
・<strong>コピーは決して上書きしない</strong>。既存IDと衝突した場合はエラーになり、途中で失敗したらコピー先を消してから例外を投げる</p>

<p>同じプリセットを複数のセッションが使う場合、構成は<strong>プリセットごとに1回だけマウントされて共有される</strong>（standing mount）。ファイルを編集した場合は更新時刻とサイズの組で変更が検知され、それ以降に作られたセッションから新しい世代に切り替わる。すでに走っているセッションは<strong>マウント時の世代のまま</strong>動き続けるため、作業中に構成を書き換えても進行中のセッションの挙動が突然変わることはない。</p>

<p>細かい話に見えるが、「エージェントがエージェントの設定を書く」という構図では、こうした境界の置き方がそのままリスクの大きさを決める。DeepSeek Harnessはここを<strong>能力を増やさない方向</strong>に倒している。</p>

<h2 id="起動しない時のために2段の起動ゲートを特定した">起動しない時のために——2段の起動ゲートを特定した</h2>

<p>ここからは実用寄りの話をする。<code class="language-plaintext highlighter-rouge">npx @deepseek-ai/dsh web</code>が動かないという報告は公開直後から出ているが、原因は大きく2つに分かれる。実測で境界を特定した。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/dsh-boot-gate.png" width="1280" height="320" alt="DeepSeek Harnessの2段の起動ゲート。Node 22.14.0以下はzstdで落ち、22.15.0でAPIキーのゲートに進む" />
  <figcaption>Nodeのマイナーバージョンを並べて実測した起動ゲート</figcaption>
</figure>

<h3 id="第1のゲートnode-22150未満は起動しない">第1のゲート：Node 22.15.0未満は起動しない</h3>

<p><code class="language-plaintext highlighter-rouge">dsh</code>のセッション永続化プラグイン<code class="language-plaintext highlighter-rouge">dsh-session-persistence-jsonl</code>は、<code class="language-plaintext highlighter-rouge">node:zlib</code>から<code class="language-plaintext highlighter-rouge">createZstdDecompress</code>をインポートする。この関数はNode 22系では<strong>22.15.0で追加された</strong>ものだ。それ未満のNodeでは、プラグイン読み込みの段階でSyntaxErrorになり、ハーネスは何も表示せずに落ちる。</p>

<p>公式tarballで3つのマイナー版を並べ、同じコマンドを実行した結果が以下である。</p>

<table>
  <thead>
    <tr>
      <th>Node</th>
      <th><code class="language-plaintext highlighter-rouge">zlib.createZstdDecompress</code></th>
      <th><code class="language-plaintext highlighter-rouge">dsh --profile headless "say hi"</code> の結果</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>22.13.1</td>
      <td><code class="language-plaintext highlighter-rouge">undefined</code></td>
      <td><strong>起動失敗</strong>（SyntaxError: <code class="language-plaintext highlighter-rouge">createZstdDecompress</code>が無い）</td>
    </tr>
    <tr>
      <td>22.14.0</td>
      <td><code class="language-plaintext highlighter-rouge">undefined</code></td>
      <td><strong>起動失敗</strong>（同上）</td>
    </tr>
    <tr>
      <td>22.15.0</td>
      <td><code class="language-plaintext highlighter-rouge">function</code></td>
      <td>起動成功 → 次のゲート（APIキー）へ</td>
    </tr>
  </tbody>
</table>

<p>厄介なのは、<strong><code class="language-plaintext highlighter-rouge">@deepseek-ai/dsh</code>のpackage.jsonに<code class="language-plaintext highlighter-rouge">engines</code>フィールドが無い</strong>ことだ。そのためnpmもpnpmも「Nodeのバージョンが足りない」という警告を出さない。ユーザーが最初に見るのは、<code class="language-plaintext highlighter-rouge">node_modules</code>の奥深くを指す読みにくいスタックトレースになる。</p>

<p>（余談だが、依存として入る<code class="language-plaintext highlighter-rouge">@earendil-works/pi-ai</code>は<code class="language-plaintext highlighter-rouge">node: &gt;=22.19.0</code>を宣言しているため、こちらは<code class="language-plaintext highlighter-rouge">EBADENGINE</code>警告が出る。宣言しているパッケージとしていないパッケージが混在している状態だ。）</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 起動する前にこれだけ確認しておく</span>
node <span class="nt">-v</span>   <span class="c"># v22.15.0 以上であること</span>
</code></pre></div></div>

<h3 id="第2のゲートapiキー">第2のゲート：APIキー</h3>

<p>Nodeを22.15.0以上にすると、今度は明快なメッセージで止まる。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dsh: MISSING_CREDENTIAL: llm-deepseek: no API key for provider route
"deepseek-official"; store DEEPSEEK_API_KEY through the credentials service
(the web Models page writes it), or export DEEPSEEK_API_KEY in the launching
environment
</code></pre></div></div>

<p>第1のゲートと違い、こちらは<strong>何をすればよいかがそのまま書いてある</strong>。既定のモデルルートは<code class="language-plaintext highlighter-rouge">deepseek-official</code>、既定モデルは<code class="language-plaintext highlighter-rouge">deepseek-v4-flash</code>である。</p>

<p>ここで実務上有用な挙動が一つある。<strong>Web UI（<code class="language-plaintext highlighter-rouge">dsh web</code>）はAPIキーが無くても起動する</strong>。ブラウザでは初回に「Add an API key to get started」というダイアログが出るが、「Configure later」で閉じれば、UIの探索・プリセットの確認・設定画面の閲覧まではキー無しで行える。本記事冒頭のスクリーンショットは、まさにその状態で撮影したものだ。触ってみるだけならキーの用意は後回しでよい。</p>

<h2 id="サンドボックスは3osで別実装そして効かない時は止まる">サンドボックスは3OSで別実装——そして「効かない時は止まる」</h2>

<p>エージェント基盤を評価するとき、当サイトが最も重視するのは<strong>ツール実行の封じ込め</strong>である。DeepSeek Harnessの<code class="language-plaintext highlighter-rouge">dsh-sandbox-local</code>は、OSごとに別のランナーを選ぶ設計になっている。</p>

<table>
  <thead>
    <tr>
      <th>OS</th>
      <th>封じ込め方式</th>
      <th>実測した状態（macOS arm64から）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Linux</td>
      <td>bwrap → Landlock の順に候補を試す</td>
      <td>プリビルドあり（<code class="language-plaintext highlighter-rouge">linux-x64</code> / <code class="language-plaintext highlighter-rouge">linux-arm64</code>）</td>
    </tr>
    <tr>
      <td>macOS</td>
      <td>Seatbelt（<code class="language-plaintext highlighter-rouge">sandbox-exec</code>）</td>
      <td>Landlockランチャは<code class="language-plaintext highlighter-rouge">unusable</code>、Seatbelt側が担当</td>
    </tr>
    <tr>
      <td>Windows</td>
      <td>ACL制限トークン・ランナー</td>
      <td>ワークスペース単位のSID＋セッション単位の一時ディレクトリ</td>
    </tr>
  </tbody>
</table>

<p>重要なのは、実装コメントが明言する次の一点だ。<strong>封じ込めが存在しない、または使えない場合、元のargvをそのまま返すのではなく失敗する（fail-closed）</strong>。効かないなら素通しではなく止まる、という側に倒してある。</p>

<figure>
  <img loading="lazy" src="/generated_images/sections/dsh-sandbox-matrix.png" width="1280" height="430" alt="DeepSeek Harnessのサンドボックス実装。LinuxはbwrapとLandlock、macOSはSeatbelt、WindowsはACL制限トークンで、いずれもfail-closed" />
  <figcaption>OSごとに別のランナーを選ぶ設計。macOSでのLandlock probe結果は実測値</figcaption>
</figure>

<p>Landlockランチャ（<code class="language-plaintext highlighter-rouge">@deepseek-ai/node-addon-landlock-run</code>）単体でも同じ思想が徹底されている。ランチャ自身にLandlockのルールセットを適用してから対象コマンドを<code class="language-plaintext highlighter-rouge">exec</code>し、ルールセットは<code class="language-plaintext highlighter-rouge">execve</code>をまたいで継承されるためプロセスツリー全体が拘束される。<strong>許可していないものはすべて拒否</strong>され、ランチャの失敗は<strong>終了コード125</strong>でコマンドを実行しないまま終わる。C言語のソース（<code class="language-plaintext highlighter-rouge">src/main.c</code>）も監査用にtarballへ同梱されている。</p>

<p>実際にmacOSで確認した結果は以下のとおりだった。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>platform: darwin arm64
launcherPath(): .../node-addon-landlock-run-darwin-arm64/bin/landlock-run
probe(): unusable
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">darwin-arm64</code>というプラットフォームパッケージは公開されていないため、<code class="language-plaintext highlighter-rouge">launcherPath()</code>は<strong>存在しないパスを決定的に返し</strong>、<code class="language-plaintext highlighter-rouge">probe()</code>は<code class="language-plaintext highlighter-rouge">unusable</code>を返す。インストール時にソースからコンパイルするフォールバックは意図的に用意されていない。ここだけ見ると「macOSでは丸腰なのか」と読みたくなるが、そうではない。<strong>macOSではSeatbeltのランナーが選ばれる</strong>ため、封じ込め自体は機能する。Landlockはあくまでもうひとつの選択肢という位置づけだ。</p>

<p>隔離の粒度をVMレベルまで上げたい場合の設計思想については、<a href="/agent/agentenv-firecracker-microvm-agent-sandbox/">AgentENV：Firecracker microVMでAIエージェントを丸ごと隔離する</a>で扱ったアプローチと読み比べると輪郭がはっきりする。DeepSeek Harnessが選んだのはOSネイティブの機構を積み上げる方向で、microVMより軽い代わりに、実装がOSごとに分かれる。</p>

<h3 id="テレメトリは既定で無効送信先はハードコード">テレメトリは既定で無効、送信先はハードコード</h3>

<p>構成ダンプにはテレメトリの行も含まれている。</p>

<p>・プラグイン：<code class="language-plaintext highlighter-rouge">@deepseek-ai/dsh-session-telemetry-otel</code><br />
・モード：<code class="language-plaintext highlighter-rouge">process.env.DSH_TELEMETRY_MODE || 'DISABLED'</code> ——<strong>既定は無効</strong><br />
・送信先：<code class="language-plaintext highlighter-rouge">process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://harness-telemetry.deepseeksvc.com/v1/logs'</code>（gzip圧縮）</p>

<p>つまり<strong>環境変数で明示的に有効化しない限り送信は行われない</strong>。エンドポイントのURLは構成に直接書かれているが、環境変数で差し替えられる。また、npmインストール時には<code class="language-plaintext highlighter-rouge">@deepseek-ai/dsh-anonymous-user-id</code>という匿名IDを扱うパッケージも同時に入る。テレメトリを有効化する運用を検討するなら、この2つをセットで確認しておくのが妥当だ。既定のまま使う限り、送信経路は無効である。</p>

<h2 id="モデル非依存はどこまで本当か">モデル非依存はどこまで本当か</h2>

<p>「model-agnostic」は公開時のアナウンスで強調された点だ。ここは数え方で印象が変わるため、測った事実だけを分けて書く。</p>

<p>・<strong>プロバイダー実装の層</strong>：モデル接続に使われる<code class="language-plaintext highlighter-rouge">@earendil-works/pi-ai</code>には、<code class="language-plaintext highlighter-rouge">dist/providers</code>配下に<strong>43個</strong>のプロバイダーモジュールが同梱されている（Anthropic、OpenAI、Amazon Bedrock、Google Vertex、Azure、Groq、Mistral、xAI、OpenRouter、Moonshot、MiniMax、Qwen、Cerebras、Fireworks、Together、NVIDIA、GitHub Copilot ほか）<br />
・<strong>プロトコルの層</strong>：DeepSeek側のアダプタ<code class="language-plaintext highlighter-rouge">dsh-llm-pi-ai</code>が名前として持つ文字列は<code class="language-plaintext highlighter-rouge">deepseek</code> / <code class="language-plaintext highlighter-rouge">openai</code> / <code class="language-plaintext highlighter-rouge">openai-completions</code> / <code class="language-plaintext highlighter-rouge">openai-responses</code> / <code class="language-plaintext highlighter-rouge">anthropic-messages</code> / <code class="language-plaintext highlighter-rouge">openrouter</code> の<strong>6種</strong><br />
・<strong>既定のルート</strong>：<code class="language-plaintext highlighter-rouge">deepseek-official</code>＋<code class="language-plaintext highlighter-rouge">deepseek-v4-flash</code></p>

<p>この6という数は「対応プロバイダーが6社」という意味ではない。<strong>プロトコルの種類</strong>であり、OpenAI互換エンドポイントやAnthropic Messages互換のエンドポイントを持つサービスは、この6種のいずれかに載って接続される。したがってBedrockやVertexが使えないという結論にはならない。プロバイダー層とプロトコル層は別の数え方だ、というのが正確な整理である。</p>

<p>いずれにせよ、<strong>モデル接続がプラグインとして差し替え可能</strong>であること自体は構成ダンプで確認できる。<code class="language-plaintext highlighter-rouge">dsh-llm</code>（インターフェース）、<code class="language-plaintext highlighter-rouge">dsh-llm-deepseek</code>、<code class="language-plaintext highlighter-rouge">dsh-llm-pi-ai</code>、<code class="language-plaintext highlighter-rouge">dsh-llm-retry</code>がそれぞれ独立した行として並んでおり、他のベンダー用アダプタを足す余地は構造的に開いている。</p>

<h2 id="日本語uiは無いja-jpのフォールバック先は英語ではなく中国語">日本語UIは無い——ja-JPのフォールバック先は英語ではなく中国語</h2>

<p>日本語話者にとって実務的に効く発見をひとつ。<strong>同梱ロケールは<code class="language-plaintext highlighter-rouge">en</code>と<code class="language-plaintext highlighter-rouge">zh</code>の2つだけ</strong>である。<code class="language-plaintext highlighter-rouge">dsh-client-locale</code>のコードに現れるロケール識別子もこの2つに限られる。</p>

<p>問題はフォールバック先だ。ブラウザのAccept-Languageを変えて実測した結果は次のようになった。</p>

<table>
  <thead>
    <tr>
      <th>ブラウザのロケール</th>
      <th>実際に表示されたUI</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">en-US</code></td>
      <td>英語</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">zh-CN</code></td>
      <td>中国語</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ja-JP</code></td>
      <td><strong>中国語</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ko-KR</code></td>
      <td><strong>中国語</strong></td>
    </tr>
  </tbody>
</table>

<p>つまり、日本語環境のブラウザでそのまま開くと、英語ではなく<strong>中国語のUIが出る</strong>。初回の「内测声明（クローズドベータの告知）」ダイアログも中国語で表示される。</p>

<figure>
  <img loading="lazy" src="/generated_images/deepseek-harness-locale-ja.png" width="1440" height="900" alt="日本語ロケールのブラウザで開いたDeepSeek Harnessの画面。UIが中国語で表示されている" />
  <figcaption>Accept-Languageを<code>ja-JP</code>にして開いた実際の画面。「新会话」「工作区」「设置」と中国語で表示される</figcaption>
</figure>

<p>英語UIで使いたい場合は、ブラウザの言語設定で英語を優先にすればよい。プリセット名も<code class="language-plaintext highlighter-rouge">标准模式</code>→<code class="language-plaintext highlighter-rouge">Standard mode</code>、<code class="language-plaintext highlighter-rouge">PTC 模式</code>→<code class="language-plaintext highlighter-rouge">Code mode</code>のように切り替わる。ちなみに中国語では<code class="language-plaintext highlighter-rouge">PTC 模式</code>、英語では<code class="language-plaintext highlighter-rouge">Code mode</code>と、<strong>同じプリセットが言語によって別の名前で提示される</strong>点も実測で確認できた事実として書き添えておく。</p>

<h2 id="土台のcordisはどこから来たのか">土台のCordisはどこから来たのか</h2>

<p>最後に、この構成を支えるCordisについて触れておく。DeepSeek Harnessは<code class="language-plaintext highlighter-rouge">@deepseek-ai/cordis</code>に依存しているが、これは<code class="language-plaintext highlighter-rouge">cordiverse/cordis</code>をそのまま参照しているのではない。リポジトリの<code class="language-plaintext highlighter-rouge">vendor/</code>配下に取り込んだうえで、<code class="language-plaintext highlighter-rouge">@deepseek-ai</code>スコープで再公開したものだ。</p>

<p><code class="language-plaintext highlighter-rouge">vendor/</code>に置かれているのは9つのパッケージである。<code class="language-plaintext highlighter-rouge">cordis</code> / <code class="language-plaintext highlighter-rouge">cosmokit</code> / <code class="language-plaintext highlighter-rouge">schemastery</code> / <code class="language-plaintext highlighter-rouge">group</code> / <code class="language-plaintext highlighter-rouge">hmr</code> / <code class="language-plaintext highlighter-rouge">include</code> / <code class="language-plaintext highlighter-rouge">loader</code> / <code class="language-plaintext highlighter-rouge">logger-console</code> / <code class="language-plaintext highlighter-rouge">timer</code>。いずれもCordis（元をたどればチャットボットフレームワークKoishiのエコシステム）由来で、package.jsonのauthorには原作者の名前がそのまま残っている。ソースコードは公開リポジトリに含まれており、隠されてはいない。</p>

<p>興味深いのはバージョンだ。</p>

<p>・上流 <code class="language-plaintext highlighter-rouge">cordis</code>（npm）：<strong>4.0.0-rc.8</strong>（リリース候補）<br />
・<code class="language-plaintext highlighter-rouge">@deepseek-ai/cordis</code>：<strong>4.0.1</strong>（安定版）</p>

<p>つまりDeepSeekは、上流がまだリリース候補の段階にあるメタフレームワークについて、<strong>自社スコープで先に安定版を出した</strong>格好になる。ハーネス本体の互換性を自分たちのリリースサイクルで管理するための判断と読める。ライセンスはいずれもMITで、この取り込み方自体に問題はない。</p>

<p>なお、ホットリロード用の<code class="language-plaintext highlighter-rouge">cordis-plugin-hmr</code>は構成ダンプ上では<code class="language-plaintext highlighter-rouge">disabled: true</code>だった。「時空間的合成可能性」の<strong>時間</strong>側——プロセスを再起動せずにプラグインを差し替える能力——は、開発時に有効化して使う位置づけである。</p>

<h2 id="deepseek-harnessと既存エージェント基盤の違い">DeepSeek Harnessと既存エージェント基盤の違い</h2>

<p>当サイトではこれまでも複数のハーネスを扱ってきた。位置づけの違いを整理しておく。</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>DeepSeek Harness</th>
      <th>OpenHarness</th>
      <th>Flue</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>提供元</td>
      <td>DeepSeek AI</td>
      <td>HKUDS</td>
      <td>Astroチーム</td>
    </tr>
    <tr>
      <td>言語</td>
      <td>TypeScript</td>
      <td>Python</td>
      <td>TypeScript</td>
    </tr>
    <tr>
      <td>中心思想</td>
      <td>すべてがプラグイン（UIも含む）</td>
      <td>Claude Codeの中核をOSS化</td>
      <td>プログラム可能なハーネス</td>
    </tr>
    <tr>
      <td>UI</td>
      <td>Web UI同梱（プラグイン）</td>
      <td>CLI＋個人AI</td>
      <td>ライブラリ（UIなし）</td>
    </tr>
    <tr>
      <td>差し替え単位</td>
      <td>プラグイン行（構成YAML）</td>
      <td>設定・モジュール</td>
      <td>コード</td>
    </tr>
    <tr>
      <td>特徴的な検査</td>
      <td>マウント時の<code class="language-plaintext highlighter-rouge">isolate</code>レルム検査</td>
      <td>権限モード（Default/Auto/Plan）</td>
      <td>耐久実行・サブエージェント</td>
    </tr>
  </tbody>
</table>

<p>Claude Codeの構造をOSSとして再現するアプローチについては<a href="/agent/hkuds-openharness/">OpenHarness徹底解説：Claude Codeの仕組みをOSS化したエージェント基盤</a>で詳しく扱った。DeepSeek Harnessが違うのは、<strong>「完成品のエージェントを配る」のではなく「エージェントを組み立てる規約を配る」</strong>点にある。129行の構成ダンプも、4つのプリセットも、その規約の実例として読むのが正しい。</p>

<p>そもそもエージェントとハーネスの関係自体を整理したい場合は、<a href="/explain/what-is-ai-agent-2026/">AIエージェントとは何か——2026年の実装水準で理解する</a>が入口になる。</p>

<h2 id="まとめ標語は数えられた">まとめ——標語は数えられた</h2>

<p>「Everything is a Plugin」は、この規模のプロジェクトにしては珍しく、<strong>額面どおりだった</strong>というのが実測の結論である。</p>

<p>・標準プロファイルは129行のプラグイン行で構成され、そのうち25行は既定で無効。ツール類をホストが持たず、プリセットが有効化する分業が実装されている<br />
・Code ModeはStandardとの差分がプラグイン1枚（YAML 4行）だけ。モードの違いがプラグイン1枚で表現されている<br />
・プリセットのサービスは<code class="language-plaintext highlighter-rouge">isolate</code>レルムに入れなければマウント時に拒否される。合成可能性が規約でなく検査として存在する<br />
・サンドボックスはOSごとに別実装で、効かない場合は素通しでなく失敗する側に倒してある<br />
・Creatorモードは自らシェルアクセス相当と宣言している。誠実だが、使いどころは限定される</p>

<p>一方で、開発者プレビューらしい粗さも実測で出た。<code class="language-plaintext highlighter-rouge">engines</code>未宣言のままNode 22.15.0以上を要求する起動失敗、日本語ロケールで中国語UIが出るフォールバック、GitHubのタグ不在といった点である。いずれも致命的ではないが、<strong>「動かない」と報告する前に<code class="language-plaintext highlighter-rouge">node -v</code>を見る</strong>だけで解決する問題が含まれているのはもったいない。</p>

<p>DeepSeek Harnessは、モデルの会社が「モデル以外」を本気で配り始めたという意味で、2026年後半のエージェント基盤の議論に確実に影響する。まずは<code class="language-plaintext highlighter-rouge">node -v</code>を確認してから、キー無しでUIを立ち上げてみるところから始めるとよい。</p>

<h2 id="参照ソース">参照ソース</h2>

<p>・<a href="https://github.com/deepseek-ai/deepseek-harness">deepseek-ai/deepseek-harness — GitHub公式リポジトリ</a>（README、<code class="language-plaintext highlighter-rouge">vendor/</code>、<code class="language-plaintext highlighter-rouge">config/agent-presets/</code>。MITライセンス）<br />
・<a href="https://news.ycombinator.com/item?id=49285244">DeepSeek Harness developer preview — Hacker News</a>（2026年8月14日投稿、本稿執筆時点729ポイント）<br />
・<a href="https://deepseek.com/harness/en/">DeepSeek Harness 公式ページ</a><br />
・<a href="https://www.npmjs.com/package/@deepseek-ai/dsh">@deepseek-ai/dsh — npm</a>（0.1.0-rc.6、本記事の実測対象）<br />
・<a href="https://github.com/cordiverse/cordis">cordiverse/cordis — 上流のCordis</a></p>

<!--
Distribution notes (Step 8)
- X: 「『すべてがプラグイン』を実際に数えた」切り口。FVはプリセット選択画面のスクショ。
  自己リプで「Node 22.15.0未満は engines 未宣言のまま落ちる」を出すとエンジニア反応が取れる。
- はてブ: 起動ゲート表（Node 3バージョン実測）が単体で保存価値あり。
- 内部リンク提案: 今後 Cordis / Code Mode SDK 単体記事を書く場合は本記事をピラー側に。
- 未実施: leakedServices のマウント拒否をライブで再現するテスト（APIキーが必要なため source 読解に留めた）。
-->]]></content><author><name></name></author><category term="agent" /><category term="AIエージェント" /><category term="DeepSeek" /><category term="harness" /><category term="agent" /><category term="オープンソース" /><category term="TypeScript" /><category term="Cordis" /><summary type="html"><![CDATA[DeepSeek Harness（dsh）は2026年8月13日にMITで公開されたエージェント・ハーネス。「すべてがプラグイン」の実体を、標準webプロファイルの構成ダンプ129行・4プリセット・2段の起動ゲート・3OSのサンドボックス実装まで実測で確かめる。npxが落ちる原因のNodeバージョン境界も特定した。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_deepseek-harness.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_deepseek-harness.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">reverse-skill徹底検証｜AI逆解析スキルルーター41ルールを実測、日本語で空振りする条件</title><link href="https://ai-heartland.com/security/reverse-skill-ai-reverse-engineering-router/" rel="alternate" type="text/html" title="reverse-skill徹底検証｜AI逆解析スキルルーター41ルールを実測、日本語で空振りする条件" /><published>2026-08-16T10:00:00+09:00</published><updated>2026-08-16T10:00:00+09:00</updated><id>https://ai-heartland.com/security/reverse-skill-ai-reverse-engineering-router</id><content type="html" xml:base="https://ai-heartland.com/security/reverse-skill-ai-reverse-engineering-router/"><![CDATA[<p>AIエージェントにAPKやバイナリの解析をやらせようとすると、最初に詰まるのは解析そのものではなく「jadxなのかapktoolなのかFridaなのかIDAなのか」という道具選びだ。<strong>reverse-skill</strong>（<a href="https://github.com/zhaoxuya520/reverse-skill">zhaoxuya520/reverse-skill</a>）は、この振り分けを41本のルーティングルールに固定してしまうスキルルーターで、2026年8月16日時点でGitHub Star 25,491・フォーク3,450・MITライセンス。5月13日の公開から3か月でこの規模に達している。</p>

<p>本記事は、READMEの紹介ではなく <strong>リポジトリを実際にクローンしてルーターを走らせ、権限ゲートを1条件ずつ崩し、ツール自動導入のハッシュを実物と照合した記録</strong> である。結論から言うと、いちばん効くのは日本語話者にとっての落とし穴だった。</p>

<figure>
  <img src="/generated_images/reverse-skill-router-ab.gif" alt="reverse-skillのmaster-route.shに日本語ヒントを渡した実測。「このAPKを解析して」はconfidence: lowでR0へ転落し、半角スペースを入れた「この APK を解析して」はR1へconfidence: highで届く" width="960" height="540" style="width:100%;height:auto;border-radius:.5rem;" />
  <figcaption>同じ内容の日本語ヒントでも、英数字トークンの前後に半角スペースがあるかどうかでPRIMARYが変わる（macOS 14.5 / Python 3 / 2026-08-16 実測。GIFは実際の出力から作成）</figcaption>
</figure>

<div class="box-tip">
  <p><strong>この記事のポイント（30秒でわかる）</strong></p>

  <p>・<strong>PRIMARY判定にLLMは入っていない</strong>。<code class="language-plaintext highlighter-rouge">routing.json</code> の41ルート・49キーワード規則を正規表現で照合し、命中数で採点するだけの決定論的な処理<br />
・<strong>日本語ヒントは空振りしうる</strong>。<code class="language-plaintext highlighter-rouge">\bapk\b</code> のような単語境界が日本語の連続文字列では成立せず、「このAPKを解析して」はR0へ転落した。半角スペース1つで直る<br />
・<strong>166ケースの回帰テストは全て通るが、日本語は1件も無い</strong>（中国語97・英語69・日本語0）。CIが緑でも日本語入力は守られていない<br />
・<strong>ACT前の権限ゲートは実在し、節をまたいだ偽装は弾いた</strong>。ただし <code class="language-plaintext highlighter-rouge">--force</code> で全条件が警告に降格する<br />
・<strong>GitHubリリースのSHA-256照合は本物</strong>。jadx v1.5.6 を実際に落として照合し一致を確認した。ただし事前ハッシュがあるのは24能力中2件</p>
</div>

<p>道具の自動導入を伴うスキルパックは、それ自体が開発環境への新しい侵入経路になりうる。前提として押さえておきたい攻撃面の全体像は<a href="/security/supply-chain-security-guide-2026/">サプライチェーンセキュリティ2026｜攻撃手法・防御ツール・実践チェックリスト</a>にまとめてある。</p>

<h2 id="reverse-skillとは逆解析タスクを41ルールで振り分けるスキルパック">reverse-skillとは——逆解析タスクを41ルールで振り分けるスキルパック</h2>

<p>reverse-skillが解こうとしている問題は、READMEの “Why this exists” に4行で書かれている。AIエージェントは与えられた対象に対してどのツールを使うべきか知らない、APK・ELF・JS・PCAP・CTFはそれぞれ別の手順書を必要とする、ツールとMCPサーバーはマシンごとに散らばっている、経験が再利用されないので同じ失敗を繰り返す——の4つだ。</p>

<p>対処として置かれているのが「ルーティング」「スキルモジュール」「ケース管理」「ツールチェーン自動導入」の4層である。実際のリポジトリを数えると、規模はこうなる。</p>

<table>
  <thead>
    <tr>
      <th>要素</th>
      <th>実測値</th>
      <th>確認方法</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ルーティングルール</td>
      <td>41（R0〜R40）</td>
      <td><code class="language-plaintext highlighter-rouge">skills/config/routing.json</code> を集計</td>
    </tr>
    <tr>
      <td>キーワード規則</td>
      <td>49</td>
      <td>同上（1ルートに複数規則あり）</td>
    </tr>
    <tr>
      <td>スキルモジュール</td>
      <td>42</td>
      <td><code class="language-plaintext highlighter-rouge">skills/</code> 配下の <code class="language-plaintext highlighter-rouge">SKILL.md</code> は全43個で、うち1つは総合入口の <code class="language-plaintext highlighter-rouge">skills/SKILL.md</code>。残る42個が <code class="language-plaintext highlighter-rouge">INDEX.md</code> の登録数と一致</td>
    </tr>
    <tr>
      <td>CTFサブスキル</td>
      <td>42</td>
      <td><code class="language-plaintext highlighter-rouge">CTF-Sandbox-Orchestrator/</code> 直下のディレクトリ数（<code class="language-plaintext highlighter-rouge">competition-*</code> が41＋オーケストレータ本体1）</td>
    </tr>
    <tr>
      <td>自動導入対象ツール</td>
      <td>24能力</td>
      <td><code class="language-plaintext highlighter-rouge">skills/scripts/bootstrap-manifest.json</code></td>
    </tr>
    <tr>
      <td>回帰テストケース</td>
      <td>166</td>
      <td><code class="language-plaintext highlighter-rouge">skills/tests/routing-benchmark.json</code></td>
    </tr>
    <tr>
      <td>リポジトリ総ファイル数</td>
      <td>586</td>
      <td><code class="language-plaintext highlighter-rouge">git clone --depth 1</code> 後に集計</td>
    </tr>
  </tbody>
</table>

<p>スキルモジュールの内訳は、<code class="language-plaintext highlighter-rouge">apk-reverse</code> / <code class="language-plaintext highlighter-rouge">mobile-reverse</code> / <code class="language-plaintext highlighter-rouge">ida-reverse</code> / <code class="language-plaintext highlighter-rouge">radare2</code> / <code class="language-plaintext highlighter-rouge">ghidra-reverse</code> / <code class="language-plaintext highlighter-rouge">js-reverse</code> / <code class="language-plaintext highlighter-rouge">dotnet-reverse</code> / <code class="language-plaintext highlighter-rouge">macos-reverse</code> / <code class="language-plaintext highlighter-rouge">go-rust-reverse</code> といった解析側と、<code class="language-plaintext highlighter-rouge">malware-analysis</code> / <code class="language-plaintext highlighter-rouge">digital-forensics</code> / <code class="language-plaintext highlighter-rouge">threat-hunting</code> / <code class="language-plaintext highlighter-rouge">supply-chain-security</code> / <code class="language-plaintext highlighter-rouge">llm-security</code> といった防御・分析側が混在している。攻撃寄りのモジュール（<code class="language-plaintext highlighter-rouge">attack-chain</code> / <code class="language-plaintext highlighter-rouge">pwn-chain</code> / <code class="language-plaintext highlighter-rouge">edr-bypass-re</code> など）も含まれるが、いずれも後述する権限ゲートを通した「認可済みの対象」を前提にした設計になっている。本記事では手法カタログには立ち入らず、<strong>振り分けの仕組みと、実行を止める側の実装</strong>だけを扱う。</p>

<p>ライセンスはリポジトリ本体がMIT、<code class="language-plaintext highlighter-rouge">CTF-Sandbox-Orchestrator/</code> 配下のみGPLv3という二層構成だ。READMEは Pentest Swarm AI（AGPL-3.0）について「CLIまたはMCC経由で呼び出すだけでソースは含まない」と明記しており、ライセンス境界の扱いは丁寧な部類に入る。</p>

<figure>
  <img src="/generated_images/sections/reverse-skill-h2-1.webp" alt="reverse-skillのPRIMARY決定フロー。ヒント文字列を49規則と照合し、命中数で採点、同点はpriority順、無命中ならR0にフォールバックする" loading="lazy" width="1280" height="320" style="width:100%;height:auto;" />
  <figcaption>PRIMARY決定の4段。LLM推論は入らない（`skills/config/routing.json` と `skills/scripts/master-route.sh` を実読して作成）</figcaption>
</figure>

<h3 id="検証環境">検証環境</h3>

<p>以下すべて、次の環境で実際に実行した結果である。</p>

<p>・macOS 14.5（Darwin 23.5.0 / Apple Silicon）<br />
・Python 3（bash版ルーターが依存）、Node.js、bash 3.2<br />
・reverse-skill を <code class="language-plaintext highlighter-rouge">git clone --depth 1</code> した 2026-08-16 時点の <code class="language-plaintext highlighter-rouge">main</code>（VERSION は 1.0.1）<br />
・PowerShellランタイム（<code class="language-plaintext highlighter-rouge">pwsh</code>）は<strong>未導入</strong>。したがって <code class="language-plaintext highlighter-rouge">.ps1</code> のみで提供されるスクリプトは実行していない（後述）</p>

<h2 id="reverse-skillのルーティングの正体ai判定ではなく正規表現の採点だった">reverse-skillのルーティングの正体——AI判定ではなく正規表現の採点だった</h2>

<p>リポジトリの説明文には “AI-powered routing”、中国語側には「AI 自动路由」とある。ここが最初の検証ポイントになった。付属の <code class="language-plaintext highlighter-rouge">skills/scripts/master-route.sh</code> を読むと、実体はこうだ。</p>

<p>bashスクリプトはヒアドキュメントでPythonを起動し、<code class="language-plaintext highlighter-rouge">routing.json</code> を読み込む。各ルートの <code class="language-plaintext highlighter-rouge">keywords</code> 配列には <code class="language-plaintext highlighter-rouge">must</code>（必須パターン）、<code class="language-plaintext highlighter-rouge">mustAll</code>（追加の必須条件）、<code class="language-plaintext highlighter-rouge">exclude</code>（除外パターン）が入っていて、これを <code class="language-plaintext highlighter-rouge">re.search(..., re.IGNORECASE)</code> で照合する。命中したルールごとに1点を加算し、<code class="language-plaintext highlighter-rouge">priority</code> 配列の順に見て最高得点のルートをPRIMARYとする。同点なら <code class="language-plaintext highlighter-rouge">priority</code> で先に定義された側が勝つ。1つも命中しなければ <code class="language-plaintext highlighter-rouge">meta.fallbackId</code>（<code class="language-plaintext highlighter-rouge">R0</code>）へ落ちる。</p>

<p>つまり <strong>モデル呼び出しもAPIキーも一切登場しない</strong>。<code class="language-plaintext highlighter-rouge">routing.json</code> 自身の <code class="language-plaintext highlighter-rouge">meta.scoring</code> フィールドにも、この採点方式が中国語で明記されている。confidence の決まり方も単純で、命中したルートが1つだけなら <code class="language-plaintext highlighter-rouge">high</code>、2つ以上なら <code class="language-plaintext highlighter-rouge">medium</code>、0なら <code class="language-plaintext highlighter-rouge">low</code> だ。</p>

<div class="mermaid">
flowchart TD
    A["ヒント文字列を小文字化"] --&gt; B{"49のキーワード規則を<br />正規表現で照合"}
    B --&gt;|"must が命中"| C{"mustAll を<br />すべて満たす?"}
    B --&gt;|"命中なし"| F["候補ゼロ"]
    C --&gt;|"No"| F
    C --&gt;|"Yes"| D{"exclude に<br />該当する?"}
    D --&gt;|"Yes"| F
    D --&gt;|"No"| E["候補に加点（+1）"]
    E --&gt; G{"候補は何本?"}
    F --&gt; H["R0 へフォールバック<br />confidence: low"]
    G --&gt;|"1本"| I["PRIMARY 確定<br />confidence: high"]
    G --&gt;|"2本以上"| J["priority 順で最高得点<br />confidence: medium"]
    H --&gt; K["route-scope.md を書き出す"]
    I --&gt; K
    J --&gt; K
</div>

<p>これは欠点ではない。むしろ<strong>設計上の美点</strong>として読むべきところで、決定論的だからこそ166ケースの回帰テストが書けるし、CIで固定できる。実際にmacOSでベンチマークを走らせると全件通った。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># reverse-skill をクローンして回帰テストを実行（Python 3 が必要）</span>
git clone <span class="nt">--depth</span> 1 https://github.com/zhaoxuya520/reverse-skill.git
<span class="nb">cd </span>reverse-skill <span class="o">&amp;&amp;</span> bash skills/scripts/test-routing.sh
<span class="c"># =&gt; TOTAL=166 PASS=166 FAIL=0 / 実行時間 約8秒（macOS 14.5 実測）</span>
</code></pre></div></div>

<p>問題は「AI-powered routing」という説明文から読者が想像するもの——自然言語を理解して意図を汲む振り分け——と、実装（正規表現の完全一致的な照合）のあいだにあるギャップだ。そしてそのギャップは、日本語で使ったときに具体的な形で表面化する。</p>

<div class="box-warning">
  <p><strong>READMEの数値と実物の差（軽微）</strong></p>

  <p>READMEとCHANGELOG v1.0.1（2026-08-08）は回帰ベンチマークを「163ケース」と記載しているが、<code class="language-plaintext highlighter-rouge">routing-benchmark.json</code> の実際の件数は166で、ランナーも <code class="language-plaintext highlighter-rouge">TOTAL=166</code> と出力する。v1.0.1リリース後に3件追加され、ドキュメント側が追いついていないだけと見られる。実装の欠陥ではないが、件数を引用するときは実ファイルを数えたほうがよい。</p>
</div>

<h2 id="日本語ヒントでreverse-skillのルーターが空振りする条件と回避策">日本語ヒントでreverse-skillのルーターが空振りする条件と回避策</h2>

<p>冒頭のGIFがこの節の全てだが、条件を切り分けて確認した。まず素の状態。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 日本語のヒントをそのまま渡す</span>
bash skills/scripts/master-route.sh <span class="nt">--hint</span> <span class="s2">"このAPKを解析して"</span>
<span class="c"># PRIMARY -&gt; skills/reverse-engineering/SKILL.md</span>
<span class="c"># Label: General reverse-engineering | confidence: low</span>
<span class="c"># NOTE: No strong keyword hit; open routing.md full matrix</span>
</code></pre></div></div>

<p>APKと明記しているのにAPK用スキル（R1）へ届かず、汎用フォールバック（R0）に落ちている。原因は <code class="language-plaintext highlighter-rouge">routing.json</code> のR1側の定義にある。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"must": "\\bapk\\b|smali|jadx|apktool|\\bandroid\\b|android.?reverse|安卓|反编译.?apk|..."
</code></pre></div></div>

<p>先頭が <code class="language-plaintext highlighter-rouge">\bapk\b</code>、つまり<strong>単語境界</strong>で囲まれている。Pythonの正規表現では、str型パターンの <code class="language-plaintext highlighter-rouge">\w</code> は既定でUnicodeの単語文字にマッチし、漢字・ひらがな・カタカナはいずれも単語文字として扱われる。したがって「このAPKを解析して」を小文字化した <code class="language-plaintext highlighter-rouge">このapkを解析して</code> では、<code class="language-plaintext highlighter-rouge">a</code> の直前が <code class="language-plaintext highlighter-rouge">の</code>（単語文字）、<code class="language-plaintext highlighter-rouge">k</code> の直後が <code class="language-plaintext highlighter-rouge">を</code>（単語文字）となり、<strong>両端とも境界が成立しない</strong>。文字列としてはAPKを含んでいるのに、正規表現としては1文字も命中しない。</p>

<p>英数字トークンの前後に半角スペースを1つ入れるだけで、結果は反転する。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># APK の前後に半角スペースを入れただけ</span>
bash skills/scripts/master-route.sh <span class="nt">--hint</span> <span class="s2">"この APK を解析して"</span>
<span class="c"># PRIMARY -&gt; skills/apk-reverse/SKILL.md</span>
<span class="c"># Label: APK reverse | confidence: high</span>
</code></pre></div></div>

<p>同じ条件を並べて確認した実測結果が次の表だ。入力文の意味は変えず、区切りだけを変えている。</p>

<table>
  <thead>
    <tr>
      <th>ヒント（入力）</th>
      <th>PRIMARY</th>
      <th>confidence</th>
      <th>判定</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">apk</code></td>
      <td>R1 apk-reverse</td>
      <td>high</td>
      <td>✅ 通る</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">このAPKを解析して</code></td>
      <td>R0 reverse-engineering</td>
      <td>low</td>
      <td>❌ 空振り</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">この APK を解析して</code></td>
      <td>R1 apk-reverse</td>
      <td>high</td>
      <td>✅ 通る</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">APKを解析して</code></td>
      <td>R0 reverse-engineering</td>
      <td>low</td>
      <td>❌ 空振り</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">androidアプリの解析</code></td>
      <td>R0 reverse-engineering</td>
      <td>low</td>
      <td>❌ 空振り</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">android アプリの解析</code></td>
      <td>R1 apk-reverse</td>
      <td>high</td>
      <td>✅ 通る</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">分析apk</code>（中国語）</td>
      <td>R0 reverse-engineering</td>
      <td>low</td>
      <td>❌ 空振り</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">安卓app分析</code>（中国語）</td>
      <td>R1 apk-reverse</td>
      <td>high</td>
      <td>✅ 通る</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">抓包分析</code>（中国語）</td>
      <td>R3 js-reverse</td>
      <td>high</td>
      <td>✅ 通る</td>
    </tr>
  </tbody>
</table>

<p>4行目に注目してほしい。<code class="language-plaintext highlighter-rouge">APKを解析して</code> は文頭にAPKがあるので前側の境界は成立するが、<code class="language-plaintext highlighter-rouge">k</code> の直後が <code class="language-plaintext highlighter-rouge">を</code> なので<strong>後ろ側で落ちる</strong>。境界は両端必要なので、片方が塞がれば同じことだ。</p>

<p>中国語話者が実質的に困らない理由も表から読める。<code class="language-plaintext highlighter-rouge">安卓</code>（Android）や <code class="language-plaintext highlighter-rouge">抓包</code>（パケットキャプチャ）といった中国語キーワードが規則側に多数登録されており、これらは単語境界を使っていないため連続文字列の中でも命中する。実際 <code class="language-plaintext highlighter-rouge">routing.json</code> の41ルート中<strong>39ルートに中国語キーワードが含まれている</strong>。一方で、<strong>ひらがな・カタカナは <code class="language-plaintext highlighter-rouge">routing.json</code> 全体で1文字も出現しない</strong>（0文字）。日本語話者だけが、ASCIIトークンに頼らざるを得ないうえ、日本語は分かち書きしないためそのASCIIトークンが境界で落ちる、という二重の不利を負う。</p>

<figure>
  <img src="/generated_images/sections/reverse-skill-h2-2.webp" alt="日本語ヒントで空振りする書き方と通る書き方の比較。連続表記はR0へ転落し、半角スペース入りはR1へ届く" loading="lazy" width="1280" height="430" style="width:100%;height:auto;" />
  <figcaption>空振りする書き方と通る書き方（`master-route.sh` の実行結果から作成）</figcaption>
</figure>

<h3 id="影響範囲どのルートが日本語で落ちるか">影響範囲——どのルートが日本語で落ちるか</h3>

<p>単語境界を使っているのは41ルート中<strong>18ルート</strong>で、そこで <code class="language-plaintext highlighter-rouge">\b</code> に囲まれているASCIIトークンは21種類ある。</p>

<p><code class="language-plaintext highlighter-rouge">apk</code> / <code class="language-plaintext highlighter-rouge">android</code> / <code class="language-plaintext highlighter-rouge">ipa</code> / <code class="language-plaintext highlighter-rouge">ida</code> / <code class="language-plaintext highlighter-rouge">r2</code> / <code class="language-plaintext highlighter-rouge">oauth</code> / <code class="language-plaintext highlighter-rouge">pwn</code> / <code class="language-plaintext highlighter-rouge">report</code> / <code class="language-plaintext highlighter-rouge">k8s</code> / <code class="language-plaintext highlighter-rouge">ad</code> / <code class="language-plaintext highlighter-rouge">sigma</code> / <code class="language-plaintext highlighter-rouge">ot</code> / <code class="language-plaintext highlighter-rouge">ics</code> / <code class="language-plaintext highlighter-rouge">crx</code> / <code class="language-plaintext highlighter-rouge">xpi</code> / <code class="language-plaintext highlighter-rouge">golang</code> / <code class="language-plaintext highlighter-rouge">rustc</code> / <code class="language-plaintext highlighter-rouge">mysql</code> / <code class="language-plaintext highlighter-rouge">sdr</code> / <code class="language-plaintext highlighter-rouge">urh</code> / <code class="language-plaintext highlighter-rouge">ble</code></p>

<p>これらを日本語の文中で連続表記した場合（「IDAで開いて」「K8sの権限」「BLEの通信を」など）、そのトークンは命中しない。ただし各ルートは複数のパターンをOR結合しているので、同じ規則内の別トークン（中国語キーワードや <code class="language-plaintext highlighter-rouge">\b</code> の無い英単語）が命中すれば救われる。したがって「18ルートが必ず壊れる」ではなく、<strong>該当トークンだけを頼りにした日本語ヒントが落ちる</strong>というのが正確な影響範囲だ。</p>

<h3 id="回避策">回避策</h3>

<p>実務上の対処は3つある。上から順に手軽い。</p>

<p>・<strong>回避策1（即効・推奨）</strong>：ヒント文字列の英数字トークンの前後に半角スペースを入れる。「この APK を解析して」「IDA で開く」。1文字の追加で済み、リポジトリを変更しない<br />
・<strong>回避策2</strong>：ヒントを英語で書く。<code class="language-plaintext highlighter-rouge">analyze this apk and find the encryption</code> は confidence: high でR1に届くことを確認済み<br />
・<strong>回避策3（恒久）</strong>：<code class="language-plaintext highlighter-rouge">skills/config/routing.json</code> に日本語キーワードを追加する。単一のソースファイルなので変更箇所は1つで済む。追加後は必ず <code class="language-plaintext highlighter-rouge">bash skills/scripts/test-routing.sh</code> を実行して166ケースが緑のままか確認する</p>

<p>自分の環境でどのヒントが落ちるかは、次のコマンドで一括確認できる。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 自分がよく使う日本語ヒントを列挙して、空振り（low）するものを洗い出す</span>
<span class="k">for </span>h <span class="k">in</span> <span class="s2">"このAPKを解析して"</span> <span class="s2">"IDAで開いて関数を追う"</span> <span class="s2">"K8sの権限設定を確認"</span> <span class="s2">"BLEの通信を解析"</span><span class="p">;</span> <span class="k">do
  </span><span class="nb">printf</span> <span class="s1">'%-24s =&gt; '</span> <span class="s2">"</span><span class="nv">$h</span><span class="s2">"</span>
  bash skills/scripts/master-route.sh <span class="nt">--hint</span> <span class="s2">"</span><span class="nv">$h</span><span class="s2">"</span> 2&gt;/dev/null | <span class="nb">grep</span> <span class="s1">'^Label:'</span>
<span class="k">done</span>
<span class="c"># confidence: low が出たヒントは、英数字の前後に半角スペースを入れて再実行する</span>
</code></pre></div></div>

<h3 id="166ケースの回帰テストがこの空振りを検出しない理由">166ケースの回帰テストがこの空振りを検出しない理由</h3>

<p>「これだけ明確な挙動なら、CIが落ちるのでは」と思うところだが、落ちない。理由はベンチマークの中身にある。<code class="language-plaintext highlighter-rouge">skills/tests/routing-benchmark.json</code> の166ケースを言語別に分類すると次のようになった。</p>

<table>
  <thead>
    <tr>
      <th>分類</th>
      <th>件数</th>
      <th>判定方法</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>中国語（漢字を含みかなを含まない）</td>
      <td>97</td>
      <td>正規表現でかな・漢字の有無を判定</td>
    </tr>
    <tr>
      <td>英語（ASCIIのみ）</td>
      <td>69</td>
      <td>同上</td>
    </tr>
    <tr>
      <td><strong>日本語（かなを含む）</strong></td>
      <td><strong>0</strong></td>
      <td>同上</td>
    </tr>
    <tr>
      <td>カバーされるルート</td>
      <td>41 / 41</td>
      <td><code class="language-plaintext highlighter-rouge">expect</code> フィールドの一意集合</td>
    </tr>
  </tbody>
</table>

<p>CHANGELOG も v1.0.1 の記述で “163 bilingual cases”（バイリンガル）と明言している。つまり<strong>設計として英語と中国語の2言語</strong>であり、日本語は最初から対象外だ。ルートのカバレッジは41/41で完璧なのに、言語カバレッジには穴がある。</p>

<p>さらに踏み込むと、中国語ケースですら「連続表記」をほとんど検証していない。166ケースのうちASCII文字が漢字と直接隣接している（間に空白が無い）ものは<strong>わずか3件</strong>で、内訳は <code class="language-plaintext highlighter-rouge">前端签名 加密参数 js逆向</code>（R3）、<code class="language-plaintext highlighter-rouge">渗透测试 端口扫描 sql注入</code>（R11）、<code class="language-plaintext highlighter-rouge">无线渗透 wifi攻击</code>（R29）。しかもこの3件はいずれも中国語キーワード側（<code class="language-plaintext highlighter-rouge">前端.?签名</code> <code class="language-plaintext highlighter-rouge">渗透测试</code> <code class="language-plaintext highlighter-rouge">无线</code>）で命中するため、単語境界の問題が露出しない。ベンチマークの著者は自然と <code class="language-plaintext highlighter-rouge">安卓 APK 加固 反编译</code> のようにトークンを空白で区切って書いており、その書き癖が問題を覆い隠している。</p>

<figure>
  <img src="/generated_images/sections/reverse-skill-h2-3.webp" alt="回帰テスト166ケースの言語内訳。中国語97件、英語69件、日本語0件" loading="lazy" width="1280" height="430" style="width:100%;height:auto;" />
  <figcaption>166ケース全てがmacOSでPASSするが、日本語ケースは1件も含まれない（`routing-benchmark.json` を集計）</figcaption>
</figure>

<div class="box-warning">
  <p><strong>ここで押さえるべき一般論</strong></p>

  <p>「CIが緑」が意味するのは「テストが検証している範囲で壊れていない」ことだけで、検証していない入力については何も言っていない。多言語で使われるルールベースの振り分けを導入するときは、<strong>回帰テストの入力言語の分布を必ず確認する</strong>。件数（166）とカバレッジ率（41/41）は立派に見えても、軸が1本足りなければその軸は無防備なままになる。</p>
</div>

<h3 id="プラットフォームによって本番の経路が違う">プラットフォームによって「本番の経路」が違う</h3>

<p>もうひとつ重要なのは、この決定論ルーターが常に使われるわけではない点だ。<code class="language-plaintext highlighter-rouge">README_AI.md</code> の手順6は、OSによって指示を分けている。</p>

<p>・<strong>Windows</strong>：<code class="language-plaintext highlighter-rouge">powershell -File skills/scripts/master-route.ps1 -Hint "&lt;task&gt;"</code> を実行する。<strong>スクリプトが本番の経路</strong><br />
・<strong>Linux / macOS / Kali</strong>：<code class="language-plaintext highlighter-rouge">skills/MASTER-ROUTING.md</code> を開く（<code class="language-plaintext highlighter-rouge">pwsh</code> があれば同じスクリプトでも可）。つまり<strong>AIがマークダウンの梯子を読んで自分で判断する</strong></p>

<p>したがって単語境界の問題を正面から踏むのはWindows利用者と、<code class="language-plaintext highlighter-rouge">master-route.sh</code> / <code class="language-plaintext highlighter-rouge">.ps1</code> を明示的に実行した利用者だ。macOS・Linuxで既定の手順に従う場合はLLMが日本語を解釈するので、日本語ヒントでも妥当なスキルに辿り着く可能性が高い。ただしその場合は<strong>166ケースの回帰保証が一切かからない</strong>——テストが検証しているのはスクリプト側だからだ。決定論の安心を取るか、言語の柔軟さを取るかが、OSによって自動的に決まってしまう構造になっている。</p>

<p>なお <code class="language-plaintext highlighter-rouge">MASTER-ROUTING.md</code>（5,687バイト）は全文が中国語で、こちらもかなの出現は0文字だった。LLMが読む前提なので実害は小さいが、日本語での運用を考えるなら翻訳版を用意する価値はある。</p>

<h2 id="reverse-skillのact前権限ゲートは何を保証し何を保証しないか">reverse-skillのACT前権限ゲートは何を保証し、何を保証しないか</h2>

<p>逆解析・ペネトレーションテスト系のスキルパックで最も重要なのは、機能ではなく<strong>実行を止める側</strong>の実装だ。reverse-skillは <code class="language-plaintext highlighter-rouge">RULES.md</code> と <code class="language-plaintext highlighter-rouge">skills/ops/scope-contract.md</code> で「auth が granted になるまで対象へACTしない」と定めており、それを機械的に検査する <code class="language-plaintext highlighter-rouge">case-guard.sh</code> が付属している。実際に動かして、条件を1つずつ崩してみた。</p>

<p>まずケースを初期化する。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ケースを作る（scope.md / timeline / workitems が生成される）</span>
bash skills/scripts/case-init.sh <span class="nt">--hint</span> <span class="s2">"apk reverse"</span> <span class="nt">--case-name</span> demo
<span class="c"># auth.status=pending network_profile=offline ready_for_act=false</span>

<span class="c"># ACT前ゲートを実行（未就緒なら exit 2）</span>
bash skills/scripts/case-guard.sh <span class="nt">--case-root</span> work/demo<span class="p">;</span> <span class="nb">echo</span> <span class="s2">"EXIT=</span><span class="nv">$?</span><span class="s2">"</span>
</code></pre></div></div>

<p>初期状態は <code class="language-plaintext highlighter-rouge">auth.status=pending</code> / <code class="language-plaintext highlighter-rouge">ready_for_act=false</code> で、ゲートは exit 2 を返した。<strong>既定値が「拒否」側に倒れている</strong>のは正しい設計だ。ここから条件を1つずつ満たしていく。</p>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>scope.md の状態</th>
      <th>結果</th>
      <th>残った指摘</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>生成直後（既定値）</td>
      <td>exit 2</td>
      <td><code class="language-plaintext highlighter-rouge">auth.status is not granted</code> / <code class="language-plaintext highlighter-rouge">ready_for_act is not true</code></td>
    </tr>
    <tr>
      <td>A</td>
      <td><code class="language-plaintext highlighter-rouge">auth.status: granted</code> のみ</td>
      <td>exit 2</td>
      <td><code class="language-plaintext highlighter-rouge">ready_for_act is not true</code></td>
    </tr>
    <tr>
      <td>B</td>
      <td>A＋<code class="language-plaintext highlighter-rouge">ready_for_act: true</code></td>
      <td><strong>exit 0</strong></td>
      <td>（なし）</td>
    </tr>
    <tr>
      <td>C</td>
      <td><code class="language-plaintext highlighter-rouge">## ops_refs</code> 配下に <code class="language-plaintext highlighter-rouge">status: granted</code> を追記＋<code class="language-plaintext highlighter-rouge">ready_for_act: true</code></td>
      <td>exit 2</td>
      <td><code class="language-plaintext highlighter-rouge">auth.status is not granted</code></td>
    </tr>
    <tr>
      <td>D</td>
      <td>全条件達成＋テンプレの定型文を削除</td>
      <td>exit 2</td>
      <td><code class="language-plaintext highlighter-rouge">network_profile.mode is offline without offline sample cue</code></td>
    </tr>
    <tr>
      <td>F</td>
      <td>既定値のまま <code class="language-plaintext highlighter-rouge">--force</code></td>
      <td><strong>exit 0</strong></td>
      <td>2件の指摘が「警告」に降格</td>
    </tr>
  </tbody>
</table>

<figure>
  <img src="/generated_images/sections/reverse-skill-h2-4.webp" alt="case-guardの4条件ラダー。auth.statusはauth節の中だけを読み、節外の偽装は無効。--forceで全条件が警告に降格する" loading="lazy" width="1280" height="430" style="width:100%;height:auto;" />
  <figcaption>ACT前ゲートの4条件（`case-guard.sh` を条件別に実行して確認）</figcaption>
</figure>

<h3 id="良かった点節をまたいだ偽装は弾かれたケースc">良かった点：節をまたいだ偽装は弾かれた（ケースC）</h3>

<p>ケースCは意図的な異常系だ。<code class="language-plaintext highlighter-rouge">scope.md</code> の末尾——つまり <code class="language-plaintext highlighter-rouge">## auth</code> ではない場所——に <code class="language-plaintext highlighter-rouge">- status: granted</code> という行を追記して、ゲートを騙せるか試した。結果は <strong>exit 2 のまま</strong>で、<code class="language-plaintext highlighter-rouge">auth.status is not granted</code> が正しく残った。</p>

<p>スクリプトを読むと、<code class="language-plaintext highlighter-rouge">section_field</code> 関数がawkで <code class="language-plaintext highlighter-rouge">## &lt;節名&gt;</code> の見出しを追跡し、目的の節がアクティブな間だけフィールドを拾う実装になっている。ソース中のコメントにも「notes・evidence・ops_refs にあるステータス風の行が認可ゲートを満たしてはならない」と明記されていた。<strong>scope.mdに外部由来のテキスト（対象から採取したログや第三者のメモ）を貼り付けても、それが認可を偽装できない</strong>ということで、これは実務上かなり効く性質だ。対照群として、同じ値を正しい <code class="language-plaintext highlighter-rouge">## auth</code> 節に書いたケースA・Bはきちんと通ることも確認している。</p>

<h3 id="弱かった点offline時のサンプル手掛かり検査は自己成就するケースd">弱かった点：offline時の「サンプル手掛かり」検査は自己成就する（ケースD）</h3>

<p><code class="language-plaintext highlighter-rouge">network_profile.mode</code> が <code class="language-plaintext highlighter-rouge">offline</code> のとき、ゲートは「オフラインなのに検体の手掛かりが無い」場合に指摘を出す設計になっている。ところがこの検査は <code class="language-plaintext highlighter-rouge">scope.md</code> 全体に対する <code class="language-plaintext highlighter-rouge">grep</code> で、<code class="language-plaintext highlighter-rouge">sample</code> / <code class="language-plaintext highlighter-rouge">offline.?path</code> / <code class="language-plaintext highlighter-rouge">\.apk</code> などにマッチすれば通ってしまう。</p>

<p>そして <code class="language-plaintext highlighter-rouge">case-init</code> が生成するテンプレートには、署名欄のチェックリストとして</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- [x] in_scope.assets non-empty OR offline sample path set
</code></pre></div></div>

<p>という定型文が常に含まれている。この行に <code class="language-plaintext highlighter-rouge">sample</code> が入っているため、<strong>実際には検体を1つも指定していなくても検査は通る</strong>。ケースDでこの行の文言だけを書き換えたところ、初めて <code class="language-plaintext highlighter-rouge">network_profile.mode is offline without offline sample cue</code> が発火した。つまり生成直後の <code class="language-plaintext highlighter-rouge">scope.md</code> に対して、この検査は原理的に失敗しない。</p>

<p>ただし影響は限定的だ。<code class="language-plaintext highlighter-rouge">offline</code> は最もリスクの低いモード（ネットワーク越しに対象へ触らない）であり、この検査が守ろうとしているのは「オフラインと宣言したのに実は何も用意していない」という運用のずさんさであって、無認可アクセスそのものではない。重大な欠陥ではなく、<strong>検査を数えるときに1つ割り引いておくべき項目</strong>という位置づけになる。</p>

<h3 id="--force-の位置づけ"><code class="language-plaintext highlighter-rouge">--force</code> の位置づけ</h3>

<p>ケースFの通り、<code class="language-plaintext highlighter-rouge">--force</code> を付けると全ての指摘が警告に降格して exit 0 を返す。これは隠れたバイパスではなく、ヘルプにもエラーメッセージにも明示された運用者向けの上書きだ。欠陥として数えるべきではない。</p>

<p>ただし、このゲートが実際に保証している内容を正確に言語化しておくことには意味がある。<strong>保証されるのは「scope.md の所定の欄が所定の値で埋まっている、または人間が明示的に <code class="language-plaintext highlighter-rouge">--force</code> を打った」ことであって、書面の許諾が実在することではない</strong>。エージェントに自動で <code class="language-plaintext highlighter-rouge">--auth-granted</code> を渡させたり、<code class="language-plaintext highlighter-rouge">--force</code> を常用させたりすれば、ゲートは形だけ残って中身が消える。AIエージェントに自律的な攻撃的操作を任せる際のガバナンス要件を体系的に押さえたいなら、<a href="/security/owasp-apts-autonomous-pentesting-standard/">OWASP APTS｜AIエージェント時代の自律型ペネトレーションテスト基準を読む</a>が対応する国際的な整理を扱っている。</p>

<h2 id="ツールチェーン自動導入のサプライチェーン対策を検証する">ツールチェーン自動導入のサプライチェーン対策を検証する</h2>

<p>reverse-skillのもう一つの中核機能が「按需自举工具链」——必要になった時点でツールを取ってきて入れる仕組みだ。開発マシンに任意のバイナリを落として実行する以上、ここは記事化の価値がいちばん高い箇所になる。</p>

<p><code class="language-plaintext highlighter-rouge">skills/scripts/bootstrap-manifest.json</code> には24の「能力」が定義され、うち22が <code class="language-plaintext highlighter-rouge">canAutoInstall: true</code> になっている。導入方式（<code class="language-plaintext highlighter-rouge">bootstrapKind</code>）は10種類——GitHubリリースのZIP、JARラッパー、pip、npm、winget、go install、git clone、Dockerイメージ、ローカルHTTP MCP、手動——に分かれる。</p>

<h3 id="何をもってピン留め済みとしているか">何をもって「ピン留め済み」としているか</h3>

<p><code class="language-plaintext highlighter-rouge">verify-routing-coherence.ps1</code> の「supply-chain pin gate」は、自動導入対象の能力が <code class="language-plaintext highlighter-rouge">pinnedVersion</code> / <code class="language-plaintext highlighter-rouge">pinnedCommit</code> / <code class="language-plaintext highlighter-rouge">pinPolicy</code> のいずれかを持つことを要求し、GitHubリリース系はさらに <code class="language-plaintext highlighter-rouge">assetSha256</code> / <code class="language-plaintext highlighter-rouge">preferApiDigest</code> を受け入れる。マニフェストを集計すると内訳はこうなった。</p>

<table>
  <thead>
    <tr>
      <th>ピン留めの種類</th>
      <th>件数</th>
      <th>意味</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">assetSha256</code>（事前登録ハッシュ）</td>
      <td>2</td>
      <td>jadx v1.5.6、apktool v3.0.2。<strong>内容が固定される</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">preferApiDigest</code>（API申告ダイジェスト）</td>
      <td>4</td>
      <td>radare2×2、ghidra-mcp、bkcrack。ダウンロード時にGitHub APIが返すダイジェストと照合</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pinnedVersion</code>（バージョン指定）</td>
      <td>7</td>
      <td>frida-tools 14.10.4、pwntools 4.15.0 など</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pinnedCommit</code>（コミット指定）</td>
      <td>4</td>
      <td>SecLists、ProxyCat、ida-pro-mcp など</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pinPolicy: winget-latest</code></td>
      <td>4</td>
      <td>adb、nmap、binwalk、yara。<strong>常に最新を入れる方針の記録</strong></td>
    </tr>
  </tbody>
</table>

<p>つまりゲートが検証しているのは「<strong>取得元について何らかの意思決定が記録されていること</strong>」であって、「取得物の内容が固定されていること」ではない。この2つは別の線だ。実際、<code class="language-plaintext highlighter-rouge">winget-latest</code> は「毎回最新を入れる」という宣言であり、内容は固定されない。それでもゲートは通る。悪い設計とは言い切れない——winget経由のnmapやyaraにハッシュを固定すると、更新のたびにマニフェスト修正が必要になり、運用が破綻する。<strong>方針を明示的に記録させる</strong>というのは現実的な落としどころだ。ただし利用者側は「ピン留め済み＝改ざん検知される」と読み替えないほうがよい。</p>

<figure>
  <img src="/generated_images/sections/reverse-skill-h2-5.webp" alt="自動導入24能力のピン留め内訳。バージョン指定7件、コミット指定4件、方針のみ4件、SHA-256ハッシュ2件" loading="lazy" width="1280" height="430" style="width:100%;height:auto;" />
  <figcaption>ピン留めの中身（`bootstrap-manifest.json` を集計）。事前ハッシュがあるのは2件だけ</figcaption>
</figure>

<h3 id="ハッシュ照合は本物かjadxを実際に落として確かめた">ハッシュ照合は本物か——jadxを実際に落として確かめた</h3>

<p>事前ハッシュが2件しか無いとはいえ、その2件が機能しているかは別問題だ。マニフェストに記載された jadx v1.5.6 のSHA-256と、GitHubリリースの実物を照合した。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># マニフェスト記載のハッシュと、GitHubリリースの実物を突き合わせる</span>
curl <span class="nt">-sL</span> <span class="nt">-o</span> jadx.zip https://github.com/skylot/jadx/releases/download/v1.5.6/jadx-1.5.6.zip
shasum <span class="nt">-a</span> 256 jadx.zip
<span class="c"># actual  : 545ea2be9c242511bc145755cf4bda2485ade42966e096f8b4d3da2a230e8974</span>
<span class="c"># manifest: 545ea2be9c242511bc145755cf4bda2485ade42966e096f8b4d3da2a230e8974  ← 完全一致</span>
</code></pre></div></div>

<p>72,646,741バイトのZIPに対して<strong>完全一致</strong>した。ハッシュは飾りではなく実在の値だ。</p>

<p><code class="language-plaintext highlighter-rouge">bootstrap-reverse.sh</code> の <code class="language-plaintext highlighter-rouge">verify_sha256()</code> 関数の実装も確認した。優先順位は「マニフェストの事前ハッシュ → GitHub APIが返したダイジェスト → どちらも無ければ警告して観測値を記録」で、<strong>不一致の場合はファイルを削除して失敗を返す</strong>（<code class="language-plaintext highlighter-rouge">rm -f "$file"</code> の後に <code class="language-plaintext highlighter-rouge">return 1</code>）。ハッシュ計算コマンドの選択も <code class="language-plaintext highlighter-rouge">sha256sum</code> が無ければ <code class="language-plaintext highlighter-rouge">shasum -a 256</code> にフォールバックする実装になっており、<code class="language-plaintext highlighter-rouge">sha256sum</code> を持たないmacOSでも整合性検査が飛ばされないようになっていた。ここは丁寧に書かれている。</p>

<h3 id="ピンの鮮度">ピンの鮮度</h3>

<p>「ピン留めしたまま放置されて何年も古いバージョンを入れ続ける」は、この種の仕組みでよくある劣化パターンだ。主要な固定値を上流と突き合わせた。</p>

<table>
  <thead>
    <tr>
      <th>能力</th>
      <th>マニフェストのピン</th>
      <th>上流の最新（2026-08-16時点）</th>
      <th>状態</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>frida（実体は <code class="language-plaintext highlighter-rouge">frida-tools</code>）</td>
      <td>14.10.4</td>
      <td>14.10.4（2026-06-25公開）</td>
      <td>✅ 最新</td>
    </tr>
    <tr>
      <td>pwntools</td>
      <td>4.15.0</td>
      <td>4.15.0（2025-10-12公開）</td>
      <td>✅ 最新</td>
    </tr>
    <tr>
      <td>jadx</td>
      <td>v1.5.6 + SHA-256</td>
      <td>—</td>
      <td>✅ ハッシュ一致を確認</td>
    </tr>
  </tbody>
</table>

<p>frida の欄には注意点がある。マニフェストの能力名は <code class="language-plaintext highlighter-rouge">frida</code> だが <code class="language-plaintext highlighter-rouge">pipPackage</code> は <code class="language-plaintext highlighter-rouge">frida-tools==14.10.4</code> で、これは <code class="language-plaintext highlighter-rouge">frida</code> 本体（PyPIでは17.x系）とは別パッケージだ。能力名だけを見て「fridaが3メジャー遅れている」と誤読しやすいので、実際に入るパッケージ名を確認してから判断したい。実測ではいずれのピンも上流の最新と一致しており、少なくとも現時点で<strong>ピンは放置されていない</strong>。</p>

<p>依存の固定と自動更新のバランスをどう取るかという一般論は、<a href="/security/renovate-dependabot-supply-chain-attack-defense/">Renovate・Dependabotのサプライチェーン攻撃対策｜自動更新を止めずに守る設定</a>で扱っている考え方がそのまま応用できる。</p>

<h3 id="導入前に自分で確認するコマンド">導入前に自分で確認するコマンド</h3>

<p>reverse-skillを自分の環境に入れる前に、<strong>何がどこから入ってくるのか</strong>を自分の目で確認しておくべきだ。マニフェストは単一のJSONなので、数分で棚卸しできる。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 自動導入される能力と、その取得元・ピン留め状況を一覧化する</span>
python3 - <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">PY</span><span class="sh">'
import json
m = json.load(open('skills/scripts/bootstrap-manifest.json', encoding='utf-8-sig'))
for c in m['capabilities']:
    if not c.get('canAutoInstall'):
        continue
    pin = c.get('assetSha256') and 'sha256' or c.get('pinnedCommit') and 'commit' </span><span class="se">\</span><span class="sh">
        or c.get('pinnedVersion') or c.get('pinPolicy') or c.get('preferApiDigest') and 'api-digest' or 'NONE'
    src = c.get('repo') or c.get('pipPackage') or c.get('npmPackage') or c.get('goPackage') or c.get('bootstrapKind')
    print(f"{c['name']:18} {str(pin):16} {src}")
</span><span class="no">PY

</span><span class="c"># 2. git clone で入る外部リポジトリ（コミット固定の有無）を確認</span>
<span class="nb">grep</span> <span class="nt">-o</span> <span class="s1">'"[^"]*github.com[^"]*"'</span> skills/scripts/bootstrap-manifest.json | <span class="nb">sort</span> <span class="nt">-u</span>
</code></pre></div></div>

<h2 id="macos--linuxでのreverse-skill実行可否と導入前チェックリスト">macOS / Linuxでのreverse-skill実行可否と導入前チェックリスト</h2>

<p>READMEは「クライアント中立」を強く打ち出しており、実際にルーティング中核・回帰テスト・マニフェスト・ケースワークフローは特定のAIクライアントに依存していなかった。一方で<strong>OSに対する中立性は、そこまで高くない</strong>。<code class="language-plaintext highlighter-rouge">skills/scripts/</code> 配下のスクリプトを拡張子で突き合わせた結果がこれだ。</p>

<table>
  <thead>
    <tr>
      <th>提供形態</th>
      <th>件数</th>
      <th>該当スクリプト</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PowerShell + bash 両方</td>
      <td>6</td>
      <td><code class="language-plaintext highlighter-rouge">master-route</code> / <code class="language-plaintext highlighter-rouge">case-init</code> / <code class="language-plaintext highlighter-rouge">case-guard</code> / <code class="language-plaintext highlighter-rouge">bootstrap-reverse</code> / <code class="language-plaintext highlighter-rouge">refresh-tool-index</code> / <code class="language-plaintext highlighter-rouge">test-routing</code></td>
    </tr>
    <tr>
      <td>PowerShellのみ</td>
      <td>10</td>
      <td><code class="language-plaintext highlighter-rouge">verify-routing-coherence</code> / <code class="language-plaintext highlighter-rouge">smoke</code> / <code class="language-plaintext highlighter-rouge">scan-leaks</code> / <code class="language-plaintext highlighter-rouge">extract-summaries</code> / <code class="language-plaintext highlighter-rouge">append-evidence</code> / <code class="language-plaintext highlighter-rouge">test-p0-friction</code> / <code class="language-plaintext highlighter-rouge">test-bootstrap-supply-chain</code> / <code class="language-plaintext highlighter-rouge">test-workflow-title-safety</code> / <code class="language-plaintext highlighter-rouge">update-star-history</code> / <code class="language-plaintext highlighter-rouge">verify-doc-facts</code></td>
    </tr>
    <tr>
      <td>bashのみ</td>
      <td>1</td>
      <td><code class="language-plaintext highlighter-rouge">test-bootstrap-manifest</code></td>
    </tr>
  </tbody>
</table>

<p><strong>日常運用の中核（ルーティング → ケース作成 → 権限ゲート → ツール導入 → 回帰テスト）は6本すべてがbashで揃っている</strong>ので、macOS・Linuxでも問題なく回る。本記事の検証もすべてbash側で実施した。</p>

<p>一方、<strong>検証・監査の層はPowerShell専用</strong>だ。とくに前節で扱ったサプライチェーンのピン留めゲート（<code class="language-plaintext highlighter-rouge">verify-routing-coherence.ps1</code>）自体がPowerShellでしか実行できない。筆者の環境には <code class="language-plaintext highlighter-rouge">pwsh</code> が無かったため、このゲートは<strong>実行しておらず</strong>、スクリプトの判定ロジックとマニフェストの中身を読み合わせて内訳を再構成した。監査を自動で回したいなら、macOS/Linuxでも <code class="language-plaintext highlighter-rouge">brew install --cask powershell</code> などで <code class="language-plaintext highlighter-rouge">pwsh</code> を用意しておくのが実際的だ。</p>

<p><code class="language-plaintext highlighter-rouge">work/</code> ディレクトリの既定位置にも差がある。<code class="language-plaintext highlighter-rouge">master-route.sh</code> はコード内のコメントで「ルート成果物は呼び出し元のプロジェクトに属する」と明記し、既定でカレントディレクトリ配下に書き出す。一方 <code class="language-plaintext highlighter-rouge">case-init.sh</code> は、別ディレクトリから呼んでもクローンしたパッケージ側（<code class="language-plaintext highlighter-rouge">&lt;reverse-skill&gt;/work/&lt;case&gt;</code>）にケースを作った。証跡が意図しない場所に溜まるので、ケース作成時は <code class="language-plaintext highlighter-rouge">--project-root</code> を明示したほうがよい。</p>

<h3 id="類似のaiエージェント向けセキュリティパックとの比較">類似のAIエージェント向けセキュリティパックとの比較</h3>

<p>同じ「AIエージェントに攻撃的セキュリティ作業をさせる」領域には複数のアプローチがある。役割が異なるので、競合というより補完関係にある。</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>reverse-skill</th>
      <th><a href="/security/pentest-ai-agents/">pentest-ai-agents</a></th>
      <th>OWASP APTS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>形態</td>
      <td>スキル手順書＋ルーター＋導入スクリプト</td>
      <td>Claude Code用サブエージェント集</td>
      <td>標準・ガバナンス要件</td>
    </tr>
    <tr>
      <td>中核</td>
      <td>41ルールの決定論的振り分け</td>
      <td>35の専門システムプロンプト</td>
      <td>173要件・3ティア</td>
    </tr>
    <tr>
      <td>主眼</td>
      <td>逆解析（APK/バイナリ/JS/CTF）</td>
      <td>ペネトレーションテスト全工程</td>
      <td>自律実行の安全性・説明責任</td>
    </tr>
    <tr>
      <td>実行制御</td>
      <td><code class="language-plaintext highlighter-rouge">case-guard</code> のscopeゲート（<code class="language-plaintext highlighter-rouge">--force</code>可）</td>
      <td>Tier 1助言 / Tier 2実行の分離</td>
      <td>Kill switch・スコープ境界の要件定義</td>
    </tr>
    <tr>
      <td>ツール導入</td>
      <td>24能力を自動導入（ハッシュ検証あり）</td>
      <td>前提ツールは利用者が用意</td>
      <td>対象外</td>
    </tr>
    <tr>
      <td>ライセンス</td>
      <td>MIT（CTF部分はGPLv3）</td>
      <td>リポジトリ参照</td>
      <td>OWASP</td>
    </tr>
  </tbody>
</table>

<p>reverse-skillの独自性は<strong>ツールチェーン自動導入まで面倒を見る点</strong>にあり、それは同時に<strong>最大のリスク面</strong>でもある。導入判断はこの一点に集約されると言っていい。</p>

<h3 id="導入前チェックリスト">導入前チェックリスト</h3>

<div class="box-warning">
  <p><strong>reverse-skillを業務環境に入れる前に確認すること</strong></p>

  <p>・<strong>用途の合法性を先に固める</strong>。逆解析・ペネトレーションテストは、対象と権限が明確な場合にのみ実施する。リポジトリのREADMEも「全ての操作は法的境界の内側で」と明記している<br />
・<strong><code class="language-plaintext highlighter-rouge">bootstrap-manifest.json</code> を先に読む</strong>。24能力・22自動導入の取得元とピン留め状況を、前節のコマンドで棚卸ししてから入れる<br />
・<strong><code class="language-plaintext highlighter-rouge">work/</code> の出力先を明示する</strong>。証跡と検体が意図しないディレクトリに溜まらないよう <code class="language-plaintext highlighter-rouge">--project-root</code> を指定する<br />
・<strong><code class="language-plaintext highlighter-rouge">--force</code> の運用を決める</strong>。エージェントに <code class="language-plaintext highlighter-rouge">--force</code> や <code class="language-plaintext highlighter-rouge">--auth-granted</code> を自動で打たせないこと。ゲートは打ち手を止めるためにある<br />
・<strong>日本語ヒントは半角スペースを入れる</strong>。または <code class="language-plaintext highlighter-rouge">routing.json</code> に日本語キーワードを追加し、<code class="language-plaintext highlighter-rouge">test-routing.sh</code> が緑のままか確認する<br />
・<strong><code class="language-plaintext highlighter-rouge">pwsh</code> を用意する</strong>。監査系スクリプトはPowerShell専用なので、macOS/Linuxでも入れておくと <code class="language-plaintext highlighter-rouge">verify-routing-coherence</code> を自分で回せる</p>
</div>

<h3 id="タイムラインリポジトリと検証の時系列">タイムライン（リポジトリと検証の時系列）</h3>

<table>
  <thead>
    <tr>
      <th>日付</th>
      <th>出来事</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2026-05-13</td>
      <td>リポジトリ作成（GitHub API の <code class="language-plaintext highlighter-rouge">created_at</code>）</td>
    </tr>
    <tr>
      <td>2026-08-08</td>
      <td>v1.0.1 リリース。routing.json の単一ソース化、163ケースの回帰ベンチマーク、サプライチェーンのピン留めゲートを追加（CHANGELOG）</td>
    </tr>
    <tr>
      <td>2026-08-15</td>
      <td>最終push（<code class="language-plaintext highlighter-rouge">pushed_at</code>）。ベンチマークは166ケースに増加</td>
    </tr>
    <tr>
      <td>2026-08-16</td>
      <td>本記事の検証実施。Star 25,491 / Fork 3,450</td>
    </tr>
  </tbody>
</table>

<h2 id="まとめ決定論の強みと日本語という死角">まとめ——決定論の強みと、日本語という死角</h2>

<p>reverse-skillは、AIエージェントの逆解析タスクを「毎回その場で考える」から「決まった手順書へ落とす」へ変える試みとして、想像よりずっと真面目に作られていた。ルーティングは単一のJSONに集約され、166ケースの回帰テストとWindows/UbuntuのCIで固定されている。ACT前の権限ゲートは既定で拒否側に倒れ、節をまたいだ偽装も弾いた。ツール導入のSHA-256照合は実物と一致し、不一致ならファイルを削除して失敗する。</p>

<p>その上で、日本語で使う人間にとっての結論はこうだ。<strong>この決定論的なルーターは、日本語の入力を想定していない</strong>。単語境界に依存したASCIIトークンは日本語の連続表記の中で命中せず、そして166ケースの回帰テストには日本語が1件も含まれていない。CIが緑であることは、この件について何も保証していない。</p>

<p>対処は半角スペース1つで済む。むしろ本記事で持ち帰ってほしいのは、その先の一般論のほうだ。ルールベースの振り分けを多言語環境に持ち込むときは、<strong>ルールの本数やテストの件数ではなく、テスト入力の言語分布を見る</strong>。そして「ピン留め済み」と書かれた仕組みを評価するときは、<strong>それが記録しているのが意思決定なのか、内容の固定なのかを分けて数える</strong>。この2つの目を持っていれば、次に流行るスキルパックも同じ手順で自分で測れる。</p>

<h2 id="参照ソース">参照ソース</h2>

<p>・<a href="https://github.com/zhaoxuya520/reverse-skill">zhaoxuya520/reverse-skill — GitHubリポジトリ</a>（README.md / README_AI.md / RULES.md / CHANGELOG.md、2026-08-16取得）<br />
・<a href="https://github.com/zhaoxuya520/reverse-skill/blob/main/CHANGELOG.md">reverse-skill CHANGELOG v1.0.1</a>（2026-08-08。routing.json単一ソース化・回帰ベンチマーク・サプライチェーンピン留めゲートの追加履歴）<br />
・<a href="https://github.com/skylot/jadx/releases/tag/v1.5.6">skylot/jadx リリース v1.5.6</a>（SHA-256照合に使用した実物のアセット）<br />
・<a href="https://docs.python.org/3/library/re.html#regular-expression-syntax">Python 公式ドキュメント — <code class="language-plaintext highlighter-rouge">re</code> の正規表現シンタックス</a>（<code class="language-plaintext highlighter-rouge">\b</code> は <code class="language-plaintext highlighter-rouge">\w</code> と <code class="language-plaintext highlighter-rouge">\W</code> の境界と定義され、str型パターンでは既定でUnicodeの単語文字が使われる＝漢字・かなも単語文字になる根拠）<br />
・<a href="https://pypi.org/project/frida-tools/">frida-tools — PyPI</a> / <a href="https://pypi.org/project/pwntools/">pwntools — PyPI</a>（ピン留めバージョンの鮮度確認）</p>

<!--
Distribution memo (Step 8)
- X: 「★25,000のAI逆解析スキルパック、日本語で指示すると41ルールのルーターが空振りする」GIF添付（実測A/Bのターミナル録画）。08時台JST。セルフリプで「回避策は半角スペース1つ」＋routing.jsonの日本語キーワード追加案
- はてブ: 「CIが緑=守られている、ではない」166ケース中日本語0件の話を主軸に
- 内部リンク提案: 今後CVE/サプライチェーン記事から本記事へ「エージェント用ツール自動導入のピン留め実例」として参照可能
- 未実行: verify-routing-coherence.ps1 / smoke.ps1（pwsh未導入のため）。pwsh導入環境での追試は別途
-->]]></content><author><name></name></author><category term="security" /><category term="security" /><category term="セキュリティ" /><category term="pentesting" /><category term="リバースエンジニアリング" /><category term="サプライチェーン" /><summary type="html"><![CDATA[reverse-skillはAPK・バイナリ・CTFの逆解析タスクをAIエージェントに振り分ける★25,000超のスキルパック。41ルーティングルールを実測した結果、PRIMARY判定はLLM推論ではなく正規表現の採点で、日本語ヒントでは空振りする条件が再現した。権限ゲートとサプライチェーン対策も検証する。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_reverse-skill-ai-reverse-engineering-router.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_reverse-skill-ai-reverse-engineering-router.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">pdf-inspector徹底解説｜PDFがOCR必要かを25msで判定しMarkdown化するRust製OSSを実測</title><link href="https://ai-heartland.com/rag/pdf-inspector-pdf-ocr-routing/" rel="alternate" type="text/html" title="pdf-inspector徹底解説｜PDFがOCR必要かを25msで判定しMarkdown化するRust製OSSを実測" /><published>2026-08-16T09:30:00+09:00</published><updated>2026-08-16T09:30:00+09:00</updated><id>https://ai-heartland.com/rag/pdf-inspector-pdf-ocr-routing</id><content type="html" xml:base="https://ai-heartland.com/rag/pdf-inspector-pdf-ocr-routing/"><![CDATA[<p>PDFをAIに読ませるパイプラインで、コストと待ち時間の大半を食っているのはOCRです。ところが実務で届くPDFの多くは、そもそも文字データが最初から埋め込まれた「テキストベース」のファイルで、OCRにかける必要がありません。<strong>pdf-inspector</strong>（<a href="https://github.com/firecrawl/pdf-inspector">firecrawl/pdf-inspector</a>・スター約1.6万・MIT）は、この「OCRが要るのか要らないのか」を数十ミリ秒で判定し、要らない文書だけをその場でMarkdown化してしまうRust製のライブラリです。RAGの文書取り込み層をどう組むかという全体像は<a href="/explain/rag-complete-guide-2026/">RAGとは？仕組み・構築・ベクトルDB選定までの2026年実装マップ</a>にまとめていますが、本記事はその最前段にあたる「前処理の振り分け」に絞って、v1.14.2を実際に動かした結果を書きます。</p>

<figure>
  <img src="/generated_images/pdf-inspector-playground.gif" alt="pdf-inspectorの公式デモサイトにPDFをドロップすると、ブラウザ内のWebAssemblyだけで文書種別・ページ数・処理時間・Markdownが表示される様子" loading="lazy" width="1040" height="514" style="width:100%;height:auto;border-radius:.5rem;" />
  <figcaption>公式デモサイト（<a href="https://firecrawl.github.io/pdf-inspector/">firecrawl.github.io/pdf-inspector</a>）に15ページの論文PDFをドロップした実際の動作。サーバーに送らずブラウザ内のWebAssemblyだけで <code>TextBased</code> と判定し、334msでMarkdownまで出している</figcaption>
</figure>

<div class="point-box">
  <p><strong>30秒でわかる pdf-inspector</strong></p>

  <p>・<strong>やること</strong>：PDFを <code class="language-plaintext highlighter-rouge">text_based</code> / <code class="language-plaintext highlighter-rouge">scanned</code> / <code class="language-plaintext highlighter-rouge">image_based</code> / <code class="language-plaintext highlighter-rouge">mixed</code> の4種に分類し、テキストベースならOCRなしでMarkdown化する<br />
・<strong>やらないこと</strong>：OCRそのもの。画像から文字を読む機能は無く、「このページはOCRが必要」と返すまでが担当<br />
・<strong>実測速度</strong>：15ページのPDFで判定24.6ms、Markdown化まで96.2ms（Apple Silicon・v1.14.2）<br />
・<strong>依存の軽さ</strong>：MLモデルなし・外部サービスなし。PDFパースの依存は <code class="language-plaintext highlighter-rouge">lopdf</code> 1つだけ<br />
・<strong>注意点</strong>：既定の判定は全ページを見ない。後述の条件で「OCRが必要なページ」を取りこぼす</p>
</div>

<div class="box-tip">
  <p><strong>この記事の位置づけ</strong>：READMEの翻訳ではなく、npm・PyPIから配布物を実際に入れ、自作の検証用PDFを食わせた実測を軸にしています。READMEの主張が再現できた点も、READMEと実装がずれている点も、両方そのまま書きます。</p>
</div>

<h2 id="pdf-inspectorとはocrに投げる前に投げるかを決めるoss">pdf-inspectorとは——OCRに投げる前に「投げるか」を決めるOSS</h2>

<p>pdf-inspectorは、Web上のデータをLLM向けに整形するサービスを提供するFirecrawlが2026年2月に公開したOSSです。2026年8月16日時点でGitHubスターは約1.6万、ライセンスはMIT、最新版はv1.14.2（2026年8月13日リリース）。crates.ioでの累計ダウンロードは99,590件を数えます。</p>

<p>このライブラリの発想は、READMEの一文に集約されています。Firecrawlは自社のパイプラインで扱うPDFのうち<strong>約54%はOCRを必要としない</strong>と述べており、その分をローカルで200ms以内に処理してOCRサービスをスキップするために作った、と説明しています。つまりpdf-inspectorは「高精度なPDF解析エンジン」ではなく、<strong>高精度なエンジンに何を渡すかを決める門番</strong>です。</p>

<figure>
  <img src="/generated_images/sections/pdfi-routing.webp" alt="PDFが届いてから10〜25msで分類し、テキストベースは自前抽出、それ以外だけをOCRへ回すルーティングの流れ" loading="lazy" width="1280" height="320" style="width:100%;height:auto;" />
  <figcaption>pdf-inspectorが担当する範囲。判定と抽出でドキュメントを1回しか読まない設計になっている</figcaption>
</figure>

<h3 id="4つの分類とそれぞれの意味">4つの分類と、それぞれの意味</h3>

<p>判定結果は4種類の文字列で返ります。実務ではこの4値がそのまま分岐条件になります。</p>

<table>
  <thead>
    <tr>
      <th>判定値</th>
      <th>意味</th>
      <th>典型例</th>
      <th>推奨アクション</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">text_based</code></td>
      <td>全ページにテキスト層がある</td>
      <td>論文・仕様書・請求書のPDF出力</td>
      <td>そのままローカル抽出（OCR費用ゼロ）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scanned</code></td>
      <td>テキスト層が無く画像だけ</td>
      <td>紙をスキャンした契約書</td>
      <td>全ページOCRへ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">image_based</code></td>
      <td>画像主体でテキストがごく僅か</td>
      <td>図面・スライドの画像化PDF</td>
      <td>全ページOCRへ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mixed</code></td>
      <td>テキストページと画像ページが混在</td>
      <td>本文＋スキャン添付の年次報告書</td>
      <td>該当ページだけOCRへ</td>
    </tr>
  </tbody>
</table>

<p>実務的にいちばん価値があるのは <code class="language-plaintext highlighter-rouge">mixed</code> です。300ページの報告書のうち画像ページが11枚だけ、という文書に対して全ページOCRを投げるのは無駄が大きい。pdf-inspectorは <code class="language-plaintext highlighter-rouge">pages_needing_ocr</code> として1始まりのページ番号リストを返すので、<strong>ページ単位で振り分けられます</strong>。</p>

<h3 id="mlモデルを持たないという設計判断">MLモデルを持たないという設計判断</h3>

<p>pdf-inspectorはニューラルネットワークを一切使いません。PDFの中身（コンテンツストリーム）を直接読み、テキスト描画命令である <code class="language-plaintext highlighter-rouge">Tj</code> / <code class="language-plaintext highlighter-rouge">TJ</code> と画像描画命令 <code class="language-plaintext highlighter-rouge">Do</code> の出現を数えて分類します。この割り切りが速度と導入コストに効いています。モデルの重みをダウンロードする必要がなく、GPUも不要で、Rustの単一バイナリとして動きます。</p>

<p>同じ「文書をAIに食わせる形に整える」領域でも、<a href="/rag/officeparser-nodejs-document-parser-guide/">officeParser入門：Node.jsでdocx・xlsx・pptxをAST解析してRAGに繋ぐ</a>で扱ったようなOffice文書のパーサや、<a href="/rag/google-langextract-structured-extraction/">Google LangExtract完全ガイド：LLMで非構造テキストから構造化抽出、ソース位置も追跡</a>のようなLLMベースの抽出器とは、担当するレイヤーが異なります。pdf-inspectorはもっと手前の、バイト列を見て振り分けるところにいます。</p>

<h2 id="インストールと最小の使い方nodepythoncliブラウザ">インストールと最小の使い方——Node・Python・CLI・ブラウザ</h2>

<p>配布は4系統あり、すべてv1.14.2で揃っています（crates.io / npm / PyPI / npm-wasm、いずれも2026年8月13日更新）。</p>

<p>READMEのPythonクイックスタートは <code class="language-plaintext highlighter-rouge">pip install maturin</code> してソースからビルドする手順になっていますが、<strong>実際にはPyPIにビルド済みwheelが公開されている</strong>ため、利用するだけならRustツールチェインは不要です。macOS（x86_64 / arm64）・manylinux（x86_64 / aarch64）・Windows向けのabi3 wheelが揃っており、次の1行で入ります。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>pdf-inspector
</code></pre></div></div>

<p>Node.jsも同様に、napi-rsのプリビルドバイナリが降ってきます。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @firecrawl/pdf-inspector
</code></pre></div></div>

<p>最小の判定コードはこれだけです。ファイルパスを渡すと分類結果とMarkdownが一度に返ります。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">pdf_inspector</span>

<span class="n">r</span> <span class="o">=</span> <span class="n">pdf_inspector</span><span class="p">.</span><span class="nf">detect_pdf</span><span class="p">(</span><span class="sh">"</span><span class="s">document.pdf</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">pdf_type</span><span class="p">)</span>           <span class="c1"># text_based / scanned / image_based / mixed
</span><span class="nf">print</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">confidence</span><span class="p">)</span>         <span class="c1"># 0.0-1.0
</span><span class="nf">print</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">pages_needing_ocr</span><span class="p">)</span>  <span class="c1"># OCRが必要な1始まりページ番号
</span></code></pre></div></div>

<p>CLIも同梱されており、<code class="language-plaintext highlighter-rouge">cargo install pdf-inspector</code> で <code class="language-plaintext highlighter-rouge">pdf2md</code> と <code class="language-plaintext highlighter-rouge">detect-pdf</code> の2コマンドが入ります。<code class="language-plaintext highlighter-rouge">pdf2md document.pdf --json</code> でMarkdownとメタ情報をまとめてJSONに吐けるため、シェルスクリプトからの利用も想定されています。</p>

<h3 id="バインディングごとに使える関数の数が違う">バインディングごとに使える関数の数が違う</h3>

<p>ここは導入前に確認しておく価値がありました。npmとPyPIから実際に入れて公開シンボルを数えたところ、<strong>同じRustコアなのにバインディングによって公開されているAPIの数が3倍以上違います</strong>。</p>

<figure>
  <img src="/generated_images/sections/pdfi-bindings.webp" alt="Node.js 18関数・Python 16関数・ブラウザWASM 5関数という公開API数の比較" loading="lazy" width="1280" height="430" style="width:100%;height:auto;" />
  <figcaption>v1.14.2を実際に導入して公開シンボルを列挙した結果</figcaption>
</figure>

<table>
  <thead>
    <tr>
      <th>機能</th>
      <th>Node.js</th>
      <th>Python</th>
      <th>ブラウザWASM</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>分類・判定（<code class="language-plaintext highlighter-rouge">classifyPdf</code> / <code class="language-plaintext highlighter-rouge">detectPdf</code>）</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
    </tr>
    <tr>
      <td>Markdown変換（<code class="language-plaintext highlighter-rouge">processPdf</code>）</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
    </tr>
    <tr>
      <td>位置情報つきテキスト抽出</td>
      <td>○</td>
      <td>○</td>
      <td>✕</td>
    </tr>
    <tr>
      <td>ページ単位のMarkdown抽出</td>
      <td>○</td>
      <td>○</td>
      <td>✕</td>
    </tr>
    <tr>
      <td>構造要素の抽出</td>
      <td>○</td>
      <td>○</td>
      <td>✕</td>
    </tr>
    <tr>
      <td><strong>表の構造抽出</strong>（<code class="language-plaintext highlighter-rouge">extractTablesWithStructure</code> 系）</td>
      <td>○</td>
      <td>✕</td>
      <td>✕</td>
    </tr>
    <tr>
      <td>ベクター罫線の検出（<code class="language-plaintext highlighter-rouge">detectVectorGridInRegion</code>）</td>
      <td>○</td>
      <td>✕</td>
      <td>✕</td>
    </tr>
    <tr>
      <td>非同期版（<code class="language-plaintext highlighter-rouge">*Async</code>）</td>
      <td>○</td>
      <td>✕</td>
      <td>✕</td>
    </tr>
    <tr>
      <td>公開関数の総数</td>
      <td><strong>18</strong></td>
      <td><strong>16</strong></td>
      <td><strong>5</strong></td>
    </tr>
  </tbody>
</table>

<p>表をセル単位で構造化して取り出したい場合、現時点ではNode.jsを選ぶしかありません。Python版にも <code class="language-plaintext highlighter-rouge">extract_text_in_regions</code>（座標範囲を指定してテキストを取る）はあるので、表の位置が既知なら回避はできますが、罫線から表を自動検出してセルに割るAPIはNodeのみです。</p>

<p>もう一点、READMEには <code class="language-plaintext highlighter-rouge">EarlyExit</code> / <code class="language-plaintext highlighter-rouge">Full</code> / <code class="language-plaintext highlighter-rouge">Sample(n)</code> / <code class="language-plaintext highlighter-rouge">Pages(vec)</code> という4つの走査戦略（ScanStrategy）の表が載っていますが、<strong>この戦略を選べるのはRustのAPIだけ</strong>でした。<code class="language-plaintext highlighter-rouge">src/python.rs</code>・<code class="language-plaintext highlighter-rouge">napi/src/lib.rs</code>・<code class="language-plaintext highlighter-rouge">wasm/src/lib.rs</code> のいずれも <code class="language-plaintext highlighter-rouge">PdfOptions::new()</code> を呼ぶだけで走査戦略には触れておらず、3つのバインディングはすべて既定値のまま動きます。</p>

<h2 id="実測判定抽出の速度とmarkdownの中身">実測——判定・抽出の速度とMarkdownの中身</h2>

<p>READMEの速度主張が手元で再現するかを確認しました。検証環境はApple Silicon（M系）、pdf-inspector v1.14.2のPyPI wheel、計測は同一処理を5回実行した中央値です。</p>

<figure>
  <img src="/generated_images/sections/pdfi-measured.webp" alt="判定24.6ms、Markdown化96.2ms、出力40,573字、ブラウザWASMでは334msという実測値" loading="lazy" width="1280" height="340" style="width:100%;height:auto;" />
  <figcaption>READMEの「判定10〜50ms」「テキストPDFを200ms未満」はいずれも再現できた</figcaption>
</figure>

<p>題材には「Attention Is All You Need」の公開PDF（15ページ・2.11MB）を使いました。</p>

<table>
  <thead>
    <tr>
      <th>対象PDF</th>
      <th>ページ数</th>
      <th>判定</th>
      <th>Markdown化まで</th>
      <th>出力文字数</th>
      <th>判定結果</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>論文（テキストベース）</td>
      <td>15</td>
      <td>24.6ms</td>
      <td>96.2ms</td>
      <td>40,573字</td>
      <td><code class="language-plaintext highlighter-rouge">text_based</code>（確信度1.00）</td>
    </tr>
    <tr>
      <td>混在（自作・11ページが画像）</td>
      <td>30</td>
      <td>10.8ms</td>
      <td>30.7ms</td>
      <td>45,072字</td>
      <td><code class="language-plaintext highlighter-rouge">mixed</code>（確信度0.73）</td>
    </tr>
    <tr>
      <td>スキャン（自作・全ページ画像）</td>
      <td>8</td>
      <td>3.5ms</td>
      <td>3.4ms</td>
      <td>0字</td>
      <td><code class="language-plaintext highlighter-rouge">scanned</code>（確信度0.95）</td>
    </tr>
  </tbody>
</table>

<p>READMEが掲げる「判定10〜50ms」「テキストベースPDFをローカルで200ms未満」は、いずれも再現できました。スキャンPDFで抽出が3.4msで終わっているのは、テキスト層が無いと判定した時点で抽出処理を打ち切るためです。無駄な処理をしないという意味で正しい挙動です。</p>

<p>出力されたMarkdownも実用に足る品質でした。見出しはフォントサイズ比から <code class="language-plaintext highlighter-rouge">##</code> / <code class="language-plaintext highlighter-rouge">####</code> に振り分けられ、著者名の脚注記号やメールアドレスも落ちていません。2段組の本文が読み順どおりに直列化されている点は、素朴なテキスト抽出との明確な差です。</p>

<p>ブラウザWASM版では同じ論文PDFが334msで処理されました。ネイティブの96.2msに対して約3.5倍かかりますが、<strong>PDFを一切サーバーに送らずに完結する</strong>点と引き換えと考えれば妥当な数字です。機密文書の一次判定をクライアント側で済ませたい構成では選択肢になります。</p>

<figure>
  <img src="/generated_images/pdf-inspector-playground-result.webp" alt="公式デモの結果パネル。DOCUMENT TYPE が TextBased、PAGES 15、PROCESSING 334 ms と表示され、右側に変換されたMarkdownが並んでいる" loading="lazy" width="1280" height="633" style="width:100%;height:auto;" />
  <figcaption>公式デモの結果表示。「Parsed textbased.pdf locally.」の通り、処理はブラウザ内で完結している</figcaption>
</figure>

<h2 id="実測でわかった落とし穴ocr不要の答えがapiで食い違う">実測でわかった落とし穴——「OCR不要」の答えがAPIで食い違う</h2>

<p>ここからが本題です。READMEに書かれていない挙動を2つ見つけました。どちらもこのライブラリの主用途である「OCRの振り分け」に直接影響します。</p>

<h3 id="既定の走査は全ページを見ていない">既定の走査は全ページを見ていない</h3>

<p>READMEの「How classification works」には、走査戦略の既定は <code class="language-plaintext highlighter-rouge">EarlyExit</code>（全ページを走査し、テキストの無いページで打ち切る）と書かれています。ところが実装を読むと、既定値は違いました。</p>

<p><code class="language-plaintext highlighter-rouge">src/detector.rs</code> の <code class="language-plaintext highlighter-rouge">DetectionConfig::default()</code> は <code class="language-plaintext highlighter-rouge">ScanStrategy::Sample(8)</code> を返します。コード中のコメントには「EarlyExitは、画像だけの表紙のあとにテキストページが続くPDF（年次報告書など）に対して過剰に反応する」と、変更した理由まで書かれています。つまり実装側の判断は妥当なのですが、<strong>READMEの記述が追随していません</strong>。</p>

<p><code class="language-plaintext highlighter-rouge">Sample(8)</code> は、先頭・末尾・その間を等間隔に割った最大8ページだけを見ます。30ページの文書なら1・5・9・13・17・21・25・30ページです。残る22ページは分類の判断材料に入りません。</p>

<h3 id="text_based-と判定された瞬間ocr対象リストは空になる"><code class="language-plaintext highlighter-rouge">text_based</code> と判定された瞬間、OCR対象リストは空になる</h3>

<p>問題はここから先です。ページ単位のOCRリスト（<code class="language-plaintext highlighter-rouge">pages_needing_ocr</code>）を組み立てる処理は、分類結果によって挙動が変わります。</p>

<p>・<code class="language-plaintext highlighter-rouge">mixed</code> と判定された場合のみ、全ページを1枚ずつ解析してリストを作る<br />
・<code class="language-plaintext highlighter-rouge">scanned</code> / <code class="language-plaintext highlighter-rouge">image_based</code> なら、中身を見ずに「全ページ」を返す<br />
・<code class="language-plaintext highlighter-rouge">text_based</code> なら、<strong>中身を見ずに空リストを返す</strong></p>

<p>したがって、抜き取った8ページがすべてテキストページだった場合、文書は <code class="language-plaintext highlighter-rouge">text_based</code> と分類され、その時点でOCR対象リストは空で確定します。サンプリングの隙間に画像ページが潜んでいても、報告されません。</p>

<p>これを確認するため、30ページのPDFを作り、<strong>11ページ目と12ページ目だけを画像化</strong>しました。この2ページは <code class="language-plaintext highlighter-rouge">Sample(8)</code> が見る8ページのどれにも当たりません。結果は次のとおりです。</p>

<figure>
  <img src="/generated_images/sections/pdfi-gap.webp" alt="同じPDFに対し、detect_pdfはtext_based・確信度1.0・OCR対象なしと返す一方、extract_pages_markdownは11・12ページのOCR必要を検出している比較" loading="lazy" width="1280" height="430" style="width:100%;height:auto;" />
  <figcaption>同じファイル・同じライブラリなのに、呼ぶ関数によって「OCRが必要か」の答えが変わる</figcaption>
</figure>

<p><code class="language-plaintext highlighter-rouge">detect_pdf()</code> は <code class="language-plaintext highlighter-rouge">text_based</code>、確信度 <strong>1.0</strong>（最大値）、<code class="language-plaintext highlighter-rouge">pages_needing_ocr</code> は <strong>空リスト</strong>を返しました。確信度が最大のまま、2ページ分の内容が静かに欠落します。実際、Markdown出力は38,322字で、11・12ページ目の中身はどこにも含まれていません。</p>

<p>一方、<strong>同じライブラリの</strong> <code class="language-plaintext highlighter-rouge">extract_pages_markdown()</code> は同じファイルに対して <code class="language-plaintext highlighter-rouge">pages_needing_ocr = [11, 12]</code> を返し、該当ページの <code class="language-plaintext highlighter-rouge">needs_ocr</code> が <code class="language-plaintext highlighter-rouge">True</code> になりました。こちらは全ページを走査するためです。</p>

<div class="box-warning">
  <p><strong>振り分けの正確さが要るなら <code class="language-plaintext highlighter-rouge">detect_pdf()</code> だけで判断しない</strong></p>

  <p>・<strong>速さ優先の一次判定</strong>なら <code class="language-plaintext highlighter-rouge">detect_pdf()</code> / <code class="language-plaintext highlighter-rouge">classify_pdf()</code> で十分（数十ms）<br />
・<strong>取りこぼしが許されない振り分け</strong>では <code class="language-plaintext highlighter-rouge">extract_pages_markdown()</code> を使い、返り値の <code class="language-plaintext highlighter-rouge">pages_needing_ocr</code> と各ページの <code class="language-plaintext highlighter-rouge">needs_ocr</code> を見る<br />
・Rustから使う場合は <code class="language-plaintext highlighter-rouge">DetectionConfig</code> の <code class="language-plaintext highlighter-rouge">strategy</code> に <code class="language-plaintext highlighter-rouge">ScanStrategy::Full</code> を明示すれば全ページ走査になる（バインディングからは指定できない）<br />
・確信度1.0は「サンプリングした範囲では確実」という意味であって、文書全体の保証ではない</p>
</div>

<p>自分の環境で同じ確認をするなら、次のコードで済みます。判定経路と全ページ走査経路の答えが一致するかを見るだけです。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">pdf_inspector</span> <span class="k">as</span> <span class="n">pi</span>

<span class="n">path</span> <span class="o">=</span> <span class="sh">"</span><span class="s">your.pdf</span><span class="sh">"</span>
<span class="n">fast</span> <span class="o">=</span> <span class="n">pi</span><span class="p">.</span><span class="nf">detect_pdf</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="n">full</span> <span class="o">=</span> <span class="n">pi</span><span class="p">.</span><span class="nf">extract_pages_markdown</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">fast:</span><span class="sh">"</span><span class="p">,</span> <span class="n">fast</span><span class="p">.</span><span class="n">pdf_type</span><span class="p">,</span> <span class="n">fast</span><span class="p">.</span><span class="n">pages_needing_ocr</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">full:</span><span class="sh">"</span><span class="p">,</span> <span class="n">full</span><span class="p">.</span><span class="n">pages_needing_ocr</span><span class="p">)</span>
<span class="k">if</span> <span class="nf">set</span><span class="p">(</span><span class="n">fast</span><span class="p">.</span><span class="n">pages_needing_ocr</span><span class="p">)</span> <span class="o">!=</span> <span class="nf">set</span><span class="p">(</span><span class="n">full</span><span class="p">.</span><span class="n">pages_needing_ocr</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">!! 不一致: 速い経路が取りこぼしています</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="ページ番号が0始まりと1始まりで混在している">ページ番号が0始まりと1始まりで混在している</h3>

<p>もう1つ、振り分けコードで事故になりやすい点があります。同じライブラリの中で、ページ番号の基点が統一されていません。8ページ全部がスキャンのPDFで確認した結果です。</p>

<table>
  <thead>
    <tr>
      <th>呼び出し</th>
      <th>返り値</th>
      <th>基点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">detect_pdf().pages_needing_ocr</code></td>
      <td><code class="language-plaintext highlighter-rouge">[1, 2, 3, 4, 5, 6, 7, 8]</code></td>
      <td>1始まり</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">classify_pdf().pages_needing_ocr</code></td>
      <td><code class="language-plaintext highlighter-rouge">[0, 1, 2, 3, 4, 5, 6, 7]</code></td>
      <td><strong>0始まり</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">extract_pages_markdown().pages</code> の <code class="language-plaintext highlighter-rouge">.page</code></td>
      <td><code class="language-plaintext highlighter-rouge">[0, 1, ...]</code></td>
      <td>0始まり</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">extract_pages_markdown().pages_needing_ocr</code></td>
      <td><code class="language-plaintext highlighter-rouge">[11, 12]</code></td>
      <td>1始まり</td>
    </tr>
  </tbody>
</table>

<p>型スタブ（<code class="language-plaintext highlighter-rouge">pdf_inspector.pyi</code>）にはそれぞれ “1-indexed” / “0-indexed” と明記されているので仕様どおりではあります。ただし<strong><code class="language-plaintext highlighter-rouge">extract_pages_markdown()</code> が返す1つのオブジェクトの中に、0始まりの <code class="language-plaintext highlighter-rouge">page</code> と1始まりの <code class="language-plaintext highlighter-rouge">pages_needing_ocr</code> が同居している</strong>点は要注意です。速度チューニングのために <code class="language-plaintext highlighter-rouge">detect_pdf()</code> を <code class="language-plaintext highlighter-rouge">classify_pdf()</code> に差し替えると、OCRへ送るページが1つずつずれます。</p>

<h2 id="pdf-inspectorとmineru型フル解析エンジンの使い分け">pdf-inspectorとMinerU型フル解析エンジンの使い分け</h2>

<p>日本語圏でPDFのMarkdown化というと<a href="/rag/mineru/">MinerU｜PDFをマークダウンに変換するOSSツール</a>のような、レイアウト検出・数式のLaTeX化・多言語OCRまで内蔵したエンジンが定番です。pdf-inspectorはこれらの競合ではなく、<strong>前段に置くもの</strong>と考えるのが正確です。</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>pdf-inspector</th>
      <th>MinerU型のフル解析エンジン</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>主目的</td>
      <td>分類とルーティング、軽量抽出</td>
      <td>高精度な文書構造の復元</td>
    </tr>
    <tr>
      <td>OCR</td>
      <td>無し（必要ページを通知するだけ）</td>
      <td>内蔵</td>
    </tr>
    <tr>
      <td>MLモデル</td>
      <td>使わない</td>
      <td>使う（レイアウト検出・数式認識など）</td>
    </tr>
    <tr>
      <td>スキャンPDF</td>
      <td>扱えない（OCRへ回す前提）</td>
      <td>扱える</td>
    </tr>
    <tr>
      <td>数式</td>
      <td>対応しない</td>
      <td>LaTeX化に対応</td>
    </tr>
    <tr>
      <td>速度</td>
      <td>15ページで約96ms</td>
      <td>モデル推論のぶん桁違いに遅い</td>
    </tr>
    <tr>
      <td>導入コスト</td>
      <td>パッケージ1つ・GPU不要</td>
      <td>モデル配布物とGPUを考慮</td>
    </tr>
  </tbody>
</table>

<p>役割分担はシンプルです。<strong>pdf-inspectorで分類し、<code class="language-plaintext highlighter-rouge">text_based</code> は自前で処理して終わり、<code class="language-plaintext highlighter-rouge">scanned</code> / <code class="language-plaintext highlighter-rouge">image_based</code> / <code class="language-plaintext highlighter-rouge">mixed</code> の該当ページだけをMinerUやOCR APIへ送る</strong>。Firecrawlが言う「54%はOCRを必要としない」が自社データでも成り立つなら、重いエンジンの処理量をその割合ぶん減らせる計算になります。</p>

<div class="mermaid">
flowchart TD
    A["PDFが到着"] --&gt; B["pdf-inspector で分類<br />（実測 3〜25ms）"]
    B --&gt;|"text_based"| C["ローカル抽出<br />（実測 96ms・課金なし）"]
    B --&gt;|"mixed"| D["pages_needing_ocr を取得<br />※全ページ走査の経路を使う"]
    B --&gt;|"scanned / image_based"| E["全ページをOCRへ"]
    D --&gt; F["該当ページだけOCRへ"]
    C --&gt; G["Markdown をRAGへ投入"]
    E --&gt; H["OCR結果を整形"]
    F --&gt; H
    H --&gt; G
</div>

<p>なお <code class="language-plaintext highlighter-rouge">mixed</code> の分岐では、前章のとおり <code class="language-plaintext highlighter-rouge">detect_pdf()</code> の結果をそのまま信じず、全ページを走査する経路でページ番号を取り直すのが安全です。</p>

<h2 id="ベンチマークは再現できるのか">ベンチマークは再現できるのか</h2>

<p>READMEには opendataloader-bench（200件のPDFコーパス）での比較表が載っています。pdf-inspectorが総合0.875でトップ、200件の処理が0.470秒という数字です。この種の自社ベンチは、数値だけ載って再現手段が無いことが珍しくないため、公開データの中身を確認しました。</p>

<p>結論として、<strong>再現用のデータは実際にコミットされています</strong>。README記載の再現ブランチには3,750ファイルが入っており、内訳は次のとおりでした。</p>

<p>・<strong>15エンジン分の予測結果</strong>：各203ファイル（pdf-inspector・liteparse・opendataloader・pymupdf4llm・markitdown に加え、mineru・marker・docling・nutrient・unstructured なども含む）<br />
・<strong>正解データ</strong>：<code class="language-plaintext highlighter-rouge">ground-truth/markdown/</code> に200件<br />
・<strong>評価チャート</strong>：総合・読み順・表構造・見出しなど7種のPNG<br />
・<strong>過去実行の履歴</strong>：<code class="language-plaintext highlighter-rouge">history/</code> 配下に日付別の <code class="language-plaintext highlighter-rouge">evaluation.csv</code> / <code class="language-plaintext highlighter-rouge">evaluation.json</code></p>

<p>つまり第三者が予測結果と正解を突き合わせて検証できる状態にあります。この点は正直に評価してよい部分です。</p>

<p>一方で、READMEの表の読み方には注意が要ります。<strong>掲載されている5エンジンは、データが存在する15エンジンからの抜粋</strong>です。README自身が「モデルベースのPDF解析を行わないローカルエンジンのみを表示し、OCRは無効化した」と条件を明記しており、意図的な絞り込みであることは明示されています。裏を返せば、この表はMinerUやmarkerのようなMLベースのエンジンとの優劣を示すものではありません。両者はそもそも土俵が違い、OCRを切った状態でMLベースのエンジンを比較しても本来の性能は出ないためです。</p>

<div class="box-info">
  <p><strong>READMEの数値のうち、裏取りできたもの・できなかったもの</strong></p>

  <p>・<strong>再現できた</strong>：判定10〜50ms（実測24.6ms）、テキストPDF200ms未満（実測96.2ms）<br />
・<strong>データを確認できた</strong>：ベンチの予測結果・正解データ・チャートは公開ブランチに実在<br />
・<strong>出典を確認できなかった</strong>：「PDFの約54%はOCR不要」という割合。Firecrawl自社パイプラインの経験値と読めるが、README内に根拠となる調査へのリンクは無い。自社データで再計測する前提の数字として扱うのが無難</p>
</div>

<h2 id="導入前に確認しておくこと">導入前に確認しておくこと</h2>

<p>実際に組み込む場合のチェックリストです。実測を踏まえた優先順で並べています。</p>

<p>・<strong>振り分けの正確さと速度のどちらが要るかを先に決める</strong>。速度優先なら <code class="language-plaintext highlighter-rouge">detect_pdf()</code>、取りこぼし回避なら <code class="language-plaintext highlighter-rouge">extract_pages_markdown()</code>。両者は同じファイルでも違う答えを返す<br />
・<strong>ページ番号の基点を関数ごとに確認する</strong>。<code class="language-plaintext highlighter-rouge">detect_pdf()</code> は1始まり、<code class="language-plaintext highlighter-rouge">classify_pdf()</code> は0始まり。差し替え時にずれる<br />
・<strong>表の構造抽出が必要ならNode.jsを選ぶ</strong>。Python・WASMには該当APIが無い<br />
・<strong>走査戦略を制御したいならRustから使う</strong>。3つのバインディングはいずれも既定値固定<br />
・<strong>OCRは別途用意する</strong>。pdf-inspectorは文字認識をしない<br />
・<strong>日本語PDFは自前のサンプルで確認する</strong>。CIDフォントのToUnicode CMap解読やUTF-16BE対応は実装されているが、公開ベンチのコーパスに日本語文書がどれだけ含まれるかは明示されていない<br />
・<strong>暗号化PDFはパスワードを渡す</strong>。<code class="language-plaintext highlighter-rouge">PdfOptions</code> に <code class="language-plaintext highlighter-rouge">password</code> フィールドがあり、デバッグ出力では <code class="language-plaintext highlighter-rouge">[REDACTED]</code> に伏せられる実装になっている</p>

<p>導入判断としては、<strong>すでにOCR APIへ全件を投げているパイプラインがあるなら、前段に挟む価値は高い</strong>と言えます。判定コストが数十msに対してOCRは秒単位かつ従量課金なので、テキストベースの文書が一定割合あれば投資回収は早い。逆に、扱う文書がほぼ全てスキャンPDFなら、このライブラリが返すのは「全ページOCRへ」という結論だけなので、得られるものは多くありません。</p>

<h2 id="まとめ">まとめ</h2>

<p>pdf-inspectorは、PDF解析の精度を競うツールではなく、<strong>重い処理に何を渡すかを数十msで決めるための道具</strong>です。実測ではREADMEの速度主張がそのまま再現でき、MLモデル無し・依存1つという軽さも確認できました。ベンチマークの再現データが実際に公開されている点も、この種の自社ベンチとしては誠実な部類です。</p>

<p>一方で、既定の走査が全ページを見ないこと、その結果 <code class="language-plaintext highlighter-rouge">text_based</code> と判定された文書ではOCR対象リストが確信度1.0のまま空で返ること、ページ番号の基点が関数間で揃っていないことは、READMEからは読み取れませんでした。いずれも「OCRの振り分け」という本来の用途に直結する挙動なので、組み込む前に自分のPDFで一度突き合わせておくことを勧めます。本記事に載せた10行程度の確認コードで足ります。</p>

<h2 id="参照ソース">参照ソース</h2>

<p>・<a href="https://github.com/firecrawl/pdf-inspector">firecrawl/pdf-inspector — GitHub公式リポジトリ</a>（README・<code class="language-plaintext highlighter-rouge">src/detector.rs</code>・<code class="language-plaintext highlighter-rouge">pdf_inspector.pyi</code>を参照。2026-08-16確認）<br />
・<a href="https://firecrawl.github.io/pdf-inspector/">pdf-inspector 公式デモサイト（ブラウザWASM版）</a>（2026-08-16確認）<br />
・<a href="https://github.com/firecrawl/opendataloader-bench/tree/abi/pdf-parser-benchmark-results">opendataloader-bench 再現ブランチ</a>（15エンジン分の予測データと正解データ。2026-08-16確認）<br />
・<a href="https://crates.io/crates/pdf-inspector">pdf-inspector — crates.io</a>（v1.14.2・累計99,590ダウンロード。2026-08-16確認）</p>

<!--
Distribution memo (Step 8)
X: 「PDFをAIに読ませる前に『そもそもOCR要る？』を25msで判定するRust製OSSを実測した。
    READMEの速度主張は再現できた一方、既定設定だと確信度1.0のまま画像ページを取りこぼす条件があった」＋ pdfi-gap.webp 添付
はてブ: 実測値と「READMEと実装のズレ」を軸に
内部リンク提案: RAG系の新規記事から本記事へ（文書取り込みの前処理として）
-->]]></content><author><name>編集部</name></author><category term="rag" /><category term="rag" /><category term="PDF" /><category term="OCR" /><category term="Rust" /><category term="オープンソース" /><category term="文書解析" /><summary type="html"><![CDATA[pdf-inspector（Firecrawl・MIT）はPDFがテキストベースかスキャンかを数十msで判定し、OCR不要な文書だけをローカルでMarkdown化するRust製OSS。v1.14.2を実測し、判定24.6ms・Markdown化96.2msの速度と、既定設定で見逃しが起きる条件を検証します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ai-heartland.com/generated_images/cover_pdf-inspector-pdf-ocr-routing.webp" /><media:content medium="image" url="https://ai-heartland.com/generated_images/cover_pdf-inspector-pdf-ocr-routing.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>