When you type @ in Claude Code, a file picker pops up so you can point Claude at a
file. It's one of the most-used features — and its defaults quietly get in your way.
The picker follows your .gitignore, so anything git ignores is invisible; it's
scoped to the current project; and its matching is stricter than you'd like. This
post is about why those defaults chafe, and how I reshaped the picker into something
that finds what I actually want.
The problem: the default picker hides too much
The @ picker is helpful right up until it isn't. Three things trip me up:
- Gitignored files are invisible. By default the picker respects
.gitignore. That sounds tidy, but the files git ignores are often exactly the ones I want to show Claude: a local.env, a generated config, a build artifact I'm debugging. If git skips it, I can't@-mention it — I'm stuck pasting the path by hand. - You can't reach outside the project. The picker lists files under the current project, full stop. A file one directory up, or in a sibling repo, doesn't show.
- The matching is picky. I remember a fragment of a filename, not its exact
spelling or casing. If I type
dfthoping to land onDemoFileTest.txt, a strict matcher comes up empty. I want fuzzy — the letters in order, case be damned.
Two of these come down to one setting; the third needs a small script. Claude Code gives you both levers.
Lever one: stop hiding gitignored files
The first fix is a single line in settings.json:
{
"respectGitignore": false
}
With that off, the picker stops filtering by .gitignore, and those local .env
files and generated configs show up again. Problem solved — but it creates a new
one. Git was also hiding the noise: node_modules, dist, target, .next.
Turn respectGitignore off and all of that floods back into the picker. So the
setting alone isn't enough; you need a way to hide the noise yourself. That's the
second lever.
Lever two: a custom suggestion command
Claude Code lets you replace the picker's file list with your own script. In
settings.json:
{
"fileSuggestion": {
"type": "command",
"command": "~/.claude/file-suggestion.sh"
}
}
Now, instead of its built-in logic, Claude Code runs your script every time you type
after @. The contract is simple:
- It gets a JSON blob on stdin —
{"query": "whatever you typed"}. - The project root arrives in the
CLAUDE_PROJECT_DIRenvironment variable. - Your script prints newline-separated relative paths; Claude Code shows the top ~15.
That's the whole interface. Once you own the list, you control everything: which folders to hide, how matching works, how results are ranked, whether directories show up. Here's how I use it.
Hiding folders, everywhere
The first job is to undo the noise that respectGitignore: false let back in. My
script lists files with find, pruning the folders I never want suggested — in any
project:
FIND=/usr/bin/find
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
cd "$ROOT" 2>/dev/null || exit 0
files="$("$FIND" . \
\( -path '*/.git' \
-o -path '*/node_modules' \
-o -path '*/target' \
-o -path '*/dist' \
-o -path '*/build' \
-o -path '*/.next' \
-o -path '*/.venv' \
-o -path '*/vendor' \) -prune \
-o \( -type f -o -type d \) -print 2>/dev/null \
| sed 's|^\./||')"
This is a global ignore list that lives in one place, independent of any project's
.gitignore. Want to hide another folder everywhere? Add one -o -path '*/NAME' -prune line. It also lists directories, not just files, so I can @-mention a
whole folder when that's what I mean.
Fuzzy, case-insensitive matching
The other job is finding the file from a scrap of a name. My script does a
subsequence match: the characters you typed have to appear in order, but not
next to each other — and everything is lowercased first, so casing never matters.
That's what lets df match both demoFile.txt and DemoFileTest.txt, and dft
match DemoFileTest.txt.
It also ranks the results so the best guess is first:
- Rank 0 — your query is a straight substring of the filename (
demo→demoFile.txt). - Rank 1 — your query is a subsequence of the filename (
dft→DemoFileTest.txt). - Rank 2 — your query only matches somewhere in the full path (via a parent folder name).
Within a rank, shorter filenames come first, then alphabetical. The result is that the file I'm picturing is usually the top hit, even when I only half-remember its name.
# No query yet: return a small slice so the picker isn't empty.
if [ -z "$query" ]; then
printf '%s\n' "$files" | head -n 20
exit 0
fi
# Otherwise: lowercase the query and rank by substring > subsequence-of-name >
# subsequence-of-path, shortest name first. (Full matching logic in the script.)
What about files outside the project?
This is the one limitation the levers don't fully erase. The picker — and my script
— are rooted at CLAUDE_PROJECT_DIR, so a file in a sibling repo still won't appear.
The thing to know is that you now own the list: since your script decides which
paths to emit, you could point ROOT elsewhere, or add a second search location, if
you genuinely need out-of-tree files. I keep mine project-rooted on purpose — the
files I mention almost always live in the project — but the door is open if your
workflow needs it.
A couple of gotchas
- Claude Code runs the script in a bare, non-interactive shell. Fancy tools like
rgandfzf, or shell wrappers aroundfind, may not be onPATH. I call the real binaries by absolute path (/usr/bin/find,/usr/bin/jq) so the script works regardless. - Keep it fast. It runs on every keystroke after
@, so a slowfindover a huge tree makes the picker feel laggy. Pruning the big folders (above) is what keeps it snappy. - Cap the output. Claude Code only shows ~15, so ending the pipeline with
head -n 20avoids doing extra work you'll never see.
A checklist to steal
- Set
respectGitignore: falseso gitignored files (.env, generated configs) can be mentioned. - Add a
fileSuggestioncommand to take control of the list once git isn't filtering it for you. - Prune noise folders (
node_modules,dist,target, …) in that script, in one place, for every project. - Match fuzzy and case-insensitive so a fragment finds the file.
- Call tools by absolute path, keep the script fast, and cap the results.
The @ picker is small, but you touch it constantly. A setting and a short script
turn it from "shows me what git allows" into "finds the file I'm thinking of" — and
that difference adds up over a day.
Comments
Be the first to comment.