Skip to content

Data loss: subagent used Path() as cleanup sentinel and shutil.rmtree(".") deleted repository #40995

Open

Description

@rahidz

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 .git directory 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/, and build-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:

  1. 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.
  2. Delegated agents should support a narrower or disposable filesystem boundary independent of the lead agent's permissions.
  3. The parent should receive visible live notice of nested-agent destructive execution and have an interruption path.
  4. Repository roots, workspace roots, user-profile roots, filesystem roots, and paths containing .git should 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.
  5. Model-side generation guidance should prohibit Path()/Path("") as an absent-path sentinel and require Path | None plus canonical parent/name validation before recursive cleanup.
  6. 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:

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.

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    windows-osIssues related to Codex on Windows systems
    subagentIssues involving subagents or multi-agent features
    sandboxIssues related to permissions or sandboxing
    on Aug 26, 2026
  2. github-actions commented on Aug 26, 2026

    @github-actions
    Contributor

    Potential duplicates detected. Please review them and close your issue if it is a duplicate.

    Powered by Codex Action

  3. rahidz commented on Aug 26, 2026

    @rahidz
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIbugSomething isn't workingsandboxIssues related to permissions or sandboxingsubagentIssues involving subagents or multi-agent featureswindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions