Claude CodeにGlobとGrepが無い理由 — bfsとugrepがBash経由で動く仕組み
macOS・Linux・WSLのClaude Codeは既定でGlobとGrepを持たず、find・grepを組み込みのbfs・ugrepとしてBashで動かします。hookと権限ルールからの見え方と、ツールを戻す条件を示します。
Claude CodeにGlobとGrepが無い理由 — bfsとugrepがBash経由で動く仕組み
ツール一覧にはGlobとGrepが載っているのに、macOSやLinuxのセッションでは呼ばれない。PreToolUseのmatcherにGrepと書いたhookが一度も発火しない。原因は不具合ではなく仕様です。
macOS・Linux・WSLでは、Claude CodeはGlobとGrepを既定のツールセットから外しています。Claudeはその代わりにBashツールでfindとgrepを実行し、Claudeのシェルの中ではこの2つが組み込み版のbfsとugrepで動きます。Windowsでは、2つとも既定のツールセットに入っています。
この仕様のせいで、検索はhookにも権限ルールにもBashの呼び出しとして届きます。GrepやGlobを名指しした設定は、既定のままでは何にも当たりません。
検索がBash呼び出しとして見えると、何が変わるのか
設定の当たり方が変わる3か所
hookのmatcher
GrepやGlobを指定したPreToolUseは対象のツール呼び出しが無いので発火しません。検索を捕まえるならBashのmatcherで、コマンド文字列を見ます。権限ルール
findとgrepは組み込みの読み取り専用コマンドに入っており、Bashの許可リストに書かなくても確認なしで動きます。止めたいならaskかdenyのルールを足します。Readのdenyルール
GrepとGlobには
Readのdenyルールが効きますが、Bash経由の検索では効き方が変わります。.envを守る目的ならサンドボックスまで視野に入ります。
3つとも根は同じです。ツールの種類がGrepからBashに変わったので、ツール名で絞る設定が外れます。
手元のClaude Codeでは、find・grepはシェル関数になっていた
仕様の文面だけでなく、実際にシェルの中がどうなっているかを見ておきます。v2.1.301のmacOS(zsh)でClaude Codeのセッション内から確かめた結果です。
grep --version | head -1
find --version | head -1
type grep出力は次の形でした(1行目のみ抜粋、パスは省略)。
ugrep 7.8.4 aarch64-apple-macosx ...
bfs 4.1.1
grep is a shell function from ...grepとfindは、システムの実行ファイルではなくシェル関数でした。関数の中身を見ると、Claude Code自身の実行ファイルを別名で呼び直しています。zshでは次のような形です(引数の素通し部分は省いた要約)。
# grep の中身(要約)
ARGV0=ugrep <claude実行ファイル> -G --ignore-files --hidden -I \
--exclude-dir=.git --exclude-dir=.svn --exclude-dir=.hg ... "$@"
# find の中身(要約)
ARGV0=bfs <claude実行ファイル> -S dfs -regextype findutils-default "$@"読み取れることは3つあります。
- 別途インストールしたツールではなく、Claude Codeの実行ファイルが
ugrep・bfsとして振る舞っています。システムのgrepやfindは使われません grepには-G(基本正規表現)、--ignore-files(.gitignoreなどに従う)、--hidden、-I(バイナリを除外)が既定で付きます。.gitなどのバージョン管理ディレクトリは既定で除外されます-zや--filterなど一部のオプションを含む呼び出しでは、関数は組み込み版ではなくシステムのgrepにそのまま渡します
これは2.1.301のmacOSで見えた実装です。関数の中身やオプションは版で変わり得るので、自分の環境ではtype grepで確かめてください。なお、同じ仕組みでrgも組み込み版を呼ぶ関数になっていました。
正規表現の書き方はPOSIX寄りのままになる
grepが-Gで動くので、波括弧は普通の文字として扱われます。手元でa{b}という行を含むファイルに対し、grep 'a{b}'は1行に一致し、grep -E 'a\{b\}'も一致しました。
一方、実体のGrepツールはripgrep製で、パターンの書式もripgrep流です。Goのinterface{}を探すにはinterface\{\}とエスケープします。ツールを戻すと正規表現の癖が変わる点は、後の節でも触れます。
hookで検索を捕まえるにはBashのmatcherを使う
PreToolUseは、ツール名で絞れるイベントです。matcherにはBash・Read・Glob・Grepなど組み込みツールの名前を書けます。ただ、検索がBash経由になったセッションでは、Grepと書いたmatcherに当たる呼び出しが存在しません。
検索を記録したいなら、Bashのmatcherで受けて、標準入力のJSONからtool_input.commandを取り出します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/log-search.sh" }
]
}
]
}
}#!/bin/bash
# log-search.sh — find / grep で始まるコマンドだけ記録する例
CMD=$(jq -r '.tool_input.command')
case "$CMD" in
find\ *|grep\ *) echo "$CMD" >> ~/.claude/search.log ;;
esac
exit 0これは構成の一例です。cd src && grep -r foo .のように複合コマンドになると先頭がgrepにならず、caseの判定をすり抜けます。パイプや&&の後ろまで見たいときは、コマンド全体を正規表現で見る形に広げます。
GrepツールとGlobツールの入力項目も押さえておきます。Grepはpattern・path・glob・output_modeなどを、Globはpatternをtool_inputに持ちます。ツールを戻した構成でhookを書くなら、Bashとは別のmatcherを足す形になります。
macOSとWindowsで同じhook設定を共有するとき
Windowsでは、GlobとGrepが既定のツールセットに残っています。macOSとWindowsの両方で同じsettings.jsonを使うチームでは、BashだけのmatcherだとWindowsの検索を拾えません。
matcherの値は、英数字・_・-・空白・,・|だけなら正確な名前の一覧として扱われ、Bash|Grep|Globと書くとこの3つのどれにも一致します。それ以外の文字を含むと、先頭や末尾を固定しない正規表現として評価されます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Grep|Glob",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/log-search.sh" }
]
}
]
}
}同じmatcherでもBashの呼び出しはtool_input.commandを、Grepはpatternを持つので、スクリプトはtool_nameを見て読む項目を分けます。上のlog-search.shを共有するなら、jq -r '.tool_name'で分岐する形に広げます。
権限ルールの見え方 — 確認なしで動き、止めるには明示が要る
権限の側にも、見え方の変化があります。ls・cat・head・tail・grep・find・wcなどは読み取り専用コマンドとして、確認なしで実行される組み込みの集合に入っています。この集合は設定で変えられません。
つまり、ClaudeがBash経由で検索しても、Grepツールと同様に確認は出ません。止めたいコマンドがあるときは、そのコマンドにaskかdenyのルールを足します。
{
"permissions": {
"ask": ["Bash(grep *)"]
}
}例外もあります。Manualモードでは、findのように書き込み系・実行系のフラグを持つコマンドは、クォートされていないglobを含むと確認が出ます。-deleteのようなフラグにglobが展開される恐れがあるためです。
Readのdenyルールが届く範囲
秘密ファイルを守る目的でRead(./.env)のdenyルールを書いている場合は、検索経路ごとに届き方が違います。
| 経路 | Readのdenyルール |
|---|---|
| Readツール | Readのdenyルール適用される |
| GrepツールとGlobツール | Readのdenyルール可能な範囲で適用される |
Bashのcat・head・tail・sed・teeとリダイレクト | Readのdenyルール認識できる形なら適用される |
grep -r pattern .のようにファイルを名指ししない実行 | Readのdenyルール適用されない |
| PythonやNodeのスクリプトによる間接的な読み取り | Readのdenyルール適用されない |
Bash経由の検索が主役になると、grep -rが表の4行目に当たる場面が増えます。ファイルを一切読ませたくないなら、OSレベルで効くサンドボックスまで含めて設計します。
権限とサンドボックスは別の層です。権限ルールはBashやReadを含む全ツールの使用可否とアクセス先を決め、サンドボックスはシェルコマンドとその子プロセスのファイル・ネットワークアクセスをOSの側で制限します。サンドボックスの設定に書いたパスと、権限ルールのパスは最終的な設定に統合されます。プロンプトインジェクションでClaudeの判断をすり抜けられても、サンドボックスの制限は残ります。Read(./.env)のdenyルールを書いたうえで、サンドボックスも有効にしておく二重の構えです。有効にする手順と設定キーはサンドボックスのページにあるので、使う前に自分の版の記載を確かめてください。
もう1点、Glob(docs/**)のようなGlobへのパス指定ルールは受け付けられても参照されず、起動時に警告が出ます。パスで絞りたいときはRead(docs/**)と書きます。
GlobとGrepを戻す条件と、戻したときの違い
戻す方法は何通りかあります。フラグごとの細かい書き分けは--toolsフラグの解説に詳しく、ここでは条件だけを並べます。
| 戻す条件 | 戻るもの | 備考 |
|---|---|---|
--toolsにGlobかGrepを書く | 戻るもの書いたほうだけ | 備考Agent SDKの同等のオプションでも同じ |
--allowedToolsに片方を書く | 戻るもの両方 | 備考設定ファイルの許可ルールにこの効果は無い |
| Bashをセッションから外す | 戻るもの両方 | 備考denyルール・--disallowedTools・--restricted |
サブエージェントのtoolsにGlob/Grepを書き、Bashを書かない | 戻るものそのサブエージェント | 備考--agentで起動すればセッション全体 |
--toolsで名前を明示したときに戻る挙動は、v2.1.162で入っています。それ以前は、これらの名前が黙って無視されていました。
戻すと、検索の中身も入れ替わります。
| 項目 | Bash経由(ugrep・bfs) | 実体のGrep・Glob |
|---|---|---|
| 検索エンジン | Bash経由(ugrep・bfs)組み込みのugrep(-Gが既定) | 実体のGrep・GlobGrepはripgrep |
| gitignoreの扱い | Bash経由(ugrep・bfs)--ignore-filesで.gitignoreに従う | 実体のGrep・GlobGrepは従う、Globは既定で従わない |
| 結果の上限 | Bash経由(ugrep・bfs)コマンド出力の上限に従う | 実体のGrep・GlobGlobは更新日時順で100ファイル |
| hook・権限での見え方 | Bash経由(ugrep・bfs)Bashとして届く | 実体のGrep・GlobGrep・Globとして届く |
Globを.gitignoreに従わせたいときは、起動前にCLAUDE_CODE_GLOB_NO_IGNORE=falseを設定します。gitignoreまわりの挙動の詳細はrespectGitignoreとGlob・Grepの違いにまとまっています。Globのタイムアウトを調整したい場合はCLAUDE_CODE_GLOB_TIMEOUT_SECONDSの解説を参照してください。
hookが発火しないときの切り分け
「Grepのhookが動かない」「Globへの許可ルールが効かない」と感じたとき、原因の候補は次の順に絞れます。
| 症状 | 最初に疑うこと |
|---|---|
Grep・Globのmatcherが発火しない | 最初に疑うことmacOS・Linux・WSLの既定でツールが無い。Bashのmatcherに切り替える |
| Bashのmatcherでも検索を拾えない | 最初に疑うこと複合コマンドやパイプで先頭がgrep・findにならない |
Readのdenyがあるのに中身を検索された | 最初に疑うことgrep -rのようにファイルを名指ししない実行。サンドボックスを検討 |
| WindowsだけBashのmatcherで検索を拾えない | 最初に疑うことWindowsはGrep・Globが既定で残る。Bash|Grep|Globのmatcherにする |
grepの結果が手元のシェルと違う | 最初に疑うこと-I・--ignore-files・--hiddenの既定が付いた組み込みugrepになっている |
コンテナ内でgrepが動かない | 最初に疑うことugrep側の不具合の可能性。amd64コンテナの事例を参照 |
経緯と、どちらを選ぶかの判断材料
この構成はv2.1.117で入りました。changelogには、macOSとLinuxのネイティブビルドでGlobとGrepのツールが組み込みのbfsとugrepに置き換わり、別ツールの往復が要らなくなる、と書かれています。Windowsとnpmでインストールしたビルドは変わりません。当時の整理はv2.1.117のリリースノートにあります。その後もv2.1.235で、組み込みgrepが異常なパターンでメモリを使い切らず早めに失敗するよう改善されています。
戻すかどうかは、hookと権限をどう使っているかで決まります。
- 検索の頻度や対象をログに残したいだけなら、Bashのmatcherで足ります。既定のままでよい構成です
- hookや権限のルールを
Grep・Globの名前で書いた資産が既にあるなら、--toolsか--allowedToolsで戻すと、書き換えずに動きます - 検索エンジンの差(ugrepとripgrepの正規表現の癖)で結果が変わると困るなら、どちらかに揃えます。セッションごとに混在させると、同じパターンが片方で通らない事態が起きます
迷ったら、まず既定の構成でBashのmatcherを試し、足りない部分だけをツールを戻す側で補う順序が、設定の差し替えが少なく済みます。
まとめ
macOS・Linux・WSLでGlobとGrepが見えないのは、v2.1.117以降の既定仕様です。検索はBashのfind・grepとして動き、hookも権限ルールもBashの目で見ます。GrepやGlobの名前で書いた設定は、ツールを戻さない限り空振りします。
手元のv2.1.301では、grepとfindはClaude Code自身をugrep・bfsの別名で呼ぶシェル関数でした。正規表現は-Gの基本正規表現で、gitignoreに従い、隠しファイルは含め、バイナリは除きます。設定を直す前に、type grepで自分の環境の実体を確かめておくと、原因の切り分けが早く済みます。