workspace-write sandbox fails with "bwrap: Can't mkdir <cwd>/.git: Permission denied" when cwd is a non-git directory containing git repos

Open 💬 0 comments Opened Aug 6, 2026 by pedroandriani

What version of Codex CLI is running?

0.146.0-alpha.3

What subscription do you have?

Pro 20x

Which model were you using?

gpt codex 5.6 sol

What platform is your computer?

Linux 6.8.0-136-generic x86_64 x86_64 (Ubuntu 24.04)

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

Reproduced from a plain bash shell over SSH (no multiplexer). Originally observed through the Codex IDE extension (VS Code-based), but the extension is not required — see repro steps.

Codex doctor report

Note on the doctor output: it shows `filesystem sandbox: unrestricted` because my persistent
config sets `sandbox_mode = "danger-full-access"` — that is precisely the workaround I adopted
for this bug. The repro above forces `-c sandbox_mode=workspace-write` explicitly, so the
sandbox path is exercised regardless of that setting. The report was generated with cwd at the
non-git root, which is why it shows `repo detected: false`.
E logo abaixo, o JSON:


{
  "schemaVersion": 1,
  "generatedAt": "1786051873s since unix epoch",
  "overallStatus": "warning",
  "codexVersion": "0.146.0-alpha.3",
  "checks": {
    "app_server.status": {
      "id": "app_server.status",
      "category": "app-server",
      "status": "ok",
      "summary": "background server is not running",
      "details": {
        "control socket": "/home/USER/.codex/app-server-control/app-server-control.sock",
        "daemon state dir": "/home/USER/.codex/app-server-daemon",
        "mode": "ephemeral",
        "pid file": "/home/USER/.codex/app-server-daemon/app-server.pid (missing)",
        "settings": "/home/USER/.codex/app-server-daemon/settings.json (missing)",
        "status": "not running",
        "update-loop pid file": "/home/USER/.codex/app-server-daemon/app-server-updater.pid (missing)"
      },
      "remediation": null,
      "durationMs": 0
    },
    "auth.credentials": {
      "id": "auth.credentials",
      "category": "auth",
      "status": "ok",
      "summary": "auth is configured",
      "details": {
        "auth file": "/home/USER/.codex/auth.json",
        "auth storage mode": "File",
        "stored API key": "false",
        "stored ChatGPT tokens": "true",
        "stored agent identity": "false",
        "stored auth mode": "chatgpt"
      },
      "remediation": null,
      "durationMs": 0
    },
    "config.load": {
      "id": "config.load",
      "category": "config",
      "status": "ok",
      "summary": "config loaded",
      "details": {
        "CODEX_HOME": "/home/USER/.codex",
        "config.toml": "/home/USER/.codex/config.toml",
        "config.toml parse": "ok",
        "cwd": "/projetos",
        "enabled feature flags": "shell_tool, unified_exec, shell_snapshot, code_mode_host, terminal_resize_reflow, sqlite, hooks, enable_request_compression, multi_agent, apps, tool_search_always_defer_mcp_tools, tool_suggest, plugins, in_app_browser, browser_use, browser_use_full_cdp_access, browser_use_external, computer_use, remote_plugin, plugin_sharing, image_generation, resize_all_images, item_ids, skill_mcp_dependency_install, skill_search, mentions_v2, steer, guardian_approval, goals, collaboration_modes, tool_call_mcp_elicitation, auth_elicitation, personality, fast_mode, tui_app_server, remote_compaction_v2, workspace_dependencies",
        "feature flag overrides": "none",
        "feature flags enabled": "37",
        "log dir": "/home/USER/.codex/log",
        "mcp servers": "0",
        "model": "<default>",
        "model provider": "openai",
        "sqlite home": "/home/USER/.codex"
      },
      "remediation": null,
      "durationMs": 0
    },
    "git.environment": {
      "id": "git.environment",
      "category": "git",
      "status": "ok",
      "summary": "git version 2.43.0",
      "details": {
        "PATH git #1": "/usr/bin/git",
        "PATH git #2": "/bin/git",
        "PATH git entries": "2",
        "git build options": "git version 2.43.0; cpu: x86_64; no commit associated with this build; sizeof-long: 8; sizeof-size_t: 8; shell-path: /bin/sh",
        "git exec path": "/usr/lib/git-core",
        "git version": "git version 2.43.0",
        "repo detected": "false",
        "selected git": "/usr/bin/git"
      },
      "remediation": null,
      "durationMs": 61
    },
    "installation": {
      "id": "installation",
      "category": "install",
      "status": "ok",
      "summary": "installation looks consistent",
      "details": {
        "current executable": "/home/USER/.antigravity-ide-server/extensions/openai.chatgpt-26.721.30844-linux-x64/bin/linux-x86_64/codex",
        "install context": "other",
        "managed by bun": "false",
        "managed by npm": "false",
        "managed by pnpm": "false",
        "managed package root": "not set"
      },
      "remediation": null,
      "durationMs": 1
    },
    "mcp.config": {
      "id": "mcp.config",
      "category": "mcp",
      "status": "ok",
      "summary": "no MCP servers configured",
      "details": {},
      "remediation": null,
      "durationMs": 0
    },
    "network.env": {
      "id": "network.env",
      "category": "network",
      "status": "ok",
      "summary": "network-related environment looks readable",
      "details": {
        "proxy env vars": "none"
      },
      "remediation": null,
      "durationMs": 0
    },
    "network.provider_reachability": {
      "id": "network.provider_reachability",
      "category": "reachability",
      "status": "ok",
      "summary": "active provider endpoints are reachable over HTTP",
      "details": {
        "ChatGPT base URL": "https://chatgpt.com/backend-api/ reachable (HTTP 403)",
        "reachability mode": "ChatGPT auth"
      },
      "remediation": null,
      "durationMs": 81
    },
    "network.websocket_reachability": {
      "id": "network.websocket_reachability",
      "category": "websocket",
      "status": "ok",
      "summary": "Responses WebSocket handshake succeeded",
      "details": {
        "DNS": "2 IPv4, 2 IPv6, first IPv6",
        "auth mode": "chatgpt",
        "connect timeout": "15000 ms",
        "endpoint": "wss://chatgpt.com/backend-api/<redacted>",
        "handshake result": "HTTP 101 Switching Protocols",
        "model provider": "openai",
        "models etag present": "true",
        "provider name": "OpenAI",
        "proxy env vars": "none",
        "reasoning header": "false",
        "server model present": "false",
        "supports websockets": "true",
        "wire API": "responses"
      },
      "remediation": null,
      "durationMs": 655
    },
    "runtime.provenance": {
      "id": "runtime.provenance",
      "category": "runtime",
      "status": "ok",
      "summary": "running local build on linux-x86_64",
      "details": {
        "commit": "unknown",
        "current executable": "/home/USER/.antigravity-ide-server/extensions/openai.chatgpt-26.721.30844-linux-x64/bin/linux-x86_64/codex",
        "install method": "other",
        "platform": "linux-x86_64",
        "version": "0.146.0-alpha.3"
      },
      "remediation": null,
      "durationMs": 0
    },
    "runtime.search": {
      "id": "runtime.search",
      "category": "search",
      "status": "warning",
      "summary": "search command could not be verified",
      "details": {
        "search command": "rg",
        "search command readiness": "No such file or directory (os error 2)",
        "search provider": "system"
      },
      "remediation": "Install ripgrep or repair the bundled Codex package.",
      "durationMs": 0
    },
    "sandbox.helpers": {
      "id": "sandbox.helpers",
      "category": "sandbox",
      "status": "ok",
      "summary": "sandbox configuration is readable",
      "details": {
        "approval policy": "OnRequest",
        "codex-linux-sandbox helper": "/home/USER/.codex/tmp/arg0/codex-arg0CqsC4g/codex-linux-sandbox",
        "execve wrapper helper": "/home/USER/.codex/tmp/arg0/codex-arg0CqsC4g/codex-execve-wrapper",
        "filesystem sandbox": "unrestricted",
        "network sandbox": "enabled"
      },
      "remediation": null,
      "durationMs": 0
    },
    "state.paths": {
      "id": "state.paths",
      "category": "state",
      "status": "ok",
      "summary": "state paths and databases are inspectable",
      "details": {
        "CODEX_HOME": "/home/USER/.codex (dir)",
        "active rollout files": "873 files, 544270840 total bytes, 623448 average bytes",
        "archived rollout files": "0 files, 0 total bytes, 0 average bytes",
        "goals DB": "/home/USER/.codex/goals_1.sqlite (file)",
        "goals DB integrity": "ok",
        "log DB": "/home/USER/.codex/logs_2.sqlite (file)",
        "log DB integrity": "ok",
        "log dir": "/home/USER/.codex/log (missing)",
        "memories DB": "/home/USER/.codex/memories_1.sqlite (file)",
        "memories DB integrity": "ok",
        "sqlite home": "/home/USER/.codex (dir)",
        "state DB": "/home/USER/.codex/state_5.sqlite (file)",
        "state DB integrity": "ok",
        "thread history DB": "/home/USER/.codex/thread_history_1.sqlite (missing)",
        "thread history DB integrity": "skipped (missing)"
      },
      "remediation": null,
      "durationMs": 2163
    },
    "state.rollout_db_parity": {
      "id": "state.rollout_db_parity",
      "category": "threads",
      "status": "ok",
      "summary": "rollout files and state DB thread inventory agree",
      "details": {
        "default model provider": "openai",
        "rollout DB active files": "873",
        "rollout DB active rows": "873",
        "rollout DB archive mismatches": "0",
        "rollout DB archived files": "0",
        "rollout DB archived rows": "0",
        "rollout DB duplicate DB paths": "0",
        "rollout DB duplicate rollout thread ids": "0",
        "rollout DB malformed file names": "0",
        "rollout DB missing active rows": "0",
        "rollout DB missing archived rows": "0",
        "rollout DB model providers": "openai=873",
        "rollout DB rows": "873",
        "rollout DB scan cap reached": "false",
        "rollout DB scan errors": "0",
        "rollout DB sources": "subagent:other=451, vscode=410, exec=9, cli=3",
        "rollout DB stale rows": "0"
      },
      "remediation": null,
      "durationMs": 773
    },
    "system.environment": {
      "id": "system.environment",
      "category": "system",
      "status": "ok",
      "summary": "OS language en-US",
      "details": {
        "EDITOR": "not set",
        "LANG": "en_US.UTF-8",
        "VISUAL": "not set",
        "os": "Ubuntu 24.4.0 (noble) [64-bit]",
        "os language": "en-US",
        "os type": "Ubuntu",
        "os version": "24.4.0"
      },
      "remediation": null,
      "durationMs": 20
    },
    "terminal.env": {
      "id": "terminal.env",
      "category": "terminal",
      "status": "ok",
      "summary": "terminal metadata was detected",
      "details": {
        "SSH_CLIENT": "present",
        "SSH_CONNECTION": "present",
        "VSCODE_IPC_HOOK_CLI": "present",
        "color output": "disabled (stdout is not a terminal)",
        "effective locale": "en_US.UTF-8",
        "stderr is terminal": "false",
        "stdin is terminal": "false",
        "stdout is terminal": "false",
        "terminal": "unknown",
        "terminal size": "unavailable (Resource temporarily unavailable (os error 11))"
      },
      "remediation": null,
      "durationMs": 4
    },
    "terminal.title": {
      "id": "terminal.title",
      "category": "title",
      "status": "ok",
      "summary": "terminal title default",
      "details": {
        "terminal title activity": "true",
        "terminal title items": "activity, project-name",
        "terminal title project source": "cwd",
        "terminal title project value": "projetos",
        "terminal title source": "default"
      },
      "remediation": null,
      "durationMs": 0
    },
    "updates.status": {
      "id": "updates.status",
      "category": "updates",
      "status": "ok",
      "summary": "update configuration is locally consistent",
      "details": {
        "cached latest version": "0.145.0",
        "check for update on startup": "true",
        "last checked at": "2026-07-22T10:36:07.314619710Z",
        "latest version": "0.146.1",
        "latest version status": "current version is not older",
        "update action": "manual or unknown",
        "version cache": "/home/USER/.codex/version.json"
      },
      "remediation": null,
      "durationMs": 93
    }
  }
}

