Skip to content

fix: follow symlinks when discovering project directories#30

Merged
wesm merged 3 commits intowesm:mainfrom
clkao:fix/symlink-discovery
Feb 25, 2026
Merged

fix: follow symlinks when discovering project directories#30
wesm merged 3 commits intowesm:mainfrom
clkao:fix/symlink-discovery

Conversation

@clkao
Copy link
Copy Markdown
Contributor

@clkao clkao commented Feb 24, 2026

Symlinked project directories under CLAUDE_PROJECTS_DIR were silently skipped, making it impossible to scan a subset of projects by symlinking selected project directories into a custom directory.

Summary

  • os.ReadDir returns DirEntry where IsDir() is false for symlinks to directories, causing DiscoverClaudeProjects and Gemini discovery to silently skip symlinked project directories
  • Adds isDirOrSymlink() helper that also checks ModeSymlink, applied to all three affected call sites (Claude projects, Gemini session discovery, Gemini source file lookup)

Test plan

  • Verified with /tmp/filtered-projects/ containing a symlink to one project: before fix discovers 0 claude files, after fix discovers 4
  • All existing discovery tests pass
  • Full test suite passes

🤖 Generated with Claude Code

@roborev-ci
Copy link
Copy Markdown

roborev-ci bot commented Feb 24, 2026

roborev: Combined Review (528b1f2)

Summary verdict: The commit successfully introduces symlink support for project discovery but misses one function update, causing a regression risk for Claude projects.

Medium

Incomplete Symlink Support in FindClaudeSourceFile

  • Location: internal/sync/discovery.go (lines 122-130)
  • Description: The commit updates DiscoverClaudeProjects, DiscoverGeminiSessions, and FindGeminiSource File to use the newly introduced isDirOrSymlink(entry) helper function. However, FindClaudeSourceFile still strictly enforces !entry.IsDir(). As a result, sessions discovered via symlinked Claude project directories may fail later in single-session re-sync/source resolution because the
    original source file cannot be located.
  • Suggested Fix: Update the FindClaudeSourceFile function to use the isDirOrSymlink helper for consistency and correct behavior:
- if !entry.IsDir() {
+ if !isDirOrSymlink(entry) {

      continue
  }

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

clkao and others added 2 commits February 24, 2026 17:57
os.ReadDir returns DirEntry where IsDir() is false for symlinks to
directories. This caused DiscoverClaudeProjects and the Gemini
discovery functions to silently skip symlinked project directories,
making CLAUDE_PROJECTS_DIR unusable with symlinked repos.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ile parity

isDirOrSymlink now stat-resolves symlink targets to confirm they
point to directories, rejecting symlinks to files and broken links.

FindClaudeSourceFile updated to use isDirOrSymlink instead of
IsDir, matching DiscoverClaudeProjects behavior for symlinked
project directories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wesm wesm force-pushed the fix/symlink-discovery branch from 528b1f2 to 0d6c74f Compare February 25, 2026 00:03
@roborev-ci
Copy link
Copy Markdown

roborev-ci bot commented Feb 25, 2026

roborev: Combined Review (0d6c74f)

Summary Verdict: The PR successfully introduces symlink resolution
for directory discovery, but contains medium-severity security vulnerabilities and test validity issues that must be addressed.

Medium

1. Symlink traversal in world-writable temp roots can broaden trust boundary

  • Files: [internal/sync/discovery.go:23](/home/roborev/.
    roborev/clones/wesm/agentsview/internal/sync/discovery.go:23), [internal/sync/discovery.go:289](/home/roborev/.roborev/clones/wesm/agentsview/internal/sync/discovery.go
    :289), internal/sync/discovery.go:344
  • Description: isDirOrSymlink now accepts
    symlinks to directories, and Gemini discovery/source lookup applies this to tmpDir entries. If tmpDir is a shared location like /tmp, another local user can plant a hash-named symlink to an arbitrary directory and influence what files are discovered/read. This is a classic local symlink
    traversal/trust-boundary issue.
  • Suggested Remediation: For tmpDir scanning, either reject symlinks entirely or resolve symlinks and enforce that the canonical target remains under the canonical tmpDir (and ideally is owned by the current UID). Apply stricter checks specifically to Gemini paths in shared temp
    locations.

2. TestFindClaudeSourceFile_Symlink does not actually prove symlink behavior

  • Files: [internal/sync/sync_test.go:780](/home/roborev/.roborev/clones/wesm/agentsview/
    internal/sync/sync_test.go:780)
  • Description: The test creates real-project directly under dir and writes sess-abc.jsonl there, then also creates linked-project -> real-project. FindClaudeSourceFile(dir, "sess-abc") can succeed via real-project even if symlink handling is broken, so this can pass falsely.
  • Suggested Remediation: Put the real target directory outside dir (e.g., another t.TempDir()), expose it only via a symlink
    under dir, and assert the returned path is through the symlinked project path.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

… test

The previous test had the real directory inside the search root,
so FindClaudeSourceFile could find the session without traversing
the symlink. Now the real directory is in a separate temp dir,
only reachable via the symlink.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci bot commented Feb 25, 2026

roborev: Combined Review (7d6d4d8)

Verdict: Changes are required to address a medium-severity security vulnerability regarding unconstrained symlink traversal.

Medium Severity

  1. Unconstrained Symlink Traversal
    Files: [internal/sync/discovery.go:24](/home/robore
    v/.roborev/clones/wesm/agentsview/internal/sync/discovery.go:24), [internal/sync/discovery.go:55](/home/roborev/.roborev/clones/wesm/agentsview/internal/sync/discovery.go
    :55), internal/sync/discovery.go:138, [internal/sync/discovery.go:289](/home/rob
    orev/.roborev/clones/wesm/agentsview/internal/sync/discovery.go:289), [internal/sync/discovery.go:344](/home/roborev/.roborev/clones/wesm/agentsview/internal/sync/
    discovery.go:344)
    Issue: isDirOrSymlink now follows symlinks to directories, and all discovery/find paths accept those symlinked directories without constraining resolved targets to an allowed root. This allows session discovery/lookup to traverse outside intended roots (explicitly validated by the
    new test), which can cause unintended file reads from arbitrary locations reachable via symlink.
    Remediation: Resolve symlinks with filepath.EvalSymlinks and enforce a canonical-root check (e.g., resolved path must be under the allowed base). If out-of-root traversal is
    required for compatibility, gate it behind explicit opt-in and enforce ownership/permission checks (especially for temp-directory scanning).
    Open Question: Is symlink traversal outside the configured search root a strict requirement, or can it be limited to in-root targets only? This decision determines whether this finding is treated as a
    bug or an intentional trust tradeoff.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

@wesm
Copy link
Copy Markdown
Owner

wesm commented Feb 25, 2026

pedantic. merging

@wesm wesm merged commit fb4daee into wesm:main Feb 25, 2026
6 checks passed
@thellert
Copy link
Copy Markdown
Contributor

Great timing on this: we ran into this exact issue today while integrating agentsview into our project and were planning to open a PR for it.

Thanks for the fix!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants