Repository navigation
Description
Security review of the worktree-isolation library layer merged in #12 (commit 1a9eb11). Found by an adversarial security pass (Codex + a second reviewer) against the threat model "a malicious or compromised Codex run, or a crafted repository, escapes the worktree sandbox / damages the host." The most serious findings were reproduced empirically, not theorized.
Context for severity: createWorktreeSession / cleanupWorktreeSession currently have no production callers (the library layer landed in #12; companion --worktree wiring is the next PR). So these are code-path severity once wired, not a live exploitable surface today. They must be closed before the --worktree flag reaches the CLI.
Findings
1. CRITICAL — .worktrees/ symlink traversal redirects worktree creation to an arbitrary host location
git.mjs createWorktree does fs.mkdirSync(path.join(repoRoot, ".worktrees"), { recursive: true }). Node's recursive mkdir follows a symlink at the final component. A crafted repo (or a co-resident user, or a prior malicious run that did rm -rf .worktrees && ln -s ~/.config .worktrees) causes the subsequent git worktree add to populate ~/.config/codex-<ts>/ — i.e. the Codex workspace-write root lands in ~/.ssh, ~/.config, ~/Library/LaunchAgents, etc. Reproduced end-to-end (.worktrees → /tmp/attacker-dest created /tmp/attacker-dest/codex-FAKE/; also the ~/.ssh variant).
2. HIGH — symlink-mode blob in the patch replays a host-file symlink into repoRoot
applyWorktreePatch captures the worktree diff with git diff --cached --binary --output and replays it with git apply --index in repoRoot. A symlink created in the worktree (ln -s ~/.ssh/authorized_keys ./steal) is captured as a mode-120000 entry with the target path verbatim, and git apply recreates it in repoRoot pointing at the host file. Subsequent cat/git show/editor-open exfiltrates; >> appends. The byte-preserving --output transfer does not help — the symlink target is legitimately part of the diff. Reproduced.
3. MEDIUM — git rev-parse --git-dir outside repoRoot → info/exclude append at an attacker-chosen path
createWorktree resolves git rev-parse --git-dir with path.resolve(repoRoot, rawGitDir). When --git-dir returns an absolute path (separate-git-dir, linked worktree, .git gitfile), path.resolve discards repoRoot and the subsequent appendFileSync(<gitDir>/info/exclude, ".worktrees/\n") writes to that absolute location. Content is benign (.worktrees/), but the missing boundary check is a structural gap. Partially reproduced (legit linked worktree); destructive chain needs a pre-staged valid git dir.
4. MEDIUM — unbounded worktree accumulation (disk DoS)
By design (leave-branch, #12) cleanupWorktreeSession never removes the worktree or branch. Every rescue call adds one .worktrees/codex-<ts>/ checkout + branch, forever. No cap, no roster, no warning. A rescue loop or long session fills disk (GB-per-call on a large repo).
Ruled out (checked, not exploitable)
- Command injection — all git calls go through
runCommand(..., {shell:false})arg-array. baseCommit/branch— plugin-generated (rev-parse HEADSHA;codex/<timestamp>).git applypath traversal via../in diff paths — git rejects even with--unsafe-paths(not passed).
Proposed fixes (for the security PR)
lstat-guard.worktrees/before mkdirSync (and reject symlinked components up the path) — closes Add model and effort flags to review commands #1. Single-line core.- Reject mode-120000 (symlink) entries in the captured patch before
git apply— closes Supersedes #408: thread model + effort overrides through app-server spawn path (fixes #476) #2. - After
path.resolve(gitDir), assertgitDiris contained in repoRoot (realpath-normalized), bail otherwise — closes Rebase fork on upstream #471 (RPC override) + cherry-pick shutdown hardening + #506/#480/#490 #3. - At session creation, list
.worktrees/codex-*; warn loudly at N (e.g. 10) or fail-closed — closes Rebase fork on upstream #471 (RPC override) + #506/#480/#490 + shutdown hardening #4. Additive, preserves the leave-branch contract.
Refs #12, fork#10, openai#135, openai#137.
Activity
axisrow commentedon Jul 19, 2026
Split into individual issues for tracking:
All four are code-path-severity-once-wired (createWorktreeSession has no production callers today; #12 landed the library layer only). Closing this umbrella in favor of the per-finding issues.