What issue are you seeing?

When a session's cwd is a directory that is not a git repository — but whose subdirectories are git repos — the workspace-write sandbox fails during setup with:

bwrap: Can't mkdir /projetos/.git: Permission denied

This happens before any apply_patch runs, so editing is blocked.

The sandbox appears to unconditionally treat <root>/.git as a protected subpath for the cwd / writable roots. When that .git does not exist, it tries to create it — which fails whenever the user cannot write to the top-level directory.

Our layout is a "monorepo of repos" on a shared multi-user server:

/projetos/ <- root-owned, NOT a git repo, not writable by regular users
/projetos/team-a/ <- a git repo, owned/writable by the user
/projetos/team-b/ <- a git repo, owned/writable by the user
...

From the user's point of view the failure looks intermittent: the agent sometimes recovers on its own by re-running the edit with cwd inside a sub-repo (where .git exists and the sandbox mounts fine). That burns turns and tokens, and often it simply stays stuck.

What steps can reproduce the bug?

No IDE and no model needed — codex sandbox reproduces it directly, since the failure is in sandbox setup.

Setup: a top-level directory that is not a git repo and is not writable by your user, containing git repos as subdirectories. E.g.:

sudo mkdir /projetos && sudo chown root:root /projetos # not writable by $USER, not a git repo
sudo -u "$USER" git init /projetos/team-a # subdirectory IS a git repo

