- date
- entry
- 042
- topic
- tooling
- rev
- —
Claude Code 裡的 grep -r 不搜 .gitignore 列的檔案:同一個字串,6 個檔案和 13 個
同一個 repo、同一個字串,Claude Code 的 Bash 工具裡 grep -rl 回 6 個檔案,command grep 回 13 個(加入本篇之前量的)。工具裡的 grep 是帶著 --ignore-files 的 ugrep。
我很常用 grep -rl 找某個字串出現在哪些檔案,也很少懷疑它的答案:搜尋範圍內的檔案有那個字串,就會被列出來。在 Claude Code 裡,agent 查東西時用的通常也是 Bash 工具裡的 grep。
但同一個 repo、同一個字串,在 Claude Code 的 Bash 工具裡 grep -rl indexnow-ping-defaults . 回 6 個檔案,換成 command grep 回 13 個。〈叫 Bash 的工具跑的是 zsh〉談過這個工具的 shell 是哪一個;這篇要談的是它的 grep 是哪一個。依序回答四件事:怎麼確認、和系統 grep 差在哪三個地方、什麼時候會讓結論錯,以及同一個工作階段裡為什麼其實有兩種 grep。
先確認:grep 是一個 shell 函式
在工具裡問 shell 這個名字是什麼(Claude Code 2.1.299,macOS):
$ type grep
grep is a shell function from /Users/…/.claude/shell-snapshots/snapshot-zsh-….sh
$ grep --version | head -1
ugrep 7.8.4 aarch64-apple-macosx …
$ command grep --version | head -1
grep (BSD grep, GNU compatible) 2.6.0-FreeBSD
函式本體(whence -f grep,節錄):
grep () {
… # 參數裡有 -Z、-z、--null、--config 之類的,改呼叫 command grep
local _cc_bin="${CLAUDE_CODE_EXECPATH:-}"
…
ARGV0=ugrep "$_cc_bin" -G --ignore-files --hidden -I \
--exclude-dir=.git --exclude-dir=.svn --exclude-dir=.hg \
--exclude-dir=.bzr --exclude-dir=.jj --exclude-dir=.sl ${1+"$@"}
}
也就是說,grep 執行的是 Claude Code 自己的執行檔,以 ugrep 的身分跑,前面固定加了一串參數。find 同理,是內建的 bfs;這個工作階段的 rg 也是同一個執行檔。Claude Code 的工具文件(Tools reference 的 Glob 一節)寫了這件事:在 macOS、Linux 和 WSL,預設工具集不含 Glob 和 Grep,Claude 改用 Bash 裡的 find 和 grep,「In Claude's shell those two commands run embedded versions of bfs and ugrep」。changelog 2.1.117(2026 年 4 月 22 日)也有同一項,當時限 macOS 與 Linux 的原生版本,Windows 和 npm 安裝的版本不變。文件同一處提到 Grep 工具會跳過 gitignore 的檔案;Bash 裡的 grep 會不會,我在那一節沒找到說明,下面是實測。
三個差別:ignore 檔、二進位檔、路徑寫法
一個測試目錄,.gitignore 寫 dist/ 和 .env,四個檔案都含 API_URL:.env、dist/app.js、src/app.js,和一個中間有 NUL 位元組的 src/blob.bin。這個目錄不是 git repo。
$ grep -rl API_URL . # 工具裡的 grep
src/app.js
$ command grep -rl API_URL . | sort # 系統的 grep
./.env
./dist/app.js
./src/app.js
./src/blob.bin
--ignore-files:遞迴時套用沿途每一層.gitignore的排除規則(含萬用字元與!否定規則),符合的檔案和目錄不搜。不需要是 git repo,有.gitignore檔就算數。-I:忽略二進位檔裡的匹配。src/blob.bin裡有 NUL 位元組,被判成二進位,因此不見。- 以
.為起點時,路徑沒有./前綴。拿輸出去和系統 grep 的結果比對前,要先把路徑正規化。
被 ignore 的東西不是搜不到,是不會在遞迴時被走進去。直接點名可以避開這個例子的父層 ignore(-I 之類的其他篩選照樣適用):grep -c API_URL .env dist/app.js 兩個檔案各算出 1 行相符(指令結束碼 0),grep -r API_URL dist 也找得到;從 dist/ 裡面往下搜同樣找得到,因為那份 .gitignore 在上一層,遞迴不會經過它。結果取決於你從哪個目錄開始搜,這一點比「會跳過 ignore 的檔案」更容易忘。
什麼時候會讓結論錯
回到開頭那兩個數字(2026 年 10 月 15 日、這篇加進 repo 之前量的)。這個站的 .gitignore 列了 node_modules/ 和 dist/:
$ grep -rl indexnow-ping-defaults . | wc -l
6
$ command grep -rl indexnow-ping-defaults --exclude-dir=node_modules --exclude-dir=.git . | wc -l
13
依當時的比對,差的 7 個都在 dist/。如果要找的只是沒被 ignore 的原始檔,少掉建置產物剛好符合目的。問題在於要查的就是建置產物的時候:「這個字串有沒有被打包進去」「舊網址還有沒有殘留在輸出裡」「金鑰有沒有漏進 bundle」。從 repo 根目錄 grep -r 不會走進 dist/,它的結果回答不了這些問題;原始碼裡剛好沒有那個字串時,它的回答就是「沒有」。
結束碼也跟著變。只有被 ignore 的檔案含那個字串、而且沒有讀取錯誤時,工具裡的 grep 回 1(沒找到),系統的 grep 回 0(TOKEN=abc、API_URL=staging 都是測試用的假值):
$ cd only
$ cat .gitignore
secret.txt
$ grep -r TOKEN .; echo "exit=$?"
exit=1
$ command grep -r TOKEN .; echo "exit=$?"
./secret.txt:TOKEN=abc
exit=0
假設有一行部署前的守門寫成 grep -rFq -- "$API_KEY" . && { echo "金鑰外洩"; exit 1; }(-F 讓金鑰照字面比對,不當正規表示式),而非空的金鑰只出現在 dist/、搜尋也沒有錯誤:在終端機或 CI 裡跑,它會進入阻擋的分支;請 agent 在 Claude Code 的 Bash 工具裡直接跑同一行,它不會進入那個分支,後面的部署會不會繼續,取決於腳本怎麼處理這一行的結束碼(這是照上面的實測推的假設情境,不是我遇過的事故)。
同一個工作階段裡,其實有兩種 grep
函式是這個 shell 的狀態(括號開出來的 subshell 會繼承)。任何「再去執行一個程式」的包裝,xargs、find -exec、timeout、env,都看不到 shell 函式,會從 PATH 找真正的執行檔;在這個工作階段的 PATH 下,找到的是 /usr/bin/grep:
$ echo . | xargs grep -rl API_URL | sort
./.env
./dist/app.js
./src/app.js
./src/blob.bin
$ bash -c 'type grep'
grep is /usr/bin/grep
bash script.sh 和 zsh -c '…' 也一樣:另外啟動、沒有重新載入函式的 shell,用的是系統的 grep(用 source 在目前 shell 執行的腳本則仍會用到函式)。所以同一個工作階段裡,直接打的 grep -r 跳過 ignore 的檔案,經過 xargs 或另起 shell 執行的不跳過。函式本身也有例外:參數裡出現 -Z、-z、--null 等,它直接交給 command grep,grep -rlZ API_URL . 因此列出全部四個檔案。
rg 的情況更直接。snapshot 裡 grep 和 find 是無條件被函式取代;rg 的函式則是備援,只在找不到 rg 執行檔時才定義。這個工作階段的 PATH 上沒有 ripgrep,所以只有函式:
$ timeout 5 rg --version
timeout: failed to run command ‘rg’: No such file or directory
$ echo x | xargs rg --version
xargs: rg: No such file or directory
我是在寫這篇的同一個工作階段撞到這一行的:為了避免搜尋跑太久,順手在 rg 前面加了 timeout 100。
我怎麼寫
- 要和終端機裡的結果一致,寫
command grep。在這個環境它會用/usr/bin/grep(要完全確定就直接寫這個絕對路徑)。在 macOS 上那是 BSD grep,和 GNU grep 也有差,但至少和我平常在終端機看到的一樣。 - 要搜被 ignore 的目錄,直接把它當參數。
grep -r X dist/會搜,grep -r X .不會進去。 - 要比對兩次搜尋的結果,兩次用同一種 grep。一次直接打、一次經過
xargs,差的可能不是內容,而是工具。 rg只有函式時,不要在它前面加timeout或xargs。它們會找不到rg;要限時就用 Bash 工具本身的 timeout 參數(文件寫明逾時的指令可能被移到背景,不一定立刻停止)。
如果只記得一件事
在這個 Claude Code 2.1.299 工作階段的 Bash 工具裡,grep -r 是會套用 .gitignore、忽略二進位檔匹配的 ugrep(參數沒有觸發改用系統 grep、也沒有用 -a 或 --no-ignore-files 之類覆寫預設時)。開頭那 6 和 13 差的是建置產物;要問的如果正是建置產物裡有什麼,用 command grep,或把目錄直接寫在參數上。
修訂紀錄
- 首次發布