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.json exists on disk and actually contains jsx-runtime in its exports.
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 from package-lock.json and tests/ — never from the pinned file (or anything under node_modules).
-
ffgrep { pattern: "version", path: "node_modules/react/package.json" }
-> first result is e2e/file-link-repro.spec.ts:50: version: 3, — completely unrelated to react.
-
ffgrep { pattern: "sideEffects", path: "node_modules/react/package.json" }
-> returns src/renderer/** files whose top hit is a useEffect line — fuzzy noise with zero relationship to either the query or the pinned path. (Because results are frecency-ranked, the App.tsx family shows up first for generic words.)
Control cases for contrast:
path: "node_modules/react/" (directory constraint, no trailing extension) + jsx-runtime -> clean No matches found (no leak — directory constraints keep the constrained query).
path: "this-dir-does-not-exist-xyz/" -> clean No 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) returns null for paths without a ../ prefix, so the query runs against the workspace finder, whose index excludes node_modules. Exact grep => 0 items.
- On 0 items,
pathTargetsFile is computed from the trailing extension, and when true the fuzzy fallback uses fuzzyQuery = pattern without 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
pathTargetsFile and the pinned path is not part of the picker's index (gitignored/excluded), return a clear message such as path 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 -> useEffect files). 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
routePathConstraint refuses workspace-relative paths even when they point outside the indexed set (e.g. node_modules/react/...).
Summary
ffgrepwith apathtargeting a file outside the index (e.g. anything undernode_modules/, which is gitignore/exclusion-respected) returns zero exact matches, then silently re-runs the search repo-wide with thepathconstraint dropped, presenting unrelated files as if they were related to the pinned path.Environment
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 is
e2e/file-link-repro.spec.ts:50: version: 3,— completely unrelated toreact.ffgrep { pattern: "sideEffects", path: "node_modules/react/package.json" }-> returns
src/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'sexecute):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.pathTargetsFileis computed from the trailing extension, and when true the fuzzy fallback usesfuzzyQuery = patternwithout the path constraint (the "file may just be misnamed" heuristic):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
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.version,sideEffects, ...) amplify the problem: the fuzzy results are barely related to the pattern at all.Expected behavior
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.path constraint ignored (target not indexed); results are repo-wide.sideEffects->useEffectfiles). Consider suppressing or clearly labeling fuzzy results.Notes
No matches found, no leak.routePathConstraintrefuses workspace-relative paths even when they point outside the indexed set (e.g.node_modules/react/...).