Skip to content

ignore_vcs hides files that live outside the repository #3922

Description

@dwin-gharibi

Description

VCSMatcher.ShouldIgnore decides whether a path is inside the repository with a raw string
prefix test:

// pkg/fsx/vcs.go:223
if !strings.HasPrefix(absPath, m.repoRoot) {
    return false
}

m.repoRoot is absolute, cleaned and symlink-resolved, with no trailing separator
(findRepoRoot, pkg/fsx/vcs.go:121-140). A string prefix therefore has no path-component
boundary, so /work/repo is a prefix of /work/repo-sibling and the sibling is treated as
being inside the repository.

Once a foreign path passes that check, filepath.Rel produces a path that climbs out, and its
components are handed straight to the gitignore matcher:

relPath, err := filepath.Rel(m.repoRoot, absPath)   // "../repo-sibling/app.log"
...
pathComponents := strings.Split(normalizedRelPath, "/")  // ["..", "repo-sibling", "app.log"]
matched := m.matcher.Match(pathComponents, isDir)

gitignore.Match matches basename patterns at any depth, so a pattern as ordinary as *.log
matches the trailing component and the file is reported as ignored — even though it is not in
the repository and no .gitignore governs it.

Expected Behavior

ShouldIgnore returns false for any path outside the repository root. Only paths genuinely
inside the worktree are matched against that worktree's .gitignore patterns.

Actual Behavior

A path in a sibling directory whose name starts with the repository directory's name is
matched against the repository's patterns, and is silently reported as ignored.

Steps to Reproduce

<tmp>/repo/            <- git worktree, .gitignore contains "*.log"
<tmp>/repo/app.log
<tmp>/repo-sibling/app.log   <- NOT in the repository
<tmp>/other/app.log          <- NOT in the repository (control)
m, _ := fsx.NewVCSMatcher(filepath.Join(tmp, "repo"))

m.ShouldIgnore(filepath.Join(tmp, "repo", "app.log"))          // true  — correct
m.ShouldIgnore(filepath.Join(tmp, "other", "app.log"))         // false — correct
m.ShouldIgnore(filepath.Join(tmp, "repo-sibling", "app.log"))  // true  — WRONG

Observed:

repoRoot = ".../001/repo"
ShouldIgnore(inside  .../001/repo/app.log)          = true   (want true)
ShouldIgnore(control .../001/other/app.log)         = false  (want false)
ShouldIgnore(sibling .../001/repo-sibling/app.log)  = true   (want false)   <-- bug

The other/ control returning false is what isolates the cause: it is the shared string
prefix, not over-broad pattern matching in general.

Docker Agent version

No response

OS & terminal

No response

Model used

No response

Error output

Screenshots

No response

Additional context

ShouldIgnore is the VCS-ignore gate for two independent subsystems.

Filesystem toolset — via shouldIgnorePath (pkg/tools/builtin/filesystem/filesystem.go:904):

Tool Callsite
directory_tree filesystem.go:941 (passed into fsx.DirectoryTree)
list_directory filesystem.go:1062
search_files_content filesystem.go:1428

ignore_vcs defaults to true (filesystem.go:192), and the matcher is built once from the
working directory (initGitignoreMatcher, :881). The toolset accepts multiple allow-list
roots (WithAllowList, :150; pathRootSet.entryFor, filesystem_paths.go:164), so an agent
configured with both /work/repo and /work/repo-data has /work/repo's .gitignore
silently applied to /work/repo-data.

RAG indexingBuildShouldIgnore (pkg/rag/strategy/helpers.go:257) builds a matcher from
buildCtx.ParentDir when respect_vcs is on, and is used by the bm25, semantic-embeddings and
chunked-embeddings strategies. Affected files never enter the index.

The failure mode is silent omission, which is what makes it costly: there is no error to
notice. Files disappear from listings and search results, and in search_files_content a
matching directory returns fs.SkipDir, so an entire subtree can vanish. The agent receives
a successful, empty result and concludes the content does not exist — then acts on that.

  • Not a regression from a specific commit; the prefix test dates from the original
    VCSMatcher implementation.
  • Several other containment checks in this codebase already do this correctly and are the
    natural reference: pkg/fsx/collect.go:131, pkg/tools/builtin/skills/skills.go:179,
    pkg/sandbox/kit/kit.go:592, pkg/acp/filesystem.go:157, pkg/path/display.go:67.
  • The fix is to reject based on filepath.Rel's output, which is already computed on the very
    next line — so the redundant prefix scan can simply be dropped rather than patched.

Metadata

Metadata

Assignees

Labels

area/ragFor work/issues that have to do with the RAG featuresarea/toolsFor features/issues/fixes related to the usage of built-in and MCP tools

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions