設計ドラフトをレビューしていて、「あるディレクトリが ignore 設定に登録済みかどうか」を確認しようとしました。単純な確認のはずが、使うコマンドによって結論が真逆になり、危うく誤った判断をするところでした。
犯人は CRLF 改行でした。grep はエラーを出さず、ただ静かに「ヒット0」を返す。その空結果を AI も人も「登録されていない」と解釈してしまう。テキストツールが黙って敗北していたのです。
本記事は、この経験から「存在・追跡・無視の判定は、テキスト grep ではなく VCS 専用コマンドを唯一の真実(SoT)にする」という運用にした記録です。
事の発端:grep と check-ignore が矛盾した
確認したかったのは、ある無視設定(gitignore)に .claude/worktrees/ のようなディレクトリのエントリが登録済みかどうか、でした。
まず素直に grep を打ちました。
1$ grep "worktree" .gitignore2(ヒット0)ヒット0です。普通に読めば「登録されていない」と判断するところです。ところが念のため git check-ignore を末尾スラッシュ付きで試すと、別の答えが返ってきました。
1$ git check-ignore -v ".claude/worktrees/"2.gitignore:103.gitignore の103行目を指して「登録済み」のように見えます。grep は「無い」と言い、check-ignore は「103行目にある」と言う。二つのツールが正面から矛盾しました。
原因究明:CRLF と末尾スラッシュ、二重の罠
矛盾の原因を追っていくと、罠が二重に仕掛けられていました。
罠1:CRLF 改行が grep の行判定を狂わせる
その .gitignore は CRLF 改行(\r\n) で保存されていました。awk / sed / grep といったテキストツールは、行末の \r の存在を前提していないので、行のカウントや一致判定が \r の影響で混乱します。指された103行目を実際に見ると、そこは「\r だけが残った空行」でした。エントリが書かれた行ですらなかったのです。
CRLF の厄介なところは、ツールがエラーを出さないことです。エンコーディングや改行の不整合があっても、grep は「異常です」とは言わず、ただ「マッチしませんでした」という空結果を返します。空結果は、本当に無いのか、ツールが取りこぼしたのか、出力からは区別がつきません。
罠2:check-ignore の末尾スラッシュ出力が誤解を招く
もう一つの罠が git check-ignore の側にありました。-v(verbose)を付けて末尾スラッシュ付きのディレクトリ形式を渡すと .gitignore:103 を返すのですが、これが誤解を招く出力でした。
そこで検証方法を変えてみました。
1$ git check-ignore -n ".claude/worktrees/anything"2:: (マッチなし=実際には ignore されていない)3
4$ git show HEAD:.gitignore | grep worktree5(ヒット0)-n(non-matching を表示)で問い直すと、返ってきたのは ::、つまり実際には ignore されていないという答えでした。さらにコミット版を git show HEAD:.gitignore で取り出して grep しても、ヒット0。コミットされた版にもエントリは無かったのです。
最終的な結論:実際には ignore されていなかった
複数の検証方法を突き合わせた結果、正しい結論は「実際には ignore されていない」でした。各コマンドの言い分を整理すると、こうなります。
| コマンド | 出力 | 解釈 |
|---|---|---|
grep "worktree" .gitignore | ヒット0 | CRLF で黙って失敗。空結果=結論不明 |
git check-ignore -v "...worktrees/" | .gitignore:103 | 末尾スラッシュの罠。103行は空行 |
git check-ignore -n "...worktrees/anything" | :: | ignore されていない(信頼できる) |
git show HEAD:.gitignore | grep worktree | ヒット0 | コミット版にエントリ無し(裏取り) |
最初の grep も、check-ignore -v の末尾スラッシュ出力も、どちらも誤った方向へ導きました。決め手になったのは git check-ignore -n(非マッチ表示)と git show HEAD: という、VCS 専用の SoT コマンドでした。
学び:判定は VCS 専用コマンドを真実とする
この一件から得た学びは、はっきりしています。
AI にテキスト処理スクリプトを生成させて「ファイルにこの文字列があるか」を判定させるとき、対象が CRLF やエンコーディングの差を含むと、grep / awk / sed はエラーを出さず黙って空結果を返します。そして AI も人も、その空結果を「存在しない」と早合点します。
特に次の種類の判定は、テキスト grep に任せてはいけません。
- 無視判定(ignore されているか)→
git check-ignore - 追跡判定(追跡対象か)→
git ls-files - コミット版の内容確認(コミットに含まれるか)→
git show HEAD:
これらは Git が状態を正確に知っている領域です。テキストとしてファイルを舐めるのではなく、Git に直接問い合わせるほうが、CRLF や空白の揺れに惑わされません。
ただし、同じ Git コマンドでも出力形式の罠が残ります。今回の check-ignore のように、末尾スラッシュ付き / -n / --no-index で結果が変わることがあります。だから結論は、複数の検証方法で裏取りするのが鉄則です。
パターン化:『空結果=存在しない』と即断しない
この事故を一般化すると、核心は「空結果を結論にしない」という一点に尽きます。
ツールが空(ヒット0)を返したとき、それは「存在しない」ではなく「このツールでは見つけられなかった」を意味する。エラーが出ない空結果ほど、結論として信頼してはならない。
grep の空結果は、
- 本当に存在しない
- CRLF / エンコーディングで取りこぼした
- 検索パターンが微妙にずれていた
——のどれなのか、出力だけでは区別できません。エラーで落ちてくれれば気づけますが、静かに空を返すのがいちばん危険です。だから「無い」を確定させたいときほど、別系統のコマンド(VCS 専用)で裏を取ります。
運用Tips:判定をAIに任せるときの3つの約束
AI に「ファイルにこのエントリがあるか確認して」と頼むときの、実務的な約束事です。
- 無視 / 追跡 / コミット内容の判定は、grep ではなく
git check-ignore/git ls-files/git show HEAD:を使え、とプロンプトに明示する。 - 「空結果=存在しない」と即断せず、異なる出力形式や別コマンドで裏取りしてから結論を出せ、と指示する。今回の
-vと-nの食い違いがまさにこれです。 - 結果が矛盾したら、矛盾を放置せず原因まで掘る。grep と check-ignore がずれた時点で「どちらかが嘘をついている」と疑い、CRLF までたどり着いたのが正解でした。
この約束事の価値は、検証時間の削減量で測れる以前に、「ツールが黙って空を返した結果を、AI がそのまま結論にする」という事故を防げる点にあります。
まとめ:黙って敗北するツールを信じない
CRLF や空白、エンコーディングの差は、テキストツールをエラーなしで敗北させます。grep はヒット0を返し、AI も人もそれを「存在しない」と読む。けれど実際には、ツールが取りこぼしているだけかもしれません。
だから、
- 無視 / 追跡 / コミット内容の判定は、grep ではなく
git check-ignore/git ls-files/git show HEAD:を SoT にする - 同じコマンドでも出力形式(末尾スラッシュ /
-n/--no-index)で結果が変わる罠を疑う - 結論は複数の検証方法で裏取りする
- 「ツールが空を返した=存在しない」と即断しない
ツールが落ちて教えてくれるなら、まだ親切です。本当に怖いのは、何事もなかったかのように空を返すツールです。判定をそのツールに委ねるのではなく、状態を正確に知っている VCS に直接問い合わせる——それが、静かな誤判断を防ぐ習慣になります。