エージェントに原文の確認を頼んだら、「本文は取得できていません。ヒット0件でした」と返ってきました。JavaScript で描画されるためだろう、という説明まで付いていました。

ところが、保存された HTML には本文の末尾まで入っていました。手元の Claude Code では grep がシェル関数に差し替わっていて、NUL バイトを含むファイルを検索対象から外していたようです。空の出力に、AI が取得失敗の理由を付けてしまっていました。

さらに、私が件数を数えるために指定した grep -c にも間違いがありました。これは出現回数を数えるオプションではありません。確認したのは Claude Code 2.1.220、macOS、zsh、2026-08-02 時点の環境です。

確認担当を分ければ安心だと思っていた

文章を書く担当とは別に、出典を取り直して確認する担当を置いていました。確認の指示は、原文の HTML に対して grep -c を実行し、該当する語句の件数を報告する、というものです。2026-07-26 に規約に書き、1 週間ほど運用していました。

ファイルの大きさだけでは、目的の本文が取れたか分かりません。同じ URL でも取得するたびに変わります。

$ U=https://x.com/kimuai08/status/2082428727401869753
$ curl -sL -A 'Mozilla/5.0' "$U" -o x.html -w '%{size_download} bytes\n'
362384 bytes

2026-08-02 に取ったときは 362,384 バイト。前日は 362,678 バイト、同日に User-Agent を Chrome のものへ変えると 367,448 バイトでした。それで、容量よりも本文中の語句を確認することにしていました。

「0件」のはずなのに、出力に 0 がない

異変に気づいたのは、下書きの文章レビューでした。「grep -c は一致しなくても 0 を出すはずなのに、この出力例には 0 がない」という指摘です。

$ type grep
grep is a shell function from /Users/nnasaki/.claude/shell-snapshots/snapshot-zsh-1785571288677-ewdgc7.sh

いつもの /usr/bin/grep ではありませんでした。シェル関数の定義を見ます。

$ /usr/bin/grep -n 'ugrep' ~/.claude/shell-snapshots/snapshot-zsh-*.sh | head -2
4608:# Shadow find/grep with embedded bfs/ugrep
4632:    ARGV0=ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git (以下、除外ディレクトリの指定が続くため筆者が省略)

Claude Code が起動時に、同梱の ugrep を呼ぶようにしていました。付いている -I は、バイナリとみなしたファイルを検索対象から外すオプションです。

NUL バイトを一つ入れたファイルで試すと、違いが出ました。

$ printf 'hello\x00world\n' > nul.txt

$ grep -c hello nul.txt; echo "exit=$?"
exit=1

$ /usr/bin/grep -c hello nul.txt; echo "exit=$?"
1
exit=0

差し替わった grep は何も出さず、終了コード 1。/usr/bin/grep は一致した行数の 1 を返しています。エージェントは、この空の出力を「0件」と読み替えて報告していました。

スナップショットは起動のたびに生成されるので、別のバージョンでも同じ実装とは限りません。上の type grep と定義の確認は、そのために載せています。

取得した HTML には本文が入っていた

調べていたのは X の投稿です。保存した HTML の NUL バイトを数えました。

$ perl -e 'open F,"<:raw",$ARGV[0]; local $/; $d=<F>;
           print "NUL bytes: ", scalar(()=$d=~/\x00/g), "\n"' x.html
NUL bytes: 17

17 個ありました。このファイルでも、コマンドによって結果が変わります。

$ grep -c 'og:description' x.html; echo "exit=$?"
exit=1

$ /usr/bin/grep -c 'og:description' x.html; echo "exit=$?"
1
exit=0

冒頭の「JavaScript で描画されるためだろう」という説明は、もっともらしくて、そのまま受け取ってしまいました。ところが /usr/bin/grep で探せば、本文の末尾の一句まで見つかります。

$ /usr/bin/grep -c 'その1つを消すMCP' x.html
1

NUL の確認でも一度間違えています。最初は grep $'\x00' を使い、証拠ファイル 82 個すべてに NUL があると思ってしまいました。手元の実行では検索語が空文字として扱われ、全部に一致していました。上で perl を使っているのはこのためです。

自分が書いた grep -c の指示も間違っていた

もう一つ、指示の側にも間違いがありました。

$ wc -l < x.html
       9

$ /usr/bin/grep -c '再現' x.html
1

$ /usr/bin/grep -o '再現' x.html | wc -l
      10

HTML は 9 行。「再現」は 10 回出てきますが、同じ行にあるので grep -c は 1 を返します。出現回数を数えるなら、-o で一致部分を一つずつ出して wc -l に渡す必要がありました。

1 行に詰まった HTML や JSON では、この違いを見落としやすいです。件数の多さを根拠にしていた箇所は読み直しになりました。

0件と報告する前に、あるはずの語を探す

この件を受けて、規約を次のように変えました。

  • 実行するコマンドを /usr/bin/grep と明示する。バイナリをテキストとして検索する --binary-files=text という方法もある
  • 出現回数を使うなら grep -o … | wc -l で数える。-c は行数として扱う
  • 0 件と報告する前に、確実に入っている語も検索して、その結果を添える

最後の確認を入れておけば、今回も og:description のような必ず入っている語まで見つからず、検索がおかしいと気づけたはずです。こうして既知の結果で検査を確かめることを、陽性対照と呼びます。

シェル関数のコメントには find の差し替えも書かれていました。そちらでどんな違いが出るかは、まだ調べていません。

確認する担当を分けても、私が渡したコマンドが間違っていれば、同じところでつまずきます。今回頼りになったのは「確認済み」という報告より、その下に残っていた空の出力でした。件数だけに要約していたら、「0 がない」という違和感ごと消えていたと思います。