You ask Claude Code to find every TSX file in your project with Glob, and it returns nothing. You drop into your terminal, run find . -name '*.tsx', and several hundred results scroll past. The first time I saw this, I rubbed my eyes for a full minute. The files are there. The pattern looks right. Only Claude Code disagrees.
Once Claude Code became part of my daily workflow as an indie developer juggling several repositories at once, I ran into this "invisible file" problem more times than I'd like to admit — usually right after switching into a monorepo, or a project where build output sits next to source. In nearly every case the root cause is one of four things: .gitignore exclusion, dotfile exclusion, a subtle pattern mismatch, or a workspace boundary the model can't cross. What follows is the triage flow I now run automatically whenever Glob or Grep claims a directory is empty.
The most common culprit: .gitignore
In my experience, seven or eight times out of ten the answer is .gitignore. Claude Code's Glob is built on top of ripgrep, and ripgrep respects your gitignore rules by default. That means node_modules/, dist/, .next/, build/, and any folder you've excluded from version control simply does not exist as far as Glob is concerned.
You'll hit this most often when:
- You want to read a specific file inside
node_modules/for a dependency - You're inspecting a
dist/orbuild/artifact - You're peeking at
.next/or.turbo/cache contents - You're looking for a
.env.localor other dotfile at the project root - You're after a workflow definition under
.github/workflows/
That last case is two problems at once. .github/ is technically tracked by git, but it starts with a dot — and ripgrep, by default, ignores hidden directories. The same holds for .vscode/, .husky/, and similar tooling folders.
How to confirm
From your terminal:
# Confirm the files actually exist
find . -type f -name '*.tsx' | head -20
# Same check, skipping node_modules manually
find . -type f -name '*.tsx' -not -path '*/node_modules/*' | head -20If find produces results that Glob does not, you're almost certainly looking at gitignore or dotfile exclusion.
The fix
The cleanest approach is to ask Claude Code to use Bash with explicit ripgrep flags. I include this instruction in my opening prompt now:
# Force ripgrep to include hidden + ignored files
rg --hidden --no-ignore --files | grep -E '\.(tsx?|jsx?)$'
# Search for a pattern across everything
rg --hidden --no-ignore -l 'useEffect' src/There is no flag on the Glob tool itself to override gitignore. When I genuinely need to search ignored content, I bypass Glob entirely and shell out.
Why Grep misses lines you can clearly see
The Grep tool is also ripgrep underneath. Three subtleties trip people up.
Literal braces
If you're hunting Go's interface{} or a TypeScript empty object, the braces are ripgrep metacharacters. They need escaping.
# Wrong — unintended matches
rg 'interface{}' .
# Right — escape the braces
rg 'interface\{\}' .I lost half an hour late last year searching a Go codebase for chan struct{} before remembering this. Bash's grep doesn't require the escape; ripgrep does. Small detail, real time sink.
Multiline mode is off by default
Claude Code's Grep tool defaults to single-line matching. A pattern that spans from struct { down to a field several lines below will return zero hits unless you explicitly pass multiline: true. The moment you realize you're trying to match across line breaks, ask Claude to set the multiline flag — it is a one-argument fix that saves a long debugging detour.
File-type filters are narrower than you think
type: 'js' matches .js files only. It does not include .jsx, .ts, or .tsx. Use a brace-expanded glob instead.
# Cover both JavaScript and TypeScript variants
rg 'pattern' --glob '*.{js,jsx,ts,tsx}'
# type filter is per-extension-group, not per-language
rg 'pattern' --type ts # .ts only — .tsx is a different groupThe same kind of pattern-syntax pitfall shows up in Claude Code's Edit tool when old_string isn't found, and the underlying mental model is the same: small differences in how a tool interprets your input change the result drastically.
Pattern syntax: the small, sharp edges
A few Glob patterns I see misused regularly:
**/*.tsx— recursive, matches any depth. Correct**/*.{ts,tsx}— brace expansion, also workssrc/**— captures everything undersrc, but folders themselves are includedsrc/**/*— files only, usually what you want!**/test/**— negation. Claude Code'sGlobdoes not currently support negation patterns directly. UseBashwith ripgrep filters when you need to exclude
I once typed *.tsx when I meant **/*.tsx and panicked because "no components exist anywhere." The leading **/ is what makes the pattern recursive. Without it, you're matching only files at the current directory level.
When the file is outside the workspace
Claude Code, by default, only sees files under the directory where the session started. If you want it to look at a parent directory or a sibling repository, one of three things needs to happen:
- Run
/add-dir <path>to grant access to additional roots - Pass an absolute path to
Readdirectly - Drop into
Bashand runfind ~/another-project -name '*.ts'from outside the boundary
When I'm doing a refactor that crosses two or three repositories, I always run /add-dir for each at the start of the session. Skipping this step and asking Claude to "look at the implementation in the other repo" is a reliable way to get a confident "0 results" back.
Read versus Glob asymmetry
Read is more forgiving than Glob. If you give it an absolute path, it can usually open the file even if the path isn't in your allowed roots. Glob and Grep are stricter — they only search inside permitted directories. So you can read a file you cannot find. If you need to discover files first, run /add-dir before searching.
Claude Code's Read tool silently truncating large files at 2,000 lines covers a different Read-side surprise. Reading the two together gives you a fuller picture of how the file-touching tools behave.
When nothing else explains it
If gitignore, hidden files, patterns, and workspace boundaries are all ruled out, I move on to filesystem-level causes. The order I work through:
# 1. Case sensitivity (macOS often is case-insensitive, Linux isn't)
ls -la src/Components 2>&1
ls -la src/components 2>&1
# 2. Broken symlinks
find . -type l ! -exec test -e {} \; -print
# 3. Invisible characters in filenames (NBSP, zero-width space)
ls src/ | cat -A
# 4. Verify the file exists at all
stat src/components/Button.tsxBroken symlinks are silently skipped by Glob. I once spent an evening confused by a pnpm workspace that had stale symlinks left over from a previous package layout. Removing the orphaned links and re-running pnpm install was the fix.
A diagnostic order I run mentally
When a search comes back empty, I now run a fixed sequence in my head before getting frustrated. It takes about thirty seconds end-to-end and saves me from re-asking Claude in increasingly desperate tones.
First, I ask whether the path I'm searching is gitignored. If the answer is "probably yes, that's a build output," I switch to Bash and rg --hidden --no-ignore immediately. Second, I check whether the file lives under a dotfile directory like .github/ or .vscode/. Third, I look at the pattern itself for anything ripgrep would treat as a metacharacter. Fourth, I ask whether the file might be outside the current workspace boundary. Only after all four of those return a clean "no" do I move on to the filesystem-level checks below.
Almost every dead end I have hit fits one of those first four buckets. The filesystem causes — broken symlinks, case sensitivity, invisible characters — are real but rare in comparison.
A small habit that prevents most of this
The change that helped me most wasn't technical — it was prompting. I now include three explicit hints at the start of any session that involves searching:
- "If you need to include gitignored or hidden files, please use
Bashwithrg --hidden --no-ignore" - "If a file might live outside the current workspace, suggest
/add-dirfirst" - "If
Globreturns zero results, fall back tofindinBashbefore giving up"
That short paragraph at the top of a prompt has eliminated the majority of "I can't find that file" detours from my sessions. Telling the model where the boundaries are turns out to be cheaper than letting it hit them.
Common Bash tool execution errors in Claude Code reinforces a related habit: deciding when to reach for Bash versus a specialized tool is itself a debugging skill. The faster you make that call, the less time the session loses.
If you're staring at a zero-result Glob right now, my first move would be: drop into Bash, run rg --hidden --no-ignore --files | grep <your-pattern>, and see whether the file shows up. That single command resolves the case more often than not.