エージェントに調べ物をさせて、その結果を信じてよいか確かめている人向けです。 「原文を取ってきて、この語句を grep して件数を報告せよ」という指示は、裏取りのやり方としてかなり筋がよく見えます。実際、私はそれを規約に書いて1週間ほど運用していました(明文化したのは 2026-07-26 です)。その規約が破れたので、破れ方と直し方を書きます。手元で最小再現まで取れたので、コマンドと出力をそのまま載せます。

先にまとめです。

  • エージェントハーネスは、あなたが呼んだつもりのコマンドを別物に差し替えていることがあります。 私の環境で grep を呼ぶと、それは GNU/BSD の grep ではなくシェル関数で ugrep -I に差し替わっていました(Claude Code 2.1.220 / macOS / zsh、2026-08-02 時点)。-I はバイナリ扱いのファイルを飛ばすオプションで、NUL バイトが1個でもあると、何も出力せず終了コード1 を返します。grep -c が出すはずの 0 すら出ません
  • そして grep -c が数えているのは行数であって出現回数ではありません。圧縮された1行の HTML では、何を探しても件数が 1 になります
  • この2つが重なった結果、サブエージェントが「本文は取得できていません(ヒット0件)」と報告してきました。実際には本文が全文入っていました

差し替えの中身はハーネスごとに違うはずですが、「自分が指定したコマンドが、その名前どおりのものとは限らない」という形は、どのハーネスでも同じだと思います。以下、私の環境での実測です。

なぜ「件数を報告せよ」にしていたか

先に背景を書きます。読み飛ばして構いません。

エージェントに文章を書かせると、もっともらしい嘘が混ざります。厄介なのは、書いた本人にそれが検出できないことです。自分の出力を自分で検算しても、生成したときと同じ経路をたどって同じ結論に戻ります。

そこで「書く人」と「原典に当たり直す人」を分けて、後者には次の形で報告させていました。

該当句を生 HTML に対して grep -c し、ヒット数を報告せよ。バイト数は同一性の証明にならない(動的サイトはリクエストごとに揺れる)。

バイト数を退けているのは、それが証拠にならないからです。同じ 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 取得です。前日 2026-08-01 に同じコマンドで取ったときは 362,678 バイト、同日に User-Agent を Chrome のものに変えたときは 367,448 バイトでした。どれも「取れた」のに、数字は一致しません。 だから「その語句が原文に在るか」という形の証拠、つまり grep の件数に寄せていた、という経緯です。

方針としては、今でも正しいと思っています。壊れていたのは道具のほうでした。

試した結果

grepgrep ではなかった

きっかけは、レビューを頼んだエージェントの指摘でした。「grep -c はヒット0でも 0 を1行出力してから終了コード1で終わるはずだが、記事の出力例に 0 が無い」。

確かめると、そのとおりに動いていませんでした。まず正体を見ます。

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

シェル関数でした。中を見ます。

$ /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 が起動時に、grep を同梱の ugrep へ差し替えていました。速度のための実装で、それ自体は妥当な判断だと思います。問題は付いているオプションのほうで、-I は「バイナリとみなしたファイルを検索対象から外す」オプションです。

確かめたのは自分の環境1つだけです。 Claude Code 2.1.220、macOS、zsh、2026-08-02 時点。スナップショットは起動のたびに生成されるもので、実装はいつでも変わりえます。あなたの環境でも同じかは、上の type grep を自分で叩いて確かめてください。

最小再現です。NUL バイトを1つ挟んだだけのテキストファイルを作ります。

$ 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

差し替わったほうは何も出力しません0 すら出ません。

ここが一番効きます。もし 0 と出るなら、報告を受けた側は「0件だった」と読めます。何も出ないと、「0件だった」と「そもそも検査できていなかった」が区別できません。そして今回、サブエージェントは空の出力を見て「0件」と要約してきました。

実物で何が起きていたか

題材にしていたのは 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 にとってこのファイルは検索対象外です。

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

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

同じファイル・同じ検索語で、片方は「無い」、もう片方は「有る」。 サブエージェントは前者を見て「本文が取得できていない。JavaScript で描画されるためだろう」と報告してきました。もっともらしい説明が付いていたので、私はそのまま受け取りました。

