Resolved linked-worktree Git directory is unreadable with `:root = "deny"`
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?
- Create a normal Git repository.
- Create a linked worktree in a directory outside the main checkout.
- Configure and select the permission profile shown above.
- Launch Codex with the linked worktree as the workspace.
- Confirm that
/statuslists the linked worktree as a workspace root. - 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/.gitis 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:
- https://github.com/openai/codex/issues/19786
- https://github.com/openai/codex/issues/23661
- https://github.com/openai/codex/issues/24115
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.
1 Comment
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_rootexplicitly understands worktree pointer files: when a writable root's.gitis agitdir: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.gitpointer. 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/HEADis 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
commondirfile pointing back at the main.git(typically../..), and real git reads resolve through it forrefs/,objects/,config, etc. The write-side carveout only pushes the immediateworktrees/<name>directory (fine for write-protection), but a read grant scoped that narrowly would fixHEAD/commondirand still fail one step later. The read-side expansion should resolvegitdir→ readcommondir→ grant the common.gitdirectory (which is what the reported workaround does by hand).Fix outline.
<root>/.gitis 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..git) for linked worktrees./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 assertget_readable_roots_with_cwdfor the worktree cwd includes the resolved common.git— plus an end-to-endgit statusunder the sandbox for the integration tier.