Repro (fails):

cd /projetos
codex -c sandbox_mode=workspace-write sandbox -- /bin/echo ok

bwrap: Can't mkdir /projetos/.git: Permission denied

Negative control (works), same command one level down:

cd /projetos/team-a
codex -c sandbox_mode=workspace-write sandbox -- /bin/echo ok

ok

The only difference between the two runs is whether cwd is itself a git repository.

Note: the failing run exits 0 despite the sandbox never being set up, which is arguably a second bug — the caller cannot distinguish "command ran" from "sandbox setup failed".

What is the expected behavior?

When the cwd (or a writable root) is not a git repository, the sandbox should skip the .git protection for that root instead of failing hard — there is nothing to protect there.

If the protection must stay unconditional, please expose a config opt-out so a non-git root can be treated as a plain writable directory.

At minimum, the failure should not exit 0.

Additional information

What we already tried (none of it works):

  • Explicit writable_roots covering the sub-repos: no effect — the .git handling for the cwd root appears unconditional.
  • project_root_markers = []: no effect — the sandbox still attempts to create /projetos/.git.
  • Looked for an opt-out (require_git_repo, ignore_git, or similar): none in this build. sandbox_workspace_write exposes only writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp.
  • Creating /projetos/.git manually: not acceptable. An empty/stub .git there makes every other tool running with cwd in that directory treat it as an invalid git repository, breaking unrelated tooling on a shared server.
  • sandbox_mode = "danger-full-access": avoids the error but discards the OS sandbox entirely — not a reasonable trade for a mount that protects nothing in this case.

