GitHub Copilot `grep_search` returns files outside `includePattern` when they are open in an editor
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
This bug surfaced during deep investigations of other multi-root workspace search problems. Please give these priority; this has cost me a significant amount of time and money to investigate these, and because they are hidden within search, they are affecting probably thousands of users daily and the users never know their models are having trouble finding files.
Ticket below drafted by Claude Opus 5:
## Summary
`grep_search` matches open editor documents against `includePattern` using a path that is not their workspace-relative path. Two consequences follow:
1. A pattern scoping to an entire workspace root also returns open documents from **other** roots.
2. A pattern that resolves to no workspace root is matched against an open document's **absolute** path, so a directory outside every workspace root — for example the roots' own parent directory — can match.
Both are silent. Nothing in the result indicates that a file was included for a reason other than the pattern. The practical effect is that `grep_search` is not reproducible: the same query with the same `includePattern` in the same workspace returns different results depending on which files happen to be open, and the caller cannot see it.
`file_search` is unaffected; verified below.
## Environment
- OS: Windows 11
- VS Code: 1.131.0
- Workspace: Multi-root, two folders
The defect does not depend on workspace folder names — it reproduces identically when the root is named by an absolute path, which involves no name at all.
## Setup
```
C:\projects\alpha\
notes.md contains "widget"
sub\
deep.md contains "widget"
C:\projects\beta\
other.md contains "widget"
```
Both folders opened as a multi-root workspace.
## Reproduction
### Case 1 — a whole-root scope admits open documents from another root
1. Close all editors.
2. `grep_search` with `query: "widget"`, `includePattern: "alpha/**"`.
→ Returns `alpha\notes.md` and `alpha\sub\deep.md`. Correct.
3. Open `C:\projects\beta\other.md` in an editor.
4. Repeat step 2 exactly.
→ **Also returns `beta\other.md`**, which is in a different workspace root than the pattern names.
5. Close the editor and repeat step 2.
→ Correct again.
The difference between steps 2 and 4 is exactly the open file and exactly its match count. Opening and closing a single editor toggles it deterministically.
Substituting `includePattern: "C:/projects/alpha/**"` — the documented absolute-path form — produces the identical leak.
### Case 2 — a non-root pattern is matched against absolute paths
1. Close all editors.
2. `grep_search` with `query: "widget"`, `includePattern: "projects/**"`.
→ Empty. Correct: `projects` is the *parent* of both workspace roots and appears in no workspace-relative path.
3. Open `C:\projects\beta\other.md`.
4. Repeat step 2.
→ **Returns `beta\other.md`.**
`projects` can only match by way of the absolute path `C:\projects\beta\other.md`. This is the minimal proof that open documents are matched against a different path than on-disk files.
### Case 3 — the boundary: a sub-root scope does not leak
With `beta\other.md` open, `includePattern: "alpha/sub/**"` returns only `alpha\sub\deep.md`. No leak.
This locates the mechanism precisely (see Analysis).
### Case 4 — `file_search` control
With `beta\other.md` open, `file_search` with `query: "projects/**"` returns no files. `file_search` does not exhibit either behavior.
## Analysis
The two cases reduce to one rule. When `includePattern` begins with a workspace root — by name or by absolute path — that prefix is stripped and the **remainder** is applied to open documents without being re-anchored to the root it came from:
| `includePattern` | Remainder applied to open documents | Open document in another root matched? |
|---|---|---|
| `alpha/**` | `**` | yes — matches every open document |
| `C:/projects/alpha/**` | `**` | yes |
| `alpha/sub/**` | `sub/**` | no — still constrains |
| `C:/projects/alpha/sub/**` | `sub/**` | no |
A whole-root scope collapses the remainder to `**`, which matches everything. A sub-root scope leaves a remainder that still discriminates, which is why Case 3 does not leak.
When the pattern does *not* begin with a workspace root, it is instead tested against the open document's absolute path — which is what makes `projects/**` match in Case 2, and what would make any ancestor directory name match.
Stated once: **open documents are matched using a path that is not their workspace-relative path**, while every other file is matched correctly.
## Impact
`includePattern` exists to scope a search. This defect means it cannot be relied upon to do so, and the failure is invisible at the call site.
For an agent, the consequence is a false positive that reads as a legitimate result. A search scoped to one module returns a file from another, and the agent has no signal distinguishing it from a genuine in-scope match — so a conclusion such as "these are all the occurrences within this component" is silently wrong. Because the contaminating factor is editor state, the same search is not reproducible across invocations, and a result verified once may differ minutes later for reasons unrelated to the code.
This compounds with the reporting problems already tracked in #328075 and #328102: a result set that is silently wider than requested is indistinguishable from a correct one, exactly as a truncated or unmatchable result is indistinguishable from a complete or empty one.
## Recommendation
1. **Match open documents by the same workspace-relative path used for on-disk files.** This is the whole fix; both cases follow from it.
2. **When a workspace-root prefix is stripped, re-anchor the remainder to that root** before applying it to open documents, so that `/**` does not degrade into a global `**`.
3. **Do not test a relative pattern against an absolute path.** A pattern should be matched against an absolute path only when the pattern itself is absolute.
4. **Add parity tests.** Two axes, both currently unguarded:
- *Editor-state parity:* the same query and pattern must return identical results with and without an arbitrary file open in an editor. Cover a whole-root scope, a sub-root scope, an ancestor-directory pattern, and a pattern matching no root.
- *Cross-tool parity:* `file_search` and `grep_search` must agree on the same pattern. They currently disagree on an ancestor-directory pattern with a file open.
## Not tested
Whether the behavior depends on the document being *dirty* versus merely open, and whether a document briefly retained by the extension host after its tab is closed continues to match. All observations above were made by opening and closing editor tabs.
## Related issues
- #293428 — `grep_search` `includePattern` silently failed for workspace-root-name prefixes (fixed in 1.112.0). Its own first recommended regression test was tool parity between `file_search` and `grep_search`; that parity is still not held, on a different axis.
- #328102 — remaining backslash-boundary defect in the same `includePattern` prefix-resolution path.
- #293430 — `\**\` globs silently degrade to single-segment wildcards.
- #328075 — truncation is not reported explicitly enough, producing false "file does not exist" conclusions.
The common thread across all five: a result set that is wider, narrower, or shorter than the caller asked for is returned in a form indistinguishable from a correct one.
Contributor guide
Assessment
This issue has not been assessed yet.