Data loss: subagent used Path() as cleanup sentinel and shutil.rmtree(".") deleted repository
What version of Codex CLI is running?
The incident rollout records codex-cli 0.149.0. The currently installed version is 0.149.1.
What subscription do you have?
ChatGPT Pro (personal account).
Which model were you using?
The lead and delegated builder were both gpt-5.6-sol. The delegated builder inherited ultra reasoning effort.
What platform is your computer?
Microsoft Windows NT 10.0.26200.0 x64
WSL 2.7.11.0
Ubuntu under WSL2, ext4 virtual disk
What terminal emulator and version are you using (if applicable)?
Unattended Codex worker/app-server run. Native Windows PowerShell host invoked commands in Ubuntu through wsl.exe. The affected repository was inside WSL.
Codex doctor report
No contemporaneous doctor report was captured. Preserved incident metadata records:
{
"codex_cli": "0.149.0",
"model": "gpt-5.6-sol",
"reasoning_effort": "ultra",
"agent": "delegated subagent",
"sandbox_mode": "danger-full-access",
"approval_policy": "never",
"permission_profile": "disabled",
"host": "Windows x64",
"target_filesystem": "Ubuntu WSL2 ext4"
}
What issue are you seeing?
Summary
A delegated Codex builder used pathlib.Path() as an absent-path sentinel after atomically installing generated output. In Python, Path() denotes . rather than a null or empty path. Its finally block therefore called shutil.rmtree('.') from the live repository root and recursively deleted the checkout, including .git, tracked files, ignored build directories, and unique ignored research artifacts.
The user did not request repository cleanup or deletion. The delegated task was to build and verify new mathematical certificate artifacts in the shared repository.
Incident-producing code
The builder generated and ran this cleanup shape:
def build(output_dir: Path, parent_dir: Path) -> dict:
stage = Path(tempfile.mkdtemp(
prefix=f".{output_dir.name}.stage-",
dir=output_dir.parent,
))
try:
# Generate files into stage.
# Atomically move stage or its files into output_dir.
install_stage(stage, output_dir, mode)
# Intended to mean: the stage no longer exists and needs no cleanup.
stage = Path()
return census
finally:
if stage and stage.exists():
shutil.rmtree(stage)
The relevant Python semantics are:
Path() == Path(".")
bool(Path()) is True
Path().exists() is True # when the current directory exists
Consequently, the finally block evaluated the sentinel as an existing path and recursively deleted .. shutil.rmtree() removed the repository contents before failing when it could not remove the process's current working directory itself.
Impact
- The complete tracked checkout and
.gitdirectory were deleted. - The delegated agent immediately recloned the private origin into the same path, restoring tracked state but potentially overwriting deleted ext4 blocks before recovery could begin.
- Ignored
build/,build-clang/, andbuild-sanitize/directories were lost but are reproducible. - Ignored
artifacts/was also lost. The tracked project scratchpad documented an approximately 104 MB local checkpoint tree plus resumable sweep JSONL files. Those artifacts were not in Git and remain missing. - No complete pre-deletion inventory existed, so the full unique-data loss cannot be certified.
- Newly authored builder/checker drafts survived in Codex rollout records and can be reconstructed.
- There is no evidence that the recursive deletion escaped the repository root. The damage was nevertheless repository-wide.
The incident occurred on 2026-08-26 at approximately 06:28 America/New_York.
Why this is a Codex safety issue rather than only a Python mistake
The Python bug is straightforward, but a single sentinel mistake in newly generated code was able to destroy a live repository without any independent blast-radius interlock. Command-prefix detection for rm, rmdir, or Remove-Item cannot catch this case because the visible command was an ordinary Python generator invocation; the destructive operation occurred inside shutil.rmtree().
The delegated agent used the highest reasoning setting. This incident therefore should not be mitigated solely by asking the model to reason more carefully.
What steps can reproduce the bug?
Use only a disposable directory. This reproducer intentionally deletes the contents of a temporary synthetic repository, never a real workspace:
from pathlib import Path
import subprocess
import sys
import tempfile
child = r'''
from pathlib import Path
import shutil
stage = Path("synthetic-stage")
stage.mkdir()
(stage / "generated.txt").write_text("generated")
# Simulate successful atomic installation followed by the faulty sentinel.
stage = Path()
if stage and stage.exists():
shutil.rmtree(stage)
'''
with tempfile.TemporaryDirectory(prefix="codex-path-sentinel-repro-") as temp:
repo = Path(temp) / "synthetic-repo"
repo.mkdir()
(repo / ".git").mkdir()
(repo / ".git" / "HEAD").write_text("ref: refs/heads/main\n")
(repo / "tracked.txt").write_text("important")
(repo / "ignored-checkpoint.bin").write_bytes(b"checkpoint")
subprocess.run([sys.executable, "-c", child], cwd=repo, check=False)
assert not (repo / "tracked.txt").exists()
assert not (repo / "ignored-checkpoint.bin").exists()
The minimal semantic trigger is:
stage = Path()
if stage and stage.exists():
shutil.rmtree(stage)
Do not run that minimal fragment from a real directory.
What is the expected behavior?
At least one independent guard should prevent a generated program's cleanup mistake from deleting a live repository:
- Cleanup-capable code authored by a delegated worker should be first-executed in a disposable worktree/clone unless the parent explicitly selects the shared live checkout.
- Delegated agents should support a narrower or disposable filesystem boundary independent of the lead agent's permissions.
- The parent should receive visible live notice of nested-agent destructive execution and have an interruption path.
- Repository roots, workspace roots, user-profile roots, filesystem roots, and paths containing
.gitshould have a non-bypassable bulk-deletion guard or recovery checkpoint, including when deletion occurs inside Python/Node/compiled code rather than a recognizable shell command. - Model-side generation guidance should prohibit
Path()/Path("")as an absent-path sentinel and requirePath | Noneplus canonical parent/name validation before recursive cleanup. - Recursive cleanup helpers should reject
.,.., empty/unresolved values, the current working directory, and the expected parent itself; first-run tests should exercise those negative cases against disposable fixtures.
The key safety property is that a language-level path mistake must not be able to escalate directly into irreversible repository-wide loss merely because the outer command was an allowed Python invocation.
Additional information
Closest related reports found before filing:
- #33557 — delegated subagent
TemporaryDirectorycleanup traversed a bind mount and deleted the host repository; same Python recursive-cleanup/subagent family, different path mechanism. - #32684 — PowerShell
$homecollision left$HOMEas the recursive-deletion target; very similar mistaken-sentinel/computed-path expansion, different language. - #37998 — delegated subagent used
git clean -fXfor one ignored file and deleted its entire ignored ancestor directory. - #35707 — an incorrectly assumed PowerShell recursive filter expanded cleanup to most of a Git repository.
- #33624 — requests a hard confirmation/recovery gate for bulk deletion even in Full Access.
I searched for Path(), shutil.rmtree, recursive deletion, subagent cleanup, and repository data loss. I did not find an existing report for this exact Path()-as-null-sentinel mechanism.
The full private rollout contains the generated source, tool call, timestamps, subsequent admission, and restoration checks. It can be provided through a private support channel with local paths and private project context redacted.
2 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
While similar, not a duplicate, as the destructive command in my case did not contain rm/rmdir/Remove-Item/etc.