Linux sandbox leaks empty .git/.agents/.codex dirs into /tmp when the helper is killed, and later runs never reclaim them
What version of Codex CLI is running?
codex-cli 0.143.0
What subscription do you have?
ChatGPT Pro (OAuth)
Which model were you using?
gpt-5.5
What platform is your computer?
Ubuntu 24.04, x86_64, kernel 6.17
What terminal emulator and version are you using (if applicable)?
Not applicable, codex exec invoked non-interactively by an agent harness.
Codex doctor report
Omitted intentionally. This is not an install, config, or auth issue; the behavior is root-caused in source below and reproduces on a healthy install.
What issue are you seeing?
Empty .git, .agents, and .codex directories appear at the top of /tmp and never go away. A stray empty /tmp/.git breaks Go builds in any git worktree under /tmp: with -buildvcs=auto, cmd/go walks up past the worktree's .git file, finds /tmp/.git, treats /tmp as the repository root, and fails with error obtaining VCS status: exit status 128. It also confuses git repository discovery for anything else running under /tmp.
Because the directories only leak when a sandboxed run dies, they reappear at unpredictable times (in my case 00:03 and 04:02 on the same night). It took an evening of forensics to trace them back to Codex; until then every scheduled job on the machine was a suspect.
Root cause, from reading codex-rs/linux-sandbox:
- Under
workspace-write,/tmpis a writable root (exclude_slash_tmpdefaults to false). For each writable root, missing protected metadata paths (.git,.agents,.codex) are masked with--perms 555 --tmpfs <path> --remount-ro <path>(append_missing_read_only_subpath_argsinbwrap.rs). Since the writable root is bind-mounted from the host, creating the mount point materializes a real empty directory on the host, e.g./tmp/.git. - These are registered as synthetic mount targets and removed during cleanup (
cleanup_synthetic_mount_targetsinlinux_run_main.rs). If the helper dies before cleanup (SIGKILL, OOM kill, crash), the directories persist on the host and the marker file in the registry ($TMPDIR/codex-bwrap-synthetic-mount-targets-<uid>/<hash>/<pid>) goes stale. - The leak is then permanent. On the next run over the same path,
register_synthetic_mount_targetsonly adopts an existing path as synthetic when the registry shows an active owner (synthetic_mount_marker_dir_has_active_synthetic_owner). The stale marker from the dead process, which is the only evidence the path is a leaked mount point rather than a real pre-existing directory, is deleted during the scan without being acted on. Every subsequent run then preserves the leaked directory as pre-existing, forever.
What steps can reproduce the bug?
- In a directory under
/tmp(a git worktree matches the real-world case), start a run:codex exec --skip-git-repo-check -s workspace-write "run: sleep 60". - While the command is running, confirm
/tmp/.git,/tmp/.agents,/tmp/.codexexist as read-only tmpfs mounts inside the sandbox and as directories on the host. kill -9the sandbox helper process mid-command.- Observe the three empty directories left behind in
/tmp. - Run any later
codex exec -s workspace-writecommand and observe they are never cleaned up. - In any git worktree under
/tmp, rungo build ./...on a module and observeerror obtaining VCS status: exit status 128.
On a clean exit the directories are removed correctly; only a killed helper leaks them, which is what makes the recurrence look random.
What is the expected behavior?
Sandbox runs should not leave permanent artifacts in /tmp (or any writable root). At minimum, a later run that encounters a leaked synthetic mount point should reclaim it instead of promoting it to a pre-existing path.
Additional information
I have a tested fix and am happy to open a PR if you want it, per the invitation flow in docs/contributing.md. Outline: during registration, count stale synthetic markers (dead pid, synthetic contents) as ownership evidence, so the next run over the same path adopts the leaked mount point and removes it in its own cleanup. Markers recorded as existing still preserve the path, and cleanup still refuses to remove non-empty directories and non-empty files, so a real repository, a worktree .git file, or user data is never touched. The change is contained to linux_run_main.rs plus two tests in linux_run_main_tests.rs (leaked empty dir with a stale synthetic marker is adopted and removed; pre-existing path with a stale existing marker is preserved). cargo test -p codex-linux-sandbox passes (94 tests), fmt and clippy clean.
Related, possibly the same family: #31225 reports the Windows Desktop app creating an empty .git directory, and #18918 covers the Windows ACL flavor of metadata protection in writable roots.
3 Comments
Warning for anyone reading: the comment above from wekaromodenu41 is not a fix. It is a throwaway account linking a zip hosted on an unrelated repository (mavagarace9/hotfix_1.3.9), which matches the fake-hotfix malware campaign currently hitting GitHub issue threads. Do not download or run it. The real fix requires a source change in codex-rs/linux-sandbox as described in the issue body; there is no legitimate downloadable patch for this.
Completely unacceptable for a sandbox wrapper to be creating any folders that do not already exist in a project.
Confirming this is still present at codex-cli 0.149.0 (npm
@openai/codex, musl worker binary), a month after it was filed here against 0.143.0. Rediscovered independently on WSL2 Ubuntu / ext4/tmpbefore finding this issue, so the repro below is derived separately rather than replayed from the report — and it adds a second real-world damage case that is not the Go-buildvcsone.Live bwrap argv, captured from the host process table mid-exec (local paths genericized; the trailing entries repeat the same triple for the cwd writable root):
Independent reproduction (2026-08-24, all observed)
statpoll aroundcodex exec --sandbox workspace-write --skip-git-repo-check ... 'run true'— three present-to-absent pulses of/tmp/.git, 6-84 ms each, one per sandboxed exec:``
``19:30:57.629306593 present
19:30:57.635757945 absent
19:31:03.115295870 present
19:31:03.127470349 absent
19:31:06.287685830 present
19:31:06.371694131 absent
sleep 6holds the window open for the command's whole duration:stat -c %w /tmp/.gitgives birth2026-08-24 19:31:36.091789600 -0500, owned by the invoking user,drwxr-xr-x, empty.kill -9on the musl worker inside that window: worker dead,/tmp/.git(birth 19:34:56) persisted — re-checked at +3 s and +11 s. Exactly the "helper dies before cleanup" path.--sandbox read-onlyexec — zero transitions. No writable roots, nothing to mask./tmp/.gitat 13:06 and again at 15:18, with sibling/tmp/.agentsand/tmp/.codex(both born 13:07) still sitting there ~6 h later until removed by hand — the "never reclaimed" half, in the wild.Second blast-radius case: any upward-walking
.gitdiscovery under /tmpAn internal Node tool resolves its repository root by walking up for
.git, then refuses writes outside a frozen corpus. With an ambient/tmp/.gitpresent, the walk terminated at/tmp,/tmpresolved as the repository root, and the guard threw — 219 failing assertions across 10 test files, none of them anywhere near a frozen tree, recurring nondeterministically for days.Cost before attribution: a 127-file per-file test bisect, several red suite runs, and a multi-session forensics arc. An earlier 50 ms watcher had caught the transient existing for exactly one sample and it was written off as sampling noise.
So the victim class generalizes past
cmd/go: any upward-walking.gitdiscovery rooted under/tmp. That is what makes this worse than a cosmetic leak — the failure surfaces in an unrelated tool, far from Codex, minutes to hours after the run that caused it, and it is nondeterministic by construction. Attribution is the expensive part, and every affected machine pays it separately.On the offered fix
Endorsing the outline as written — counting stale synthetic markers with a dead pid as ownership evidence, with
existingmarkers still preserving real paths. From the outside it looks like the only rung that holds the information needed to distinguish a leaked mount point from a genuinely pre-existing directory, and it is precisely where that evidence is currently discarded during the scan. Happy to exercise a PR against 0.149.0 on WSL2 if a second platform data point is useful.