ffgrep: path constraint targeting an unindexed file silently widens to a repo-wide fuzzy search
Summary
ffgrep with a path targeting a file outside the index (e.g. anything under node_modules/, which is gitignore/exclusion-respected) returns zero exact matches, then silently re-runs the search repo-wide with the path constraint dropped, presenting unrelated files as if they were related to the pinned path.
Environment
- OS: Windows 11
- pi: 0.84.3
- @ff-labs/pi-fff: 0.10.5 (tools-and-ui mode)
- CWD: Node project with
node_modules/(react installed);node_modules/react/package.jsonexists on disk and actually containsjsx-runtimein itsexports.
Reproduction
All commands run from the repo root:
ffgrep { pattern: "jsx-runtime", path: "node_modules/react/package.json" }->[0 exact matches. Maybe you meant this?]then matches frompackage-lock.jsonandtests/— never from the pinned file (or anything undernode_modules).ffgrep { pattern: "version", path: "node_modules/react/package.json" }-> first result ise2e/file-link-repro.spec.ts:50: version: 3,— completely unrelated toreact.ffgrep { pattern: "sideEffects", path: "node_modules/react/package.json" }-> returnssrc/renderer/**files whose top hit is auseEffectline — fuzzy noise with zero relationship to either the query or the pinned path. (Because results are frecency-ranked, theApp.tsxfamily shows up first for generic words.)
Control cases for contrast:
path: "node_modules/react/"(directory constraint, no trailing extension) +jsx-runtime-> cleanNo matches found(no leak — directory constraints keep the constrained query).path: "this-dir-does-not-exist-xyz/"-> cleanNo matches found.path: "package-lock.json"(an indexed file) -> works correctly.
Root cause
In src/index.ts (the grep tool's execute):
- Workspace-relative paths never reach an aux finder:
routePathConstraint(aux-finders.ts) returnsnullfor paths without a../prefix, so the query runs against the workspace finder, whose index excludesnode_modules. Exact grep => 0 items. - On 0 items,
pathTargetsFileis computed from the trailing extension, and when true the fuzzy fallback usesfuzzyQuery = patternwithout the path constraint (the "file may just be misnamed" heuristic):
const lastSeg = params.path?.split(/[\/]/).pop() ?? "";
const pathTargetsFile = /\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSeg);
const fuzzyQuery = pathTargetsFile ? pattern : query;
const fuzzy = picker.grep(fuzzyQuery, { mode: "fuzzy", ... });The heuristic never checks whether the pinned file is actually reachable in the picker/index. For any unindexed file path — and node_modules/... is always unindexed — every grep is guaranteed to hit this fallback and return repo-wide noise.
Why it's dangerous
- An agent pinning
path: "node_modules/<pkg>/package.json"to check a dependency's contents receives matches from arbitrary source files, plausibly presented as if they concerned that file. - Short, common patterns (
version,sideEffects, ...) amplify the problem: the fuzzy results are barely related to the pattern at all.
Expected behavior
- Don't broaden when the target isn't in the index. If
pathTargetsFileand the pinned path is not part of the picker's index (gitignored/excluded), return a clear message such aspath not indexed (gitignored or excluded): no matches— or, better, read/match that single file on disk so the query actually answers the question. - At minimum, state that the constraint was dropped. When the fallback does broaden repo-wide, the output must say:
path constraint ignored (target not indexed); results are repo-wide. - The fuzzy fallback for common/generic patterns degrades into pure noise (
sideEffects->useEffectfiles). Consider suppressing or clearly labeling fuzzy results.
Notes
- Directory constraints (no trailing extension) keep the constrained query and are safe — verified
No matches found, no leak. - Aux finders currently can't rescue this case because
routePathConstraintrefuses workspace-relative paths even when they point outside the indexed set (e.g.node_modules/react/...).
Source: dmtrKovalenko/fff