CLI AI

Grep recursively while excluding a directory

2026-06-08

grep -rn --exclude-dir={.git,node_modules,vendor} 'TODO' . finds every match while skipping the three directories that ruin most recursive greps in a real repository. --exclude-dir only kicks in together with -r, and it compares bare directory names, not paths. Below is the full recipe, from the naive recursive search to the point where you just install ripgrep instead.

1. Search the whole project

clai
$ clai find TODO in every file inside g→ grep -rn 'TODO' g/g/src/a.js:1:TODO fixg/node_modules/b.js:1:TODO dep

-r recurses into subdirectories, -n adds the line number. The problem shows immediately: half the matches come from someone else's code.

2. Exclude one directory

clai
$ clai find TODO but don't look inside node_modules→ grep -rn --exclude-dir=node_modules 'TODO' g/g/src/a.js:1:TODO fix

--exclude-dir only works together with -r. The directory name is a pattern, not a path — don't add slashes.

3. Exclude several at once

clai
$ clai find TODO, skip node_modules, .git, and vendor→ grep -rn --exclude-dir={.git,node_modules,vendor} 'TODO' .

The shell expands the braces before grep ever runs, so grep receives three separate flags. In a sh without brace expansion you'll need three --exclude-dir flags in a row instead.

4. Search only the files you care about

clai
$ clai find TODO only in javascript files→ grep -rn --include='*.js' 'TODO' .

--include is an allowlist. When you already know where to look, it's shorter and faster than a long chain of exclusions.

5. Don't trip over binaries

clai
$ clai find TODO, ignoring binary files→ grep -rnI 'TODO' .

Without -I, grep prints "Binary file … matches" for every binary hit instead of a line — it clutters the output and breaks anything parsing the results downstream. -I just skips those files.

6. When the flags pile up

clai
$ clai find TODO in the project, respecting gitignore→ rg 'TODO'

ripgrep reads .gitignore by default, skips .git, and skips binaries — everything above, with zero flags, and faster. If you're writing your third --exclude-dir chain today, it's time to install it.

Gotchas

  • --exclude-dir without -r silently does nothing. The flag is accepted but never applied, and you'll assume it's broken.
  • The pattern in --exclude-dir isn't a path. --exclude-dir=src/vendor won't match anything, because grep compares the directory name, not the path to it. Write --exclude-dir=vendor.
  • Quote the pattern. grep -r --include=*.js lets the shell expand *.js against the current directory before grep ever sees it, and you get a completely different search.

Related questions

How do I exclude the .git directory itself? --exclude-dir=.git. The leading dot doesn't matter — running -r over a repo without this flag dumps garbage from git's object store.

How do I count matches instead of printing them? grep -rc 'TODO' . prints a per-file count; grep -rho 'TODO' . | wc -l gives the total number of hits.

grep or ripgrep? grep is everywhere and worth knowing. ripgrep is faster and sane by default, but you have to install it, and on someone else's server that's not always an option.

See also

Stop memorizing exclude flags — describe what you're looking for and CliAI writes the grep. Install it in one line.