All articles

Customizing the @ File Picker in Claude Code

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 dft hoping to land on DemoFileTest.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_DIR environment 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 (demodemoFile.txt).
  • Rank 1 — your query is a subsequence of the filename (dftDemoFileTest.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 rg and fzf, or shell wrappers around find, may not be on PATH. 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 slow find over 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 20 avoids doing extra work you'll never see.

A checklist to steal

  • Set respectGitignore: false so gitignored files (.env, generated configs) can be mentioned.
  • Add a fileSuggestion command 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.