Skip to content
Agentic Control Plane

How to stop Codex from deleting your files

David Crowe David Crowe · · 10 min read
codex permissions hooks defense-in-depth
Share X HN LinkedIn

Three things, in this order. Keep the sandbox on, add rules that prompt on the delete commands, and commit before each task:

# ~/.codex/config.toml
sandbox_mode = "workspace-write"
approval_policy = "on-request"
# ~/.codex/rules/default.rules
prefix_rule(pattern = ["rm"], decision = "prompt", justification = "Deletes files")
prefix_rule(pattern = ["git", "clean"], decision = "prompt", justification = "Deletes untracked files")
prefix_rule(pattern = ["git", "reset", "--hard"], decision = "prompt", justification = "Discards uncommitted work")

The sandbox keeps a delete from reaching outside the project. It does not stop a delete inside it, and that is where most lost work is.

Sandbox, rules and git

In workspace-write mode, writes are limited to the current directory and temp directories such as /tmp. Inside the workspace, .git, .codex and .agents are read-only, so the agent cannot delete your git history from under you. Everything else in the project is writable (approvals and security docs). --yolo turns the sandbox and the approvals off together.

Rules are prefix_rule() entries in files under ~/.codex/rules/. pattern is the command prefix, and decision is allow, prompt or forbidden. When several rules match, the strictest wins. Codex splits a simple chain joined by &&, ||, ; or | and checks each part, so allowing git add does not approve git add . && rm -rf /. Rules take effect after a restart, and OpenAI marks them experimental (rules docs).

Test a rule before you trust it:

codex execpolicy check --pretty --rules ~/.codex/rules/default.rules -- rm -rf build

For recovery, OpenAI’s own advice is to work on a clean branch and commit often. A committed file that Codex deletes is one git checkout away. An uncommitted one is gone.

A hook narrower than the rm rule

A rule on rm prompts for every rm, including rm -rf node_modules. A hook can be narrower. This one blocks a recursive delete only when the target is outside the project. Save it as ~/.codex/hooks/no-rm-outside.mjs:

#!/usr/bin/env node
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
  let cmd = "";
  try { cmd = String(JSON.parse(raw).tool_input?.command ?? ""); } catch {}
  for (const m of cmd.matchAll(/(?:^|[;&|(]\s*|\bsudo\s+)rm\s+([^;&|\n]*)/g)) {
    const words = m[1].trim().split(/\s+/);
    const recursive = words.some((w) => /^-[a-zA-Z]*[rR]/.test(w) || w === "--recursive");
    const targets = words.filter((w) => !w.startsWith("-")).map((w) => w.replace(/^["']/, ""));
    const outside = targets.some((t) => /^(\/|~|\$|\.\.)/.test(t) || t.includes("/../"));
    if (recursive && outside) {
      console.error("Blocked: recursive delete outside the project. Stop and ask the human.");
      process.exit(2);
    }
  }
});

Register it in ~/.codex/hooks.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node \"$HOME/.codex/hooks/no-rm-outside.mjs\"" }
        ]
      }
    ]
  }
}

Then run /hooks inside Codex and trust it. A hook you have not reviewed there does not run. Exit code 2 blocks the call and the reason on stderr goes to the model (hooks docs).

The same docs say a PreToolUse error, timeout or malformed response can let the tool run, and we found no setting to make a failed hook block. OpenAI also describes tool hooks as “a useful guardrail, not a complete enforcement boundary.”

Deletes only the sandbox or a commit covers

The script’s output for fourteen commands:

BLOCKED  rm -rf ~/projects/old
BLOCKED  rm -rf /var/www/html
BLOCKED  cd build && rm -rf ../data
BLOCKED  rm -rf $BUILD_DIR
BLOCKED  sudo rm -r /opt/app
ran      rm -rf node_modules
ran      rm -rf ./dist
ran      rm notes.txt
ran      find ~/projects -name '*.log' -delete
ran      python3 -c "import shutil; shutil.rmtree('/srv/data')"
ran      bash -c 'rm -rf ~/projects/old'
ran      /bin/rm -rf ~/projects/old
ran      git clean -fdx
ran      rm -rf src

find -delete and shutil.rmtree never use rm, so neither the hook nor an rm rule sees them. With the sandbox on, those two still cannot write outside the workspace, which is the reason to keep it on. rm -rf src is inside the project, so the sandbox and the hook both allow it. Only the rm rule prompts on it, and only a commit gets the files back.

File edits go through apply_patch, not the shell, so a shell rule does not cover them. A PreToolUse hook does fire for apply_patch (hooks docs), so a hook matched on it can check the patch.

ACP’s Codex hook

ACP installs a Codex hook like the one above and keeps it current. A recursive delete of /, your home directory or a system directory is always blocked. A recursive delete of any other literal path outside the working directory, temp directories such as /tmp excepted, is blocked until you approve it on the console or from the email, and then you ask Codex to retry. Its check also matches /bin/rm and reads the command inside bash -c '...'.

It does not catch find -delete or shutil.rmtree either. A delete inside the project is recorded, not held. What you get for those is the log: every shell, apply_patch and MCP call Codex made, in order, so “what did it delete” has an answer.

Frequently asked questions

Codex deleted my files. How do I prevent it?

Run Codex in the workspace-write sandbox so writes are limited to the project and temp directories, add rules in ~/.codex/rules/default.rules that prompt on rm, git clean and git reset –hard, and commit before each task so a delete inside the project can be undone with git.

Does the Codex sandbox stop deletes inside my project?

No. In workspace-write mode Codex can edit files and run commands in the working directory without asking. The sandbox limits where writes land. Inside the workspace it keeps .git, .codex and .agents read-only. Your source files are writable.

What does a Codex rule with decision forbidden do?

It blocks the matching command without prompting. prompt asks before each matching call, and allow runs it without asking. When several rules match, the most restrictive one wins: forbidden, then prompt, then allow.

Can a Codex hook ask me before a delete?

Not today. OpenAI’s hooks docs say permissionDecision ask is parsed but not supported yet. A PreToolUse hook can deny. For a prompt, use a rule with decision prompt.

Share X HN LinkedIn
Get the next data drop
What agents actually cost, new tool-surface captures, and the occasional incident post-mortem — sent when we publish something worth your inbox, not on a schedule. Unsubscribe anytime.
Share: Twitter LinkedIn
Related posts

← back to blog