What happens
verifyCodeSurfaceWithGit refuses any worktree whose path has a symlinked ancestor, not merely a symlinked locator:
src/campaign/worktree/index.ts:554-562
const lexicalRoot = resolve(path)
const canonicalRoot = realpathSync(path)
if (
lstatSync(lexicalRoot).isSymbolicLink() ||
(process.platform !== 'win32' && lexicalRoot !== canonicalRoot)
) {
throw new WorktreeAdapterError(
`CodeSurface worktree locator must not contain a symbolic link: ${path}`,
)
}
resolve() is lexical; realpathSync() resolves the whole prefix. On macOS os.tmpdir() is /var/folders/..., and /var is a symlink to /private/var, so the two always differ and the throw is unconditional for anything under a temp root. /tmp → /private/tmp has the same effect.
Reached from the public verifyCodeSurface (:780), resolveWorktreePath (:790), and finalize (:744).
Measured
In @tangle-network/agent-runtime's suite on macOS (Darwin 25.1.0, full pnpm test), this is the second-largest failure cluster — 8 occurrences:
8 Error: CodeSurface worktree locator must not contain a symbolic link: /var/folders/wk/qcfly3h940s4cfmlpxbyf21w0000gn/…
This package's own worktree suite builds on exactly the failing shape — tests/campaign/worktree.test.ts:37:
repoRoot = mkdtempSync(join(tmpdir(), 'wt-repo-'))
No realpathSync wrapper, so on macOS every case in that file takes the throw. tests/sandbox-harness.test.ts:171,186,187 does wrap mkdtempSync in realpathSync, so the pattern is known here — it just was not applied.
The part that makes this hard to notice
Two tests assert this exact message and therefore pass for the wrong reason on macOS — they cannot distinguish a real detection from the tmpdir false positive:
tests/campaign/worktree.test.ts:351 — "rejects a symbolic worktree locator"
tests/campaign/worktree.test.ts:366 — "rejects a worktree locator that traverses a symbolic-link parent"
The second one names the behavior as intended, which is why I have not sent a patch: rejecting a symlinked parent looks deliberate, and guessing at the intent of a security check is worse than leaving it. This issue is the question, not a fix.
The shape a fix could take
lstatSync(lexicalRoot).isSymbolicLink() already expresses "the locator itself is not a link". The very next check in the same function compares canonical to canonical and is unaffected:
const repoRoot = gitText(git, ['rev-parse', '--show-toplevel'], path)
const canonicalRepoRoot = realpathSync(repoRoot)
if (canonicalRepoRoot !== canonicalRoot) { … }
So one option is to drop the lexicalRoot !== canonicalRoot operand and carry canonicalRoot forward, which keeps "the locator is not a symlink" and "the locator is the repository root" while letting an OS-supplied symlinked prefix through. If instead the symlinked-parent rejection is a property you want to keep, then the tests need a realpathSync wrapper so they stop passing for the wrong reason, and the constraint should be documented — because as written the worktree adapter cannot be used under os.tmpdir() on macOS at all.
Either way the two rejection tests above should be made to fail on macOS for the right reason before the behavior is settled.
Environment
- macOS Darwin 25.1.0, arm64, Node v24.11.1
- agent-eval
origin/main @ 85068915 (0.170.0)
os.tmpdir() → /var/folders/wk/…/T; realpathSync → /private/var/folders/wk/…/T
What happens
verifyCodeSurfaceWithGitrefuses any worktree whose path has a symlinked ancestor, not merely a symlinked locator:src/campaign/worktree/index.ts:554-562resolve()is lexical;realpathSync()resolves the whole prefix. On macOSos.tmpdir()is/var/folders/..., and/varis a symlink to/private/var, so the two always differ and the throw is unconditional for anything under a temp root./tmp→/private/tmphas the same effect.Reached from the public
verifyCodeSurface(:780),resolveWorktreePath(:790), andfinalize(:744).Measured
In
@tangle-network/agent-runtime's suite on macOS (Darwin 25.1.0, fullpnpm test), this is the second-largest failure cluster — 8 occurrences:This package's own worktree suite builds on exactly the failing shape —
tests/campaign/worktree.test.ts:37:No
realpathSyncwrapper, so on macOS every case in that file takes the throw.tests/sandbox-harness.test.ts:171,186,187does wrapmkdtempSyncinrealpathSync, so the pattern is known here — it just was not applied.The part that makes this hard to notice
Two tests assert this exact message and therefore pass for the wrong reason on macOS — they cannot distinguish a real detection from the tmpdir false positive:
tests/campaign/worktree.test.ts:351— "rejects a symbolic worktree locator"tests/campaign/worktree.test.ts:366— "rejects a worktree locator that traverses a symbolic-link parent"The second one names the behavior as intended, which is why I have not sent a patch: rejecting a symlinked parent looks deliberate, and guessing at the intent of a security check is worse than leaving it. This issue is the question, not a fix.
The shape a fix could take
lstatSync(lexicalRoot).isSymbolicLink()already expresses "the locator itself is not a link". The very next check in the same function compares canonical to canonical and is unaffected:So one option is to drop the
lexicalRoot !== canonicalRootoperand and carrycanonicalRootforward, which keeps "the locator is not a symlink" and "the locator is the repository root" while letting an OS-supplied symlinked prefix through. If instead the symlinked-parent rejection is a property you want to keep, then the tests need arealpathSyncwrapper so they stop passing for the wrong reason, and the constraint should be documented — because as written the worktree adapter cannot be used underos.tmpdir()on macOS at all.Either way the two rejection tests above should be made to fail on macOS for the right reason before the behavior is settled.
Environment
origin/main@85068915(0.170.0)os.tmpdir()→/var/folders/wk/…/T;realpathSync→/private/var/folders/wk/…/T