Resolved linked-worktree Git directory is unreadable with `:root = "deny"`

Open 💬 1 comment Opened Aug 15, 2026 by tpansino

What version of Codex CLI is running?

codex-cli 0.147.0

What subscription do you have?

ChatGPT Plus

Which model were you using?

gpt-5.6-sol

What platform is your computer?

Darwin 25.5.0 arm64 arm macOS 26.5.2 on Apple Silicon.

What terminal emulator and version are you using (if applicable)?

Zed using the Codex integration.

Codex doctor report

Not included because the affected session was running through the Zed integration. A redacted CLI doctor report from the same Codex installation can be provided if useful.

What issue are you seeing?

A custom permission profile extends :workspace but denies filesystem reads by default:

default_permissions = "restricted-workspace"

[permissions.restricted-workspace]
extends = ":workspace"

[permissions.restricted-workspace.filesystem]
":minimal" = "read"
":root" = "deny"

When the active workspace is a Git linked worktree, its .git pointer file is readable, but the resolved Git metadata directory outside the workspace is not.

Example layout:

/Users/me/dev/repo/.git/
/Users/me/dev/worktrees/repo/task/

The worktree contains:

gitdir: /Users/me/dev/repo/.git/worktrees/task

Codex can read the worktree's .git file, but the sandbox denies reads from the resolved directory, including:

/Users/me/dev/repo/.git/worktrees/task/HEAD
/Users/me/dev/repo/.git/worktrees/task/commondir

The worktree is listed as an active workspace root by /status.

Explicitly adding the main repository's .git directory as a read permission works around the problem:

[permissions.restricted-workspace.filesystem]
"/Users/me/dev/repo/.git" = "read"

Removing ":root" = "deny" also avoids the problem, but restores broad filesystem read access and defeats the least-privilege purpose of the custom profile.

What steps can reproduce the bug?

  1. Create a normal Git repository.
  2. Create a linked worktree in a directory outside the main checkout.
  3. Configure and select the permission profile shown above.
  4. Launch Codex with the linked worktree as the workspace.
  5. Confirm that /status lists the linked worktree as a workspace root.
  6. Run:
cat .git
gitdir=$(sed -n 's/^gitdir: //p' .git)
cat "$gitdir/HEAD"
cat "$gitdir/commondir"
git status

The .git pointer can be read, but reads from $gitdir are denied by the sandbox.

What is the expected behavior?

When a workspace root contains a .git pointer file, Codex should dynamically add the resolved worktree Git directory and common Git directory to the readable roots.

These paths should remain protected from writes unless separately authorized.

This would match the documented workspace-write behavior:

If /.git is a pointer file (gitdir: ...), the resolved Git directory path is also protected as read-only.

A read-only protected path should remain readable when a custom permission profile replaces ambient root reads with an explicit allowlist.

Additional information

This appears distinct from existing reports about Git metadata being read-only for writes:

Those issues concern operations such as git add and git commit. This report concerns basic read access needed by commands such as git status.

Possible root-cause hypothesis:

In the macOS sandbox implementation, resolved Git metadata appears to be added as a protected path in the write policy. When full-disk reads are disabled, however, the read policy appears to admit only explicitly configured readable roots. The dynamically resolved Git directory does not appear to be added to that set.

Relevant implementation:

https://github.com/openai/codex/blob/rust-v0.147.0/codex-rs/sandboxing/src/seatbelt.rs#L595-L681

A possible fix would be to resolve the active workspace's .git pointer and commondir, add those locations to the sandbox's readable roots, and retain the existing recursive write protection.

View original on GitHub ↗

1 Comment

jdcodes1 · 11 days ago

Traced on main @ 1f41cc5d92 — the linked-worktree gitdir expansion exists in the permission machinery, but only on the write side; the read side has no counterpart, which is exactly the hole a ":root" = "deny" read profile falls into.

Where the expansion lives today. default_read_only_subpaths_for_writable_root explicitly understands worktree pointer files: when a writable root's .git is a gitdir: pointer, it resolves the target and adds it as a read-only carveout:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/protocol/src/permissions.rs#L1607-L1627

Note what that encodes: the design intent is "agents may read git metadata but not write it" — the resolved gitdir is protected from writes, not from reads.

Where the read profile is computed. get_readable_roots_with_cwd (permissions.rs#L984-L998) filters the profile's resolved entries by read access — it never consults the workspace's .git pointer. So with reads default-denied, the readable set contains the workspace root (which includes the pointer file) but not the externally-resolved metadata directory. Result: gitdir: is readable, …/.git/worktrees/task/HEAD is not — your exact symptom, and it silently degrades every git-aware feature in a linked worktree.

One subtlety for whoever fixes this: the immediate gitdir is not enough. A linked worktree's gitdir contains a commondir file pointing back at the main .git (typically ../..), and real git reads resolve through it for refs/, objects/, config, etc. The write-side carveout only pushes the immediate worktrees/<name> directory (fine for write-protection), but a read grant scoped that narrowly would fix HEAD/commondir and still fail one step later. The read-side expansion should resolve gitdir → read commondir → grant the common .git directory (which is what the reported workaround does by hand).

Fix outline.

  1. When resolving workspace roots into readable entries, if <root>/.git is a pointer file, add the resolved gitdir and its resolved common dir as implicit readable roots — mirroring (and sharing helpers with) resolve_gitdir_from_file, which already exists in this file.
  2. Keep the existing write-side carveout so the newly-readable paths remain non-writable — that combination reproduces the normal-repo invariant (readable, write-protected .git) for linked worktrees.
  3. Since this implicitly widens a deny-by-default read profile to a path outside the workspace, it's worth surfacing in /status (e.g. listing the derived git-metadata read root alongside the workspace roots) so restricted-profile users can see and, if desired, override it.

Test shape: build a repo + git worktree add, apply a profile with ":root" = "deny", and assert get_readable_roots_with_cwd for the worktree cwd includes the resolved common .git — plus an end-to-end git status under the sandbox for the integration tier.