Skip to content

scanner: nested git worktrees inside the repo are scanned as project files (5.5x file-count inflation) #131

Description

@JordanCoin

Found while running agent fleets in this repo. Claude Code creates isolated worktrees at .claude/worktrees/agent-<id>/, each a full checkout of the repository. codemap walks straight into them and counts every file as part of the project.

Impact, measured

With 18 agent worktrees present, then removing them:

$ codemap .                     # 18 worktrees present
│ Files: 3943 | Size: 5.7MB

$ git worktree remove ...        # removed 18 finished worktrees
$ codemap .
│ Files: 718 | Size: ...

5.5x inflation. This repo has roughly 150 source files.

Everything derived from the walk is affected:

  • codemap . file count and the session-start hook that every agent sees first
  • codemap context — reported file_count: 3223, and the working_set filled with worktree paths
  • codemap handoff — the entire "Delta (Recent Work)" section was 18 .claude/worktrees/agent-*/ (untracked) lines, pushing real changed files out of the artifact
  • hub detection and importer counts, since the same file is indexed once per worktree copy

The handoff case is the worst: the artifact whose whole job is telling the next agent what was recently worked on was reporting worktree directories instead.

Root cause

scanner.IgnoredDirs (scanner/walker.go:113) has ~30 entries — .git, node_modules, vendor, target, .next, .venv, and so on — but nothing covering agent worktrees. .git only skips the directory named .git; a linked worktree's .git is a file containing a gitdir: pointer, and the worktree directory itself has an ordinary name.

.claude/worktrees/ is also not gitignored, so the gitignore path doesn't catch it either:

$ git check-ignore -v .claude/worktrees/
(no match)

The interesting part: the detection already exists, and is ast-grep-only

findNestedGitRepos (scanner/astgrep.go:190) already finds nested repositories and excludes them from the ast-grep scan via --globs !repo/** (astgrep.go:267). It is the only consumer — the file walker never calls it.

So --deps partially avoids the problem while codemap ., context, and handoff do not. That inconsistency is worth fixing on its own.

It would also miss worktrees even if the walker did call it, for three independent reasons:

for _, e := range entries {
    if !e.IsDir() || strings.HasPrefix(e.Name(), ".") { continue }   // (1) skips .claude entirely
    gitPath := filepath.Join(root, e.Name(), ".git")
    if info, err := os.Stat(gitPath); err == nil && info.IsDir() {   // (3) worktree .git is a FILE
        repos = append(repos, e.Name())
    }
}
  1. Dotted directories are skipped, so .claude/ is never examined.
  2. Only top-level entries are read (one os.ReadDir, no recursion), so .claude/worktrees/agent-X is two levels deeper than it looks.
  3. It requires .git to be a directory. A linked worktree's .git is a regular file holding gitdir: /path/to/.git/worktrees/<name>, so info.IsDir() is false.

Confirmed a nested worktree is genuinely a separate work tree:

$ git -C .claude/worktrees/agent-<id> rev-parse --show-toplevel
/Users/…/codemap/.claude/worktrees/agent-<id>

Suggested fix

Teach the walker to skip any directory that is a distinct git work tree, and share one implementation with ast-grep instead of keeping the logic in two places:

  • Treat a .git entry as a boundary marker whether it is a directory or a regular file (the worktree/submodule form).
  • Don't skip dotted directories when looking for boundaries — .claude/worktrees/ is exactly where agent harnesses put them.
  • Detect at any depth, not just top level.

A cheap interim mitigation is adding .claude to IgnoredDirs, but that only covers one harness's convention; the boundary check is the general fix and also handles vendored repos and submodules.

Worth noting this will get more common, not less — agent harnesses creating in-repo worktrees is becoming the default, and codemap is a tool agents run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions