Worktrees
Git worktrees allow multiple working directories to share a single repository. For AI coding agents, worktrees enable session isolation — each agent session can operate in its own checkout without interfering with the user’s working directory. This page traces how the three reference implementations handle (or don’t handle) worktrees, and designs OpenOxide’s approach.
Feature Definition
Section titled “Feature Definition”A git worktree is a secondary checkout of a repository. Created with git worktree add, it produces a separate directory with its own branch, index, and working tree, but sharing the same object database and refs as the parent. The .git entry in a worktree is a file (not a directory) containing a gitdir: pointer back to the main repository’s .git/worktrees/<name>/ directory.
Worktrees solve two problems for AI agents:
- Session isolation: The agent can make changes on a separate branch without touching the user’s working directory. If the agent’s changes are bad, reset the worktree. The user’s work is untouched.
- Parallel execution: Multiple agents can operate simultaneously in separate worktrees without file-level conflicts.
The challenge is that most git tooling assumes a single working directory. Operations like git status, git diff, and git stash all need to be directed at the correct worktree. Config inheritance, trust boundaries, and process isolation all become more complex with worktrees.
Aider Implementation
Section titled “Aider Implementation”Aider has no explicit worktree support. It works passively through GitPython.
Implicit Tolerance
Section titled “Implicit Tolerance”Aider creates its git.Repo object in aider/repo.py:126:
git.Repo(path, odbt=git.GitDB)GitPython’s Repo constructor follows .git file pointers automatically. When called from within a worktree, repo.working_tree_dir returns the worktree’s directory (not the main repo’s), and repo.git_dir returns the worktree-specific git directory (e.g., ../.git/worktrees/<name>/).
This means Aider can run from inside a worktree. Commits, diffs, and file operations target the worktree’s branch correctly because GitPython delegates to the underlying git commands, which respect the worktree context.
What Is Not Handled
Section titled “What Is Not Handled”- No worktree creation: Aider cannot create worktrees. The user must set them up manually.
- No worktree detection: Aider does not know whether it is running in a worktree or the main checkout. It treats both identically.
- No worktree-specific configuration: No config inheritance or trust delegation.
- No testing: No test coverage for worktree scenarios in the Aider test suite.
Aider’s implicit support is fragile. If GitPython changes how it resolves .git file pointers, or if Aider adds path-based assumptions (e.g., comparing repo.git_dir against known patterns), worktree support could break silently.
Codex Implementation
Section titled “Codex Implementation”Codex has partial but deliberate worktree support: it detects worktrees for trust inheritance and config resolution, but does not create or manage them.
Git Repo Root Detection
Section titled “Git Repo Root Detection”The primary repo detection function get_git_repo_root() (codex-rs/core/src/git_info.rs:28) walks up the directory tree looking for a .git file or directory. The function’s docstring explicitly acknowledges the limitation:
/// Note that this does **not** detect *work-trees* created with/// `git worktree add` where the checkout lives outside the main repository/// directory. If you need Codex to work from such a checkout simply pass the/// `--allow-no-git-exec` CLI flag that disables the repo requirement.The function finds .git entries but does not follow gitdir: pointers in .git files. For worktrees inside the main repo’s directory tree, this works fine (the walk will find the parent’s .git directory). For worktrees outside the main repo (e.g., git worktree add /tmp/my-worktree), the function fails to find a repo and requires --allow-no-git-exec as a workaround.
Trust Resolution via --git-common-dir
Section titled “Trust Resolution via --git-common-dir”A separate function, resolve_root_git_project_for_trust() (git_info.rs:610-636), handles worktrees correctly:
pub fn resolve_root_git_project_for_trust(cwd: &Path) -> Option<PathBuf> { let git_dir_out = std::process::Command::new("git") .args(["rev-parse", "--git-common-dir"]) .current_dir(base) .output() .ok()?; // ... let git_dir_path = std::fs::canonicalize(&git_dir_path_raw) .unwrap_or(git_dir_path_raw); git_dir_path.parent().map(Path::to_path_buf)}git rev-parse --git-common-dir returns the shared .git directory regardless of whether the current directory is in a worktree or the main checkout. This function then takes the parent of that directory to get the project root. Path canonicalization handles macOS /var vs /private/var symlink ambiguity.
This is used in config/mod.rs:465-474 to resolve project-level config, allowing worktrees to inherit trust and configuration from their parent repository.
A unit test (git_info.rs:1124-1151) named resolve_root_git_project_for_trust_detects_worktree_and_returns_main_root confirms this behavior: it creates a worktree via git worktree add, then verifies that paths within the worktree resolve to the main repo root.
Dangerous Command Filtering
Section titled “Dangerous Command Filtering”Codex blocks --work-tree and --work-tree= as dangerous git flags (codex-rs/shell-command/src/command_safety/is_dangerous_command.rs:39,51). This prevents the agent from overriding the working tree via shell commands, which could be used to escape the sandbox or modify files outside the intended scope.
Diff Tracking Cache
Section titled “Diff Tracking Cache”The TurnDiffTracker (codex-rs/core/src/turn_diff_tracker.rs:41,141-143) caches git worktree roots to avoid repeated filesystem walks during a turn. find_git_root_cached() stores resolved roots and reuses them for subsequent file lookups.
Windows Sandbox Worktree Handling
Section titled “Windows Sandbox Worktree Handling”The Windows sandbox implementation (codex-rs/windows-sandbox-rs/src/elevated_impl.rs:52-81) has explicit .git file parsing:
fn find_git_root(start: &Path) -> Option<PathBuf> { // ... if metadata.is_file() { // .git is a file → this is a worktree let content = std::fs::read_to_string(&git_path).ok()?; if let Some(gitdir) = content.strip_prefix("gitdir: ") { let resolved = if Path::new(gitdir_str).is_absolute() { PathBuf::from(gitdir_str) } else { cur.join(gitdir_str) }; return resolved.parent().and_then(|p| p.parent()).map(|p| p.to_path_buf()); } }}When .git is a file (worktree indicator), the code reads the gitdir: pointer, resolves it (handling both absolute and relative paths), and navigates up to find the main repo root. This is used to inject safe.directory config for the sandbox user.
What Codex Does Not Do
Section titled “What Codex Does Not Do”- No worktree creation or management — you cannot
codex worktree addor similar. - No multi-worktree agent orchestration — no built-in support for running multiple agents in separate worktrees.
- No worktree lifecycle management — no reset, remove, or cleanup operations.
OpenCode Implementation
Section titled “OpenCode Implementation”OpenCode has a complete worktree lifecycle management system behind the experimental flag. This is the most comprehensive worktree implementation of the three.
Architecture
Section titled “Architecture”The implementation lives in packages/opencode/src/worktree/index.ts (643 lines) and is exposed via four HTTP endpoints in packages/opencode/src/server/routes/experimental.ts:89-186.
Data Types
Section titled “Data Types”export const Info = z.object({ name: z.string(), // Human-readable name (e.g., "brave-cabin") branch: z.string(), // Git branch (e.g., "opencode/brave-cabin") directory: z.string(), // Absolute path to worktree directory});
export const CreateInput = z.object({ name: z.string().optional(), startCommand: z.string().optional(),});Name Generation
Section titled “Name Generation”OpenCode generates worktree names using an adjective-noun pattern (worktree/index.ts:124-205). Two arrays of 29 adjectives and 30 nouns (e.g., “brave-cabin”, “stellar-falcon”, “nimble-wizard”) produce random names. If a user provides a name, it is slugified (lowercased, non-alphanumeric replaced with hyphens).
The candidate() function (worktree/index.ts:268-284) tries up to 26 variations before giving up. For each candidate, it checks:
- Does the directory already exist on the filesystem?
- Does the branch
refs/heads/opencode/<name>already exist viagit show-ref --verify?
Both must be clear for the name to be accepted.
Worktree Creation
Section titled “Worktree Creation”Worktree.create() (worktree/index.ts:334-417):
- Validates the project uses git (
Instance.project.vcs !== "git"check). - Creates the worktree storage root at
~/.opencode/data/worktree/{projectId}/. - Generates or slugifies the name, finds a unique candidate.
- Executes
git worktree add --no-checkout -b {branch} {directory}from the main repo root. - Registers the worktree in the database via
Project.addSandbox(). - Kicks off async initialization in a
setTimeout(0)callback: a.git reset --hardto populate the worktree with files. b.Instance.provide()to bootstrap a new OpenCode instance (loads project config, starts LSP servers, etc.). c. Emitsworktree.readyorworktree.failedviaGlobalBus. d. Runs the project’s start command + any worktree-specific start command.
The --no-checkout flag on git worktree add creates the worktree without populating files, allowing the separate git reset --hard step to handle checkout. This provides a clean error boundary — creation is separate from checkout.
Worktree Removal
Section titled “Worktree Removal”Worktree.remove() (worktree/index.ts:419-506):
- Canonicalizes the target directory path (handles symlinks, trailing slashes, platform-specific case).
- Parses
git worktree list --porcelainoutput to find the matching entry by canonical path comparison. - If the worktree is not in the git list but the directory exists, falls back to direct
fs.rm(). - Executes
git worktree remove --force {path}. - If git removal fails, re-checks the worktree list. If the entry is gone (git removed it but returned non-zero), proceeds with cleanup. If still present, throws.
- Direct filesystem cleanup via
fs.rm()with retries (maxRetries: 5, retryDelay: 100ms) to handle permission errors and locked files. - Deletes the associated branch with
git branch -D {branch}.
The retry logic on filesystem removal is important: on Windows (and sometimes macOS), file locks from IDEs or LSP servers can prevent immediate deletion. The 5-retry with 100ms delay handles most transient lock scenarios.
Worktree Reset
Section titled “Worktree Reset”Worktree.reset() (worktree/index.ts:508-642) resets a worktree to the default branch. This is the most complex operation:
- Safety check: Refuses to reset the primary workspace (compares canonical paths).
- Default branch detection (4-level fallback):
a.
origin/HEADsymbolic ref → parse remote branch name. b. If noorigin, check forupstreamremote. c. Localmainbranch exists? Use it. d. Localmasterbranch exists? Use it. e. No default found → throw error. - Fetch: If using a remote branch, fetch it first (
git fetch {remote} {branch}). - Hard reset:
git reset --hard {target}in the worktree directory. - Aggressive clean:
git clean -ffdxwith a two-passsweep()function. The first pass may fail on locked files;failed()parses git’s “warning: failed to remove” output to extract problematic paths.prune()attempts directfs.rm()on those paths (with path traversal protection — rejects paths outside the worktree root). A secondgit clean -ffdxhandles any remaining files. - Submodule reset: Three commands in sequence:
git submodule update --init --recursive --forcegit submodule foreach --recursive git reset --hardgit submodule foreach --recursive git clean -fdx
- Validation:
git status --porcelain=v1must return empty. If dirty files remain, throws. - Restart: Queues the project’s start script asynchronously.
HTTP API
Section titled “HTTP API”Four experimental endpoints (experimental.ts:89-186):
| Method | Path | Input | Output |
|---|---|---|---|
| POST | /experimental/worktree | CreateInput (name?, startCommand?) | Worktree.Info |
| GET | /experimental/worktree | — | string[] (directories) |
| DELETE | /experimental/worktree | RemoveInput (directory) | boolean |
| POST | /experimental/worktree/reset | ResetInput (directory) | boolean |
Event System
Section titled “Event System”Two bus events for async status tracking:
worktree.ready— emitted after successful bootstrap (properties:name,branch).worktree.failed— emitted on any failure during async initialization (properties:message).
These events flow through GlobalBus and are visible to all connected clients via SSE.
Frontend Integration
Section titled “Frontend Integration”The web app (packages/app/src/utils/worktree.ts, 73 lines) implements a lightweight state machine for the worktree lifecycle:
- States:
pending,ready,failed(terminal states prevent regression). - Normalizes directory paths (trailing slashes).
- Shared waiter pattern for concurrent creation requests.
The sidebar (packages/app/src/pages/layout/sidebar-project.tsx) displays workspaces as either “local” (primary) or “sandbox” (worktree), with a badge showing the count. Context menus expose workspace operations.
Error Types
Section titled “Error Types”Six specific error types (worktree/index.ts:82-122):
WorktreeNotGitError— non-git projects.WorktreeNameGenerationFailedError— 26 naming attempts exhausted.WorktreeCreateFailedError—git worktree addfailed.WorktreeStartCommandFailedError— startup script failed.WorktreeRemoveFailedError— removal or branch deletion failed.WorktreeResetFailedError— any reset step failed.
Database Integration
Section titled “Database Integration”Worktree directories are persisted in the project’s sandbox list via Project.addSandbox() and Project.removeSandbox(). This survives restarts — OpenCode remembers which worktrees exist even if the process is killed.
Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”.git File vs. Directory
Section titled “.git File vs. Directory”The fundamental worktree detection problem: .git is a directory in normal repos and a file in worktrees. Any code that does if dir.join(".git").is_dir() will miss worktrees entirely. Codex’s get_git_repo_root() uses .exists() which catches both, but doesn’t follow the gitdir: pointer in the file. The Windows sandbox code is the only part that actually reads the pointer.
--git-common-dir vs. --git-dir
Section titled “--git-common-dir vs. --git-dir”git rev-parse --git-dir returns the worktree-specific git directory (e.g., .git/worktrees/my-worktree/). git rev-parse --git-common-dir returns the shared repository root. Using the wrong one produces incorrect paths for config, hooks, and refs. Codex uses --git-common-dir for trust resolution — the correct choice.
External Worktrees Break Simple Detection
Section titled “External Worktrees Break Simple Detection”When a worktree lives outside the main repo’s directory tree (e.g., /tmp/my-worktree), walking up the directory hierarchy will never find the main .git directory. The only reliable detection is reading the .git file or using git rev-parse. Codex documents this as a known limitation with a CLI flag workaround.
Locked Files During Removal
Section titled “Locked Files During Removal”IDE language servers, file watchers, and operating system indexing services can lock files in a worktree. OpenCode handles this with retry logic (5 attempts, 100ms delay) and a two-pass clean strategy that parses git’s warning output. Without this, worktree removal fails intermittently on Windows and macOS.
Branch Cleanup After Worktree Removal
Section titled “Branch Cleanup After Worktree Removal”git worktree remove deletes the checkout but does not delete the branch. If the branch is not explicitly deleted, it accumulates over time. OpenCode’s remove() explicitly runs git branch -D after removing the worktree. Without this, repeated create/remove cycles pollute the branch namespace.
Submodule State in Worktrees
Section titled “Submodule State in Worktrees”Git submodules maintain their own state per worktree. A git reset --hard on the worktree root does not touch submodule contents. OpenCode’s reset runs three separate submodule commands (update, reset, clean) after the main reset. Missing any of these can leave stale submodule state.
git clean -ffdx Can Fail Silently
Section titled “git clean -ffdx Can Fail Silently”The double -f flag in git clean -ffdx is required to remove untracked nested git repositories. Even with this, git clean can fail on locked files and report success (exit code 0) while leaving files behind. OpenCode validates with a final git status --porcelain=v1 check and throws if anything remains.
Path Canonicalization Is Critical
Section titled “Path Canonicalization Is Critical”Worktree directory paths can differ between what git worktree list reports and what the user provides (symlinks, trailing slashes, case differences on case-insensitive filesystems). OpenCode canonicalizes all paths through fs.realpath() + path.normalize() + platform-specific lowercasing before comparison.
Worktree Storage Location
Section titled “Worktree Storage Location”OpenCode stores worktrees in ~/.opencode/data/worktree/{projectId}/{name}/, not alongside the project. This avoids polluting the project directory with worktree checkouts, but means the worktrees are invisible to users browsing their project in a file manager. The tradeoff is intentional — worktrees are an internal implementation detail, not user-facing directories.
No Concurrent Agent Worktree Isolation Yet
Section titled “No Concurrent Agent Worktree Isolation Yet”None of the three tools use worktrees for automatic per-agent isolation. OpenCode’s worktrees are user-managed through the API, not automatically created per session. The next step for AI agent tooling is automatic worktree creation per subagent or per task, with cleanup on completion.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Architecture: Optional Worktree Isolation Layer
Section titled “Architecture: Optional Worktree Isolation Layer”OpenOxide should treat worktrees as an opt-in isolation layer with three modes:
- Direct mode (default): Agent operates in the user’s working directory, same as Aider/Codex today.
- Worktree mode (manual): User or config creates worktrees via CLI; agent sessions are assigned to them.
- Auto-worktree mode (future): Subagents automatically get fresh worktrees; changes are reviewed before merging back.
Crate: openoxide-worktree
Section titled “Crate: openoxide-worktree”pub struct WorktreeManager { main_repo: PathBuf, // The .git root of the main repository storage_root: PathBuf, // Where worktrees are stored (~/.openoxide/worktrees/) worktrees: HashMap<String, WorktreeInfo>,}
pub struct WorktreeInfo { pub name: String, pub branch: String, pub directory: PathBuf, pub status: WorktreeStatus,}
pub enum WorktreeStatus { Creating, Ready, Failed(String), Removing,}Worktree Detection
Section titled “Worktree Detection”Use git2 crate’s Repository::open() which handles .git files natively — no manual pointer parsing needed. For trust resolution, shell out to git rev-parse --git-common-dir (matching Codex’s approach) since git2 does not expose the common dir API directly.
Creation Flow
Section titled “Creation Flow”- Generate name (adjective-noun pattern, adopt OpenCode’s approach).
git2::Repository::worktree()to create the worktree with--no-checkout.git2::Repository::open()to open the new worktree.git2::Repository::reset()withResetType::Hardto populate.- Emit
WorktreeEvent::Readyvia the event bus. - Optionally run project start command via
openoxide-exec.
Reset Flow
Section titled “Reset Flow”- Resolve default branch (remote HEAD → local main → local master).
git2::Repository::reset()withResetType::Hardto the target.- Shell out to
git clean -ffdx(git2 does not have an equivalent). - Handle submodule reset via shell commands.
- Validate clean state via
git2::Repository::statuses().
Removal Flow
Section titled “Removal Flow”- Delete working directory via
std::fs::remove_dir_all()with retry wrapper. git worktree pruneto clean stale entries.- Delete the branch via
git2::Branch::delete().
CLI Integration
Section titled “CLI Integration”openoxide worktree create [--name NAME] [--start-command CMD]openoxide worktree listopenoxide worktree remove <directory>openoxide worktree reset <directory>Auto-Worktree for Subagents (Future)
Section titled “Auto-Worktree for Subagents (Future)”When spawning a subagent in auto-worktree mode:
- Create a fresh worktree branching from the current HEAD.
- Assign the subagent session to the worktree directory.
- On completion, generate a diff of the worktree against the parent.
- Present the diff for review (or auto-merge if configured).
- Remove the worktree.
This provides true session isolation without the agent needing to know it is in a worktree.
Crates
Section titled “Crates”openoxide-worktree:WorktreeManager, creation/reset/removal operations, name generation, event emission.openoxide-git: Shared git utilities (repo detection, trust resolution, default branch detection). Used by worktree manager and other git features.openoxide-exec: Start command execution (reused from command execution feature).
Key Design Decisions
Section titled “Key Design Decisions”- Use
git2for worktree creation — avoids shelling out for the core operation, gets proper error types. - Shell out for
git clean— git2 has no clean equivalent. Accept the subprocess cost. - Store worktrees outside the project — follow OpenCode’s
~/.openoxide/worktrees/pattern. Keep the project directory clean. - Branch naming convention:
openoxide/<name>prefix, matching OpenCode’sopencode/<name>. - Retry on removal — file locks are real. Implement a 5-attempt retry with exponential backoff.
- Path canonicalization everywhere — use
std::fs::canonicalize()before any path comparison. Handle platform-specific quirks.