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 indexing — BuildShouldIgnore (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.
Description
VCSMatcher.ShouldIgnoredecides whether a path is inside the repository with a raw stringprefix test:
m.repoRootis absolute, cleaned and symlink-resolved, with no trailing separator(
findRepoRoot,pkg/fsx/vcs.go:121-140). A string prefix therefore has no path-componentboundary, so
/work/repois a prefix of/work/repo-siblingand the sibling is treated asbeing inside the repository.
Once a foreign path passes that check,
filepath.Relproduces a path that climbs out, and itscomponents are handed straight to the gitignore matcher:
gitignore.Matchmatches basename patterns at any depth, so a pattern as ordinary as*.logmatches the trailing component and the file is reported as ignored — even though it is not in
the repository and no
.gitignoregoverns it.Expected Behavior
ShouldIgnorereturnsfalsefor any path outside the repository root. Only paths genuinelyinside the worktree are matched against that worktree's
.gitignorepatterns.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
Observed:
The
other/control returningfalseis what isolates the cause: it is the shared stringprefix, 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
ShouldIgnoreis the VCS-ignore gate for two independent subsystems.Filesystem toolset — via
shouldIgnorePath(pkg/tools/builtin/filesystem/filesystem.go:904):directory_treefilesystem.go:941(passed intofsx.DirectoryTree)list_directoryfilesystem.go:1062search_files_contentfilesystem.go:1428ignore_vcsdefaults to true (filesystem.go:192), and the matcher is built once from theworking directory (
initGitignoreMatcher,:881). The toolset accepts multiple allow-listroots (
WithAllowList,:150;pathRootSet.entryFor,filesystem_paths.go:164), so an agentconfigured with both
/work/repoand/work/repo-datahas/work/repo's.gitignoresilently applied to
/work/repo-data.RAG indexing —
BuildShouldIgnore(pkg/rag/strategy/helpers.go:257) builds a matcher frombuildCtx.ParentDirwhenrespect_vcsis on, and is used by the bm25, semantic-embeddings andchunked-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_contentamatching directory returns
fs.SkipDir, so an entire subtree can vanish. The agent receivesa successful, empty result and concludes the content does not exist — then acts on that.
VCSMatcherimplementation.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.filepath.Rel's output, which is already computed on the verynext line — so the redundant prefix scan can simply be dropped rather than patched.