On versions: this CLI ships bundled inside the IDE extension, so we cannot upgrade it independently of the extension. The bundled build is 0.146.0-alpha.3; the latest published @openai/codex at the time of writing is 0.146.1, and codex doctor itself reports latest version status: current version is not older. Happy to re-verify on a newer build if you point me at one.

Related issues — this looks like one root cause with several symptoms. The .git protection appears to assume a standard layout (a real .git directory, present and writable by the sandbox setup). Each open report is that assumption breaking in a different way:

  • #27354 — cwd is a repo, but .git is a symlink created by Android's repo tool: cannot enforce sandbox read-only path /.git because it crosses writable symlink. Closest to this report: the protection is applied unconditionally and hard-fails on a non-standard layout. That author asks for the same thing I do — degrade instead of failing outright.
  • #27418 — sandbox remounts the worktree gitdir read-only despite explicit .git write permission.
  • #15505.git stays read-only under workspace-write even with the repo in writable_roots (touch: cannot touch '.git/codex_write_test': Read-only file system); reported as a regression from v0.115.0.
  • #14338 — writable gitdir for the current worktree in workspace-write.
  • #13635 / #14261 — bwrap: Can't mkdir .codex/memories: same failure mode (a sandbox-setup mkdir hard-fails), different path.

Mine is the ".git does not exist at all" corner: setup tries to create it in a directory the user cannot write to. Gating the whole protection on "is this root actually a git repository, and is its gitdir a plain directory?" — and degrading rather than aborting when it is not — would likely cover #27354 and this one together.

Not a duplicate of #31888, which is the Windows sandbox helper failing with EPERM on %USERPROFILE% — different platform, different code path, no bubblewrap involved.

Happy to provide an strace of the bwrap invocation if useful.

View original on GitHub ↗