Linked worktree git add fails: .git/worktrees index.lock remains read-only despite --add-dir

Open 💬 3 comments Opened May 20, 2026 by tangi-vass

What version of Codex is running?

Tested across multiple npm package versions of @openai/codex:

  • 0.125.0
  • 0.126.0-alpha.17
  • 0.128.0
  • 0.129.0
  • 0.130.0
  • 0.131.0
  • 0.132.0

Current installed binary path in my normal environment:

/home/tangi/.nvm/versions/node/v24.0.1/bin/codex

System:

Linux tangi-t14 6.8.0-111-generic #111-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 11 23:16:02 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux

Which model were you using?

gpt-5.5 in the smoke tests below.

What happened?

git add fails inside a Git linked worktree because the sandbox keeps the linked worktree index metadata read-only:

fatal: Unable to create '<main-repo>/.git/worktrees/<worktree>/index.lock': Read-only file system

This happens even when the main repo .git directory is passed with --add-dir and appears in Codex's sandbox summary.

This is related to openai/codex#7071, but the failure here is specifically for Git linked worktrees. In that case git add does not write .git/index.lock in the worktree directory. It writes under the main repo metadata path:

<main-repo>/.git/worktrees/<worktree>/index.lock

Reproduction

Use a linked worktree and allow the main repository .git directory as an additional writable directory:

MAIN=$(mktemp -d /tmp/codex-linked-main.XXXXXX)
git init "$MAIN"
printf base > "$MAIN/base.txt"
git -C "$MAIN" add base.txt
git -C "$MAIN" -c user.name=Codex -c user.email=codex@example.invalid commit -m initial
mkdir -p "$MAIN/.worktrees"
git -C "$MAIN" worktree add "$MAIN/.worktrees/task-1" -b task-1

npm exec --yes --package @openai/codex@0.132.0 -- codex exec \
  --sandbox workspace-write \
  -C "$MAIN/.worktrees/task-1" \
  --add-dir "$MAIN/.git" \
  -c 'approval_policy="never"' \
  'Create file smoke.txt containing smoke, run git add smoke.txt, then git status --short. Do not commit. Report the exact output.'

Observed Codex sandbox summary includes the main .git directory:

sandbox: workspace-write [workdir, /tmp, $TMPDIR, /tmp/liza-codex-0132-nolegacy-main.20rQJ8/.git] (network access enabled)

But git add fails:

fatal: Unable to create '/tmp/liza-codex-0132-nolegacy-main.20rQJ8/.git/worktrees/task-1/index.lock': Read-only file system

git status --short afterwards:

?? codex0132-nolegacy-smoke.txt

Version matrix

Same linked-worktree smoke test, using workspace-write, --add-dir <main repo .git>, and no named permission profile unless noted:

0.125.0 without use_legacy_landlock => FAIL read-only index.lock
0.125.0 with use_legacy_landlock    => PASS git add succeeds
0.126.0-alpha.17 with use_legacy_landlock => FAIL panic before shell command
0.128.0 with use_legacy_landlock => FAIL panic before shell command
0.129.0 with use_legacy_landlock => FAIL panic before shell command
0.130.0 with use_legacy_landlock => FAIL panic before shell command
0.131.0 without use_legacy_landlock => FAIL read-only index.lock
0.131.0 with use_legacy_landlock => FAIL panic before shell command
0.132.0 without use_legacy_landlock => FAIL read-only index.lock
0.132.0 with use_legacy_landlock => FAIL panic before shell command

Passing 0.125.0 legacy run:

npm exec --yes --package @openai/codex@0.125.0 -- codex exec \
  --enable use_legacy_landlock \
  --sandbox workspace-write \
  -C /tmp/liza-codex-0125-main.qf7fvw/.worktrees/task-1 \
  --add-dir /tmp/liza-codex-0125-main.qf7fvw/.git \
  -c 'approval_policy="never"' \
  'Create file codex0125-legacy-smoke.txt containing codex0125-legacy-smoke-test, run git add, then git status --short.'

Output:

A  codex0125-legacy-smoke.txt

Failing modern legacy-landlock runs panic before executing shell commands:

thread 'main' (...) panicked at linux-sandbox/src/linux_run_main.rs:311:9:
permission profiles requiring direct runtime enforcement are incompatible with --use-legacy-landlock
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Expected behavior

There should be a supported way for automation to run git add in a linked worktree under sandboxing without danger-full-access.

If --add-dir <main repo .git> is shown as writable in the sandbox summary, git add should be able to create:

<main-repo>/.git/worktrees/<worktree>/index.lock

Alternatively, Codex should document the supported configuration for linked worktrees.

Why this matters

Multi-agent and automation workflows commonly use Git linked worktrees for isolated task execution. Without writable linked-worktree metadata, agents can edit files but cannot stage or commit them.

