エージェントに原文の確認を頼んだら、「本文は取得できていません。ヒット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 がない」という違和感ごと消えていたと思います。