Fixing Claude Code's broken file search
Table of contents
Claude Code has a nice feature where you can type @ followed by a file name to reference files in your conversation. The problem is that it’s broken. At least for me, it doesn’t work well when the .git folder is not in the repo root; like when using git worktrees. There’s an open issue about it.
After some frustration, I decided to write my own file suggestion hook.
The Hook System
Claude Code lets you override the file suggestion behavior with a custom command. You configure it in your settings file (.claude/settings.json):
{
"fileSuggestion": {
"type": "command",
"command": "python3 ~/.claude/hooks/hook_file_suggestion.py"
}
}
The hook receives JSON on stdin with the search query:
{ "query": "some_file" }
And it should output file paths to stdout, one per line. Simple enough.
See the settings documentation for more details.
A Minimal Implementation
Here’s a basic version that uses ripgrep to search for files:
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
def main() -> int:
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
return 1
query = input_data.get("query", "")
if not query:
return 0
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
project_dir = os.path.abspath(project_dir)
cmd = f"rg --follow --files | rg -i -F '{query}'"
result = subprocess.run(
cmd,
shell=True,
cwd=project_dir,
capture_output=True,
text=True,
)
for line in result.stdout.splitlines()[:15]:
print(line)
return 0
if __name__ == "__main__":
sys.exit(main())
The script:
- Reads the query from stdin
- Uses
rg --filesto list all files (respects.gitignore) - Uses
--followto include symlinked files - Pipes to another
rgto filter by the query - Outputs the first 15 matches
This already works better than the built-in search for my use case.
Searching the Whole Repository
One issue with the minimal version: if you’re in a subdirectory, it only searches that subdirectory. Sometimes you want to find files elsewhere in the repo.
Here’s an improved version that searches both the current directory and the repository root:
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
def get_git_root(start_dir: str) -> str | None:
"""Get git repository root, or None if not in a git repo."""
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=start_dir,
capture_output=True,
text=True,
)
if result.returncode == 0:
return result.stdout.strip()
return None
def search_dir(cwd: str, query: str, project_dir: str, limit: int) -> list[str]:
"""Search a directory and return relative paths."""
cmd = f"rg --follow --files | rg -i -e '{query}'"
result = subprocess.run(
cmd,
shell=True,
cwd=cwd,
capture_output=True,
text=True,
)
paths = []
for line in result.stdout.splitlines()[:limit]:
if line.strip():
abs_path = os.path.join(cwd, line)
rel_path = os.path.relpath(abs_path, project_dir)
paths.append(rel_path)
return paths
def main() -> int:
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
return 1
query = input_data.get("query", "")
if not query:
return 0
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
project_dir = os.path.abspath(project_dir)
# Search current directory first (higher priority)
cwd_files = search_dir(
cwd=project_dir,
query=query,
project_dir=project_dir,
limit=100,
)
# Search repo root if we're in a git repo and it's different from cwd
repo_files = []
git_root = get_git_root(start_dir=project_dir)
if git_root and git_root != project_dir:
repo_files = search_dir(
cwd=git_root,
query=query,
project_dir=project_dir,
limit=100,
)
# Merge results: cwd files first, then repo files (excluding duplicates)
cwd_set = set(cwd_files)
combined = cwd_files + [f for f in repo_files if f not in cwd_set]
# Sort by path length (shorter paths are usually more relevant)
combined.sort(key=len)
for path in combined[:15]:
print(path)
return 0
if __name__ == "__main__":
sys.exit(main())
The key changes:
get_git_root()finds the repository root usinggit rev-parse --show-toplevel- We search both directories if they’re different
- Results from the current directory take priority
- Duplicates are removed
- Results are sorted by path length (shorter paths first, hacky proxy for relevance)
Why This Works Better
First, it’s not broken. But it also means that now I have complete control over how files are searched and surfaced. This, together with --add-dir (or /add-dir) can be very powerful.
By using ripgrep directly, we get:
- Fast substring matching
- Control over result ordering
- Predictable behavior
You can use fd, pipe to fzf, or whatever tool you prefer. The hook system is flexible.
It’s not perfect. The query is used as a literal substring match, so searching for tst won’t find test. But once you have a basic hook, you can start extending it as needed. Just be mindful of performance, since this runs every time you type a new character after @.
Save the script somewhere (I use ~/.claude/hooks/hook_file_suggestion.py), make it executable, and add the configuration to your settings file.
![rand[om]](/img/bike_m.png)