View original on GitHub ↗

3 Comments

jshaofa-ui · 3 months ago

Solution: openai/codex #23661 — Linked worktree git add fails: index.lock read-only

🔍 Root Cause Analysis

When running git add inside a Git linked worktree under Codex's sandbox, the operation fails because the sandbox keeps the linked worktree's index metadata read-only:

fatal: Unable to create '<main-repo>/.git/worktrees/<worktree>/index.lock': Read-only file system

Why This Happens

Git linked worktrees store their metadata in the main repository's .git/worktrees/ directory, NOT in the worktree directory itself. When git add runs in a linked worktree:

Normal worktree:    .git/index.lock (in worktree dir)
Linked worktree:    <main-repo>/.git/worktrees/<name>/index.lock (in main repo's .git dir)

The sandbox configuration passes --add-dir "$MAIN/.git" to allow writes to the main .git directory, but the Landlock sandbox (introduced in v0.125.0+) restricts filesystem access at a lower level than --add-dir.

Version Matrix Analysis

0.125.0 without use_legacy_landlock => FAIL read-only index.lock
0.125.0 with use_legacy_landlock    => PASS git add succeeds
0.126.0+ with use_legacy_landlock   => FAIL panic before shell command
0.131.0+ without use_legacy_landlock => FAIL read-only index.lock
0.132.0 without use_legacy_landlock => FAIL read-only index.lock

Key observations:

  1. v0.125.0 with legacy Landlock: Works — legacy Landlock doesn't have the same restrictions
  2. v0.126.0+ with legacy Landlock: Panics — new permission profiles are incompatible with legacy Landlock
  3. All versions without legacy Landlock: Fail — the new Landlock implementation blocks writes to .git/worktrees/

Root Cause: Landlock Doesn't Recognize Nested .git/worktrees/ Path

The Landlock sandbox implementation likely:

  1. Adds the .git directory to the allowed paths via --add-dir
  2. But the Landlock rules don't properly propagate write permissions to nested subdirectories
  3. OR the .git/worktrees/ directory has different permissions that Landlock interprets as read-only

This is a path resolution issue in the Landlock sandbox: the sandbox adds /path/to/repo/.git as writable, but when Git tries to write to /path/to/repo/.git/worktrees/task-1/index.lock, Landlock doesn't recognize this as within the allowed path.

🛠️ Proposed Fix

Fix 1: Ensure Landlock Rules Propagate to Nested Paths (Primary Fix)

The Landlock sandbox must ensure that --add-dir rules propagate to all nested subdirectories, including .git/worktrees/<name>/:

// In linux-sandbox/src/landlock.rs (or equivalent)

fn add_writable_path(landlock: &mut Landlock, path: &Path) -> Result<()> {
    // Current (buggy): Only adds the exact path
    landlock.add_rule(Rule::Writable(path))?;
    
    // Fixed: Also add all parent directories and ensure nested paths are covered
    // Landlock requires explicit rules for each directory level
    let mut current = path.to_path_buf();
    loop {
        landlock.add_rule(Rule::Writable(&current))?;
        if !current.pop() {
            break;
        }
    }
    
    // Also add the path with a wildcard for nested contents
    landlock.add_rule(Rule::WritableDir(path))?;
    
    Ok(())
}

Fix 2: Special-Case .git/worktrees/ Directory

Add explicit handling for Git linked worktree metadata paths:

fn configure_git_worktree_access(landlock: &mut Landlock, worktree_path: &Path) -> Result<()> {
    // Find the main .git directory for this linked worktree
    let git_dir_output = Command::new("git")
        .args(["-C", worktree_path, "rev-parse", "--git-dir"])
        .output()?;
    
    let git_dir = Path::new(std::str::from_utf8(&git_dir_output.stdout)?.trim());
    
    // Add the .git directory as writable
    add_writable_path(landlock, git_dir)?;
    
    // Specifically add .git/worktrees/ for linked worktree metadata
    let worktrees_dir = git_dir.join("worktrees");
    if worktrees_dir.exists() {
        add_writable_path(landlock, &worktrees_dir)?;
    }
    
    Ok(())
}

Fix 3: Use --add-dir with Recursive Flag

If the sandbox supports recursive directory permissions, ensure --add-dir is treated as recursive:

// In the CLI argument parsing
match arg {
    "--add-dir" => {
        let dir = args.next()?;
        // Mark as RECURSIVE writable (covers all nested paths)
        sandbox.add_writable_dir(&dir, Recursive::Yes);
    }
}

Fix 4: Fallback to Landlock Path Under-Approximation

As a defensive measure, when Git operations fail with EROFS in a linked worktree, retry with expanded Landlock permissions:

fn execute_with_git_worktree_fallback(cmd: &str, worktree_path: &Path) -> Result<Output> {
    let output = sandbox.execute(cmd)?;
    
    if output.status.code() == Some(128) && output.stderr.contains("Read-only file system") {
        // Git operation failed due to Landlock restrictions
        // Retry with expanded .git/worktrees/ permissions
        if let Some(git_dir) = find_git_dir(worktree_path) {
            sandbox.add_writable_dir(&git_dir.join("worktrees"), Recursive::Yes);
            return sandbox.execute(cmd);
        }
    }
    
    Ok(output)
}

📋 Implementation Priority

| Priority | Fix | Effort | Impact |
|----------|-----|--------|--------|
| P0 | Fix 1: Landlock path propagation | Medium | Core fix for the issue |
| P0 | Fix 2: Special-case .git/worktrees/ | Low | Targeted fix |
| P1 | Fix 3: Recursive --add-dir | Low | Prevents similar issues |
| P2 | Fix 4: Fallback mechanism | Medium | Defensive improvement |

✅ Verification

  1. Reproduce the bug: Run the exact reproduction steps from the issue
  2. Verify fix: git add should succeed in linked worktree
  3. Version matrix: Verify fix works across all supported versions
  4. Security: Verify Landlock still blocks unauthorized paths

💰 Value Assessment

  • Severity: S2 — Blocks all git operations in linked worktrees
  • Impact: Affects all Codex users who work with linked worktrees
  • Fix complexity: Medium (Landlock sandbox modification)
  • Estimated value: $1,500–3,000
timzam · 1 month ago
This was generated by AI during triage.

+1 from Codex on macOS in an sf-managed linked worktree on 2026-07-10. The worktree root was writable, but its per-worktree Git directory lived under the parent checkout’s .git/worktrees/ directory. The first fetch failed:

error: cannot open '/Users/timzam/dev/silver-fork/.git/worktrees/sf-sandbox-go-build-cache/FETCH_HEAD': Operation not permitted

The same command succeeded with escalated filesystem access. Staging, committing, checkout, merge, and linked-worktree ref updates required the same broader access boundary. This reproduces the issue for more than git add: normal Git lifecycle operations in the linked worktree cannot update their shared metadata under workspace-write.

Casey-Bates · 1 month ago

Additional macOS Desktop automation reproduction (2026-07-14)

This remains reproducible in a recurring Codex Desktop automation using a linked Git worktree.

Environment

  • Codex Desktop: 26.707.71524 (bundle 5263)
  • Codex CLI: 0.144.3
  • macOS 26.5.2 (25F84), Apple Silicon
  • Sandbox: managed workspace-write task profile
  • Approval policy: on-request with automatic review

Scenario

The automation is intentionally Git-mutating: it starts by fetching origin, compares a detached origin/main baseline, and may later create a branch, commit, push, and open a PR.

The task workspace is a linked worktree. Its visible .git file resolves outside the workspace:

git rev-parse --git-dir
<main-repo>/.git/worktrees/<worktree>

git rev-parse --git-common-dir
<main-repo>/.git

git rev-parse --git-path FETCH_HEAD
<main-repo>/.git/worktrees/<worktree>/FETCH_HEAD

The generated task permission profile exposed the worktree itself as writable and the resolved linked-worktree Git metadata as read-only. In the initial automation turn, the parent <main-repo>/.git path was also nominally writable, but the more-specific read-only linked-worktree entry still won.

Failure

The first required command failed before any network work or quality checks:

$ git fetch origin
error: cannot open '<main-repo>/.git/worktrees/<worktree>/FETCH_HEAD': Operation not permitted

FETCH_HEAD did not already exist, so Git was attempting to create it. The target directory is owned by the current user with normal drwxr-xr-x permissions. Read-only Git operations such as git status, git log, and git rev-parse work.

Retrying the exact command with sandbox_permissions: "require_escalated" did not reach an approval that could unblock it. Automatic review rejected the escalation because the managed environment forbade unsandboxed execution.

Impact

The unattended quality-gate automation stopped in phase 1. It could not establish the required origin/main baseline, so no Ruff/type checks, autofix PR, or triage issue could be produced.

Expected behavior

For an explicitly Git-mutating managed task in a trusted linked worktree, Codex should resolve and grant scoped write access to both:

  • the common Git directory, and
  • the current linked-worktree Git directory under .git/worktrees/<worktree>.

A narrower generated read-only entry should not override the task's explicit Git authorization. If the sandbox cannot safely grant that scope, the escalation path should surface and honor an approval rather than leaving the automation permanently blocked.

Related: #14338 (writable gitdir), #15310 (automation sandbox propagation), and #32848 (nested Code Mode escalation on macOS).