実際には、記事の末尾の一句まで HTML に入っていました。

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

ついでに書いておくと、NUL の有無を調べるのに grep $'\x00' は使えません。シェルが $'\x00' を空文字に潰すので、全ファイルが一致します。私は最初これで調べて、82個の証拠ファイル全部が「NUL あり」という結果を見て一瞬信じました。上で perl を使っているのはそのためです。

もう1つの嘘: -c は行数である

こちらは仕様どおりの挙動ですが、証拠として使うときには同じくらい効きます。

$ wc -l < x.html
       9

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

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

9行しかない HTML です。「再現」は10回出てきますが、grep -c1 と答えます。行数を数えているからです。

minify された HTML、JSON、1行に詰めたログ。この種のファイルでは、-c の値はほぼ常に 10 です。「N 件ヒットした」を証拠として扱う規約が、実は「在るか無いか」しか運んでいなかったことになります。件数の多寡に意味を持たせていた箇所は、全部読み直しになりました。

手元にどう効きそうか

ここから先は推測です。

同じことが find にも起きているのではないか。 上のシェル関数のコメントは “Shadow find/grep with embedded bfs/ugrep” でした。find も差し替わっています。まだ確かめていませんが、既定の除外や挙動の差が同じ形で効く可能性はあります。

「陽性対照が無い検査は検査ではない」のほうが、本質的な教訓ではないか。 陽性対照というのは実験科学の言い方で、答えが分かっているサンプルを一緒に流して、検査そのものが動いていることを確かめることです。grep が返す 0 には「本当に無い」と「検査が動いていない」の2つの読み方があり、出力からは区別できません。だから /usr/bin/grep -c '<html' x.html のような必ず在る語を1本添えて、それが 1 を返したときにだけ、続く 0 を「無い」と読むわけです。

今回、私は「ヒット0でした」という報告を疑いませんでした。疑う手がかりが出力に無かったからです。もし規約に「ヒット0を報告するときは、確実に在る語で1本取って一緒に出せ」と書いてあれば、その1本も0件になって、その場で気づけたはずです。これは grep の話に限りません。否定的な結果を根拠にするときは、検査が生きていることを別に示さないと、その結果は何も意味しません。

エージェントに実行環境の癖を説明させても、たぶん効かないのではないか。 今回、間違えたのはサブエージェントですが、同じ環境で私も同じ結果を得ています。モデルの注意力の問題ではなく、道具が黙って嘘をつくかどうかの問題なので、対処はプロンプトではなくコマンドの指定側に置くのが筋だと思っています。

直したもの

規約側を3点変えました。

  • 照合に使うのは /usr/bin/grep と明示する(--binary-files=text でも同じことができます)
  • 件数を根拠にするなら grep -o … | wc -l を併記する-c の値は「在る/無い」としてだけ読む
  • ヒット0を報告する前に、確実に在る語で陽性対照を1本取る

3点目が一番効くと思っています。前の2つは今回見つかった穴を塞ぐだけですが、3点目はまだ見つけていない穴に対しても働きます

まとめ

  • エージェントハーネスの中の grep は、grep とは限りません。 私の環境(Claude Code 2.1.220、2026-08-02 時点)では ugrep -I に差し替わっていました。NUL を1個含むだけで、何も出力せず終了コード10 すら出ないので、「0件」と「検査できていない」が区別できません。まず type grep を叩いて、自分の環境の正体を確かめてください
  • grep -c は行数です。1行に詰まった HTML や JSON では、何を探しても 1 になります
  • この2つが重なって、「本文が取得できていない」という誤報告が通りました。実際には全文入っていました。証拠として決めたコマンドが、証拠にならない値を返していたわけです
  • 対処は /usr/bin/grep の明示・-o | wc -l の併記・陽性対照を必ず1本取ること。最後のものだけが、まだ知らない穴にも効きます

なお、この件を見つけたきっかけは、記事の下書きに対する文章レビューでした。事実照合(原文にそう書いてあるか)を担当したエージェントは、この不具合を捕まえていません。同じ壊れた道具を使っていたからです。 検査を独立させるというのは、主体を分けるだけでは足りなくて、道具も分けないと成立しないのかもしれません。ここはまだ整理できていません。