# Claude Code Deleted My Files? The Layer That Stops It

A Claude Code delete guard is four layers that stop an agent deleting your files: an approval prompt, a deny rule, a PreToolUse hook and work that is committed and pushed. ConvOps adds isolated runs and a sign-off before merge for the runs nobody watches.

By David Marsa, Founder & CEO. Updated 2026-10-09.

## What you will be able to do

- Know what comes back after a delete (committed work, archived files) and what does not.
- Add a deny block to Claude Code settings and know which delete shapes it misses.
- Write a PreToolUse hook that blocks deletes and tells the agent what to do instead, then test it with sample JSON.
- Decide which layers protect your unattended runs, given that they skip prompts.
- Apply the same guard shape in OpenCode and Kimi Code CLI.

## Key takeaways

- To stop Claude Code deleting your files, block deletes with a deny rule and a PreToolUse hook, and commit and push so a missed delete has a way back.
- In our scratch runs under --dangerously-skip-permissions, a Claude Code deny rule and a PreToolUse hook each blocked rm; approval prompts never appear in unattended runs.
- Deny rules match command text, so find -delete, /bin/rm and a Python one-liner slipped past them; our hook caught the first two and missed the third, because a hook blocks only what its patterns name.
- A block message that names the safe alternative (move to the project's _archive/ folder) keeps the agent from improvising around the block.
- A text-matching hook over-blocks harmless commands; test it with fixtures and accept the false positive over a missed delete.

If Claude Code deleted your files, only what exists somewhere else comes back. To stop Claude Code deleting files again, push your work, then add a deny rule and a hook. This guide sets up every layer for [Claude Code](/directory/claude-code) in about 30 minutes and shows what each one misses.

We run a delete guard on every Claude Code session in our own repository. The examples use Marrowby Tiles, a made-up tile shop; the rules and patterns mirror ours.

## Claude Code Deleted Files: What Comes Back

The way back is built before the delete, never after.

| Comes back | Stays gone |
|---|---|
| Committed files (local or pushed); only pushed commits survive a lost or wiped folder | Uncommitted files removed by `rm`, `find -delete` or a script |
| Files a hook redirected into `_archive/` instead of deleting | Uncommitted changes wiped by `git checkout --`, `git reset --hard` or `git restore` |

Lost files just now? Run these in order:

1. Look in the project's `_archive/` folder.
2. Run `git status` to list missing tracked files.
3. Committed but not pushed? `git restore --source=HEAD --staged --worktree <path>` brings it back from your last local commit.
4. Run `git fetch`, then `git restore --source=origin/main <path>` to get a file back as last pushed (swap in your branch name).
5. If a commit removed the file, `git log --diff-filter=D -- <path>` names that commit.

Anything these miss is gone from disk.

## Why Claude Code Deletes Files

A prompt protects the person at the keyboard. It does nothing for a run at 3 a.m.

Agents delete for ordinary reasons: asked to "start fresh", Claude Code clears the folder first, and in our scratch test it chose `find -delete` on its own. Git deletes too: `git reset --hard`, `git clean`, `git restore`, `git checkout --` and `git stash drop` throw away uncommitted work, and a plain `git stash` hides it until you pop it.

The approval prompt is the weakest layer. People approve what they skim, and our scheduled agent runs use `--dangerously-skip-permissions`, so no prompt appears.

| Metric | Value | Source |
|---|---|---|
| Dangerous commands caught by human reviewers in a controlled study | 13.6% | [Anthropic](https://claude.com/resources/articles/auto-mode-default-in-claude-code), 2026 |
| Dangerous commands caught by Claude Code's auto mode classifier, same study | 89% | [Anthropic](https://claude.com/resources/articles/auto-mode-default-in-claude-code), 2026 |
| Files Claude Code allegedly deleted for one developer | 48,000 in 103 seconds | [TechRadar](https://www.techradar.com/ai-platforms-assistants/do-not-let-programs-run-commands-on-your-machine-claude-code-allegedly-deleted-48-000-files-in-103-seconds-and-its-a-terrifying-warning-about-ai-agents), 2026 |

[See how ConvOps puts a sign-off step in front of every merge](https://convops.app), so a delete made during an unattended run never reaches main without a person's yes.

## Which Layer Actually Stops a Delete

The deny list is a speed bump. The hook is a wall with known gaps. Pushed work is the floor.

**What is a deny rule?** A deny rule is a line in Claude Code's settings that refuses any Bash command whose text matches a pattern, such as `Bash(rm *)`.

**What is a PreToolUse hook?** A PreToolUse hook is a script Claude Code runs before a tool call; when the script exits with code 2, the call is blocked and the agent reads the script's message.

![Four layers sit between an agent's delete and your files, and only the approval prompt is skipped when nobody is watching.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/stop-claude-code-deleting-files-v1-layers.png)

A deny rule and a hook each blocked `rm` alone in our scratch runs. Neither stopped everything.

![Each layer lets something through on its own, a find delete past the deny rule and a Python one-liner past the hook, and the next layer covers it.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/stop-claude-code-deleting-files-v2-fails.png)

## Commit and Push Before an Agent Touches Anything

Unpushed work dies with the folder that holds it.

Before you start Claude Code, save everything to the remote:

```bash
git status
git add src/ data/products.json
git commit -m "Save work before the agent run"
git push
```

Start the agent only on a clean tree that is up to date with the remote. Our workflows push each step's output before advancing, and a stop hook blocks ending a session with unpushed work. Run risky work in a git worktree, such as `~/marrowby-shop-run-0412`, or in an isolated run that starts from the remote.

## Add Deny Rules to Claude Code

Deny rules match the command text, not what the command does.

This block goes in the project's `.claude/settings.json`:

```json
{
  "permissions": {
    "deny": [
      "Bash(rm *)",
      "Bash(rmdir *)",
      "Bash(git rm *)",
      "Bash(git clean *)",
      "Bash(git reset *)",
      "Bash(git restore *)",
      "Bash(git checkout -- *)",
      "Bash(git stash drop *)",
      "Bash(git stash clear *)",
      "Bash(git push --force *)",
      "Bash(git push -f *)"
    ]
  }
}
```

`Bash(rm *)` and the older `Bash(rm:*)` behaved the same on 2.1.295, even under `--dangerously-skip-permissions`. Anthropic's [permission modes docs](https://code.claude.com/docs/en/permission-modes) state that deny rules "block in every mode, including `bypassPermissions`", and list [auto mode](/news/claude-code-auto-mode-default-aug-2026) as one of those modes. The [permissions docs](https://code.claude.com/docs/en/permissions) also say a deny rule "isn't a security boundary around the program". In our run, `find -delete`, `/bin/rm` and a `python3 -c` one-liner each deleted the file; `sh -c 'rm ...'` was caught.

## Add a Hook That Blocks and Offers a Way Out

A block without an alternative teaches the agent to route around you.

The full `.claude/settings.json`, with a `hooks` key next to `permissions` ([hooks guide](https://code.claude.com/docs/en/hooks-guide)):

```json
{
  "permissions": {
    "deny": [
      "Bash(rm *)",
      "Bash(rmdir *)",
      "Bash(git rm *)",
      "Bash(git clean *)",
      "Bash(git reset *)",
      "Bash(git restore *)",
      "Bash(git checkout -- *)",
      "Bash(git stash drop *)",
      "Bash(git stash clear *)",
      "Bash(git push --force *)",
      "Bash(git push -f *)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/delete-guard.py"],
            "timeout": 5
          }
        ]
      }
    ]
  }
}
```

`${CLAUDE_PROJECT_DIR}` points at the project root, so the path holds after the agent changes folder. A wrong script path makes `python3` exit 2, so every Bash call is blocked with a `can't open file` error. That fails closed, and the first command shows it.

And the script, `.claude/hooks/delete-guard.py`:

```python
import json, re, sys

ARCHIVE = ("file deletion is denied, archive instead: move it to _archive/ "
           "in this project (mkdir -p _archive && mv <path> _archive/)")
PATTERNS = [
    (r"\brm\s+-rf\b", "rm -rf is irreversible recursive deletion, " + ARCHIVE),
    (r"(?<!-)\brm\b", ARCHIVE),
    (r"\brmdir\b", ARCHIVE),
    (r"\bfind\b.*\s-delete\b", ARCHIVE),
    (r"\bgit\s+checkout\s+--\s+", "git checkout -- reverts uncommitted work"),
    (r"\bgit\s+reset\b", "git reset can destroy commit history"),
    (r"\bgit\s+restore\b", "git restore reverts uncommitted work"),
    (r"\bgit\s+clean\b", "git clean deletes untracked files"),
    (r"\bgit\s+stash\b", "git stash can drop uncommitted work"),
    (r"\bgit\s+push\b.*\s(?:-f|--force)\b", "force push overwrites remote history"),
]
MESSAGE_ARG = re.compile(r"""(?:^|\s)(?:-m|--message)(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)""")

def main():
    try:
        command = json.load(sys.stdin)["tool_input"]["command"]
    except Exception:
        return 0
    scanned = MESSAGE_ARG.sub(" ", command)
    for pattern, reason in PATTERNS:
        if re.search(pattern, scanned):
            print(f"BLOCKED: Destructive command detected.\n  Reason: {reason}\n"
                  f"  Command: {command}", file=sys.stderr)
            return 2
    return 0

sys.exit(main())
```

Every line has a reason:

- **Exit code 2** blocks the call; the agent reads stderr.
- **`(?<!-)\brm\b`** catches any `rm` token, `/bin/rm` included. The lookbehind keeps `docker run --rm` allowed.
- **Unparseable input exits 0**: it never jams a session, and it goes unchecked.
- **No interpreter pattern.** `os.remove` and `shutil.rmtree` one-liners pass: the hook blocks only what its patterns name.

A hook runs on every Bash call, whatever the agent was told ([graph engineering](/learn/guides/what-is-graph-engineering)). That is why a rule like this belongs in a hook, not in a [CLAUDE.md that keeps growing](/learn/guides/claude-md-too-long).

## Test the Hook Before You Trust It

An untested regex is a guess.

Pipe a sample payload into the script:

```bash
echo '{"tool_name":"Bash","tool_input":{"command":"find exports -mindepth 1 -delete"}}' \
  | python3 .claude/hooks/delete-guard.py; echo "exit $?"
```

Add one fixture per shape, including expected misses:

| Command | Expected |
|---|---|
| `rm -rf exports/` | exit 2 |
| `git checkout -- src/` | exit 2 |
| `docker run --rm alpine true` | exit 0 |
| `git commit -m "Never rm -rf exports, archive instead"` | exit 0 |
| `python3 -c "import os; os.remove('exports/product-feed.csv')"` | exit 0, a known miss |
| `git push -f origin main` | exit 2 |
| `grep -rn "rm -rf" scripts/` | exit 2, a known over-block |

Finish with one live block: in a scratch folder, ask Claude Code to delete a throwaway file.

## One Delete Attempt, Traced

The best block is the one the agent recovers from on its own.

![The hook blocks the agent's find delete with exit 2, its message names the project's _archive/ folder, and the agent moves the files there instead, so nothing is deleted.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/stop-claude-code-deleting-files-v3-trace.png)

1. **Trigger.** Marrowby Tiles asks: "Regenerate the product feed export, start fresh." The agent runs `find exports -mindepth 1 -delete` (callout 1).
2. **Block.** The hook matches `\bfind\b.*\s-delete\b` and exits 2; its message names `_archive/` in this project (callout 2).
3. **Redirect.** The agent runs `mkdir -p _archive && mv exports/* _archive/` (callout 3).
4. **Verdict.** 2 items moved, 0 deleted: `_archive/product-feed.csv` and `_archive/old/` (callout 4).

## What Goes Wrong With a Delete Guard

We would rather explain a false block than restore a folder.

![A read-only search for "rm -rf" is blocked because the hook matches text, and we keep that false positive on purpose: a false block beats a missed delete.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/stop-claude-code-deleting-files-v4-overblock.png)

Four failures we hit, each with its rule:

- **A search was blocked.** A read-only `grep -rn "rm -rf" scripts/` matched. Rule: keep the false positive (fail closed) and its fixture.
- **Commit messages fired the hook.** "Never rm -rf exports" matched like a command. Rule: strip quoted `-m` and `--message` arguments first.
- **The archive path left the project.** Our block message named a folder at our repository root, and a test agent in another project moved its files there. Rule: name `_archive/` in this project.
- **A git command wiped work.** `git checkout --` on two folders erased uncommitted work. Rule: deny and hook git's delete commands too.

## When Nobody Is Watching

Prompts are not a control for unattended runs. Gates and isolation are.

Our unattended runs skip prompts by design. Three controls carry them:

| Control | What it does | What it does not do |
|---|---|---|
| Deny rules and the hook | Block the delete shapes they name in local runs | Catch shapes they do not name |
| Isolated run | Runs each job in its own environment, such as `marrowby-run`, from pushed commits, so a delete cannot reach your copy | Stop a command inside the run |
| Sign-off step before merge | A person approves before a change, a delete included, reaches main | Stop a command during the run |

Our [workflows stop for a sign-off](/learn/guides/run-a-company-with-ai-agents) where the risk is ([human in the loop](/blog/human-in-the-loop-ai-systems)), and our [site repair loop](/learn/guides/self-healing-website-ai-agent-qa-fix-loop) runs in an isolated environment.

Copy the deny block and the hook today. Then [run your unattended agent jobs as ConvOps tasks](https://my.convops.app/register), with a sign-off step before merge and an isolated run.

## The Same Guard in OpenCode and Kimi Code CLI

One guard shape, three files.

We tested this guard in scratch projects; our day-to-day guard runs in Claude Code.

![The same pre-run delete check lives in each tool's own file: a PreToolUse hook in Claude Code, a tool.execute.before plugin in OpenCode, and a hooks entry in Kimi Code CLI.](https://storage.googleapis.com/neomanex-public-assets/neomanex/guides/stop-claude-code-deleting-files-v5-tools-v2.png)

### OpenCode 1.18.31

In [OpenCode](/directory/opencode), deny rules in `permission.bash` stopped `rm` and `find -delete`, then the agent deleted with a Python one-liner. A plugin, `.opencode/plugins/delete-guard.js`, checks every `bash` command in `tool.execute.before`, with `os.remove` and `shutil.rmtree` among its patterns, and throws a message naming `_archive/`. It blocked all six shapes we tried, and the agent archived instead. `opencode run --pure` skips plugins, the guard included.

### Kimi Code CLI 0.31.1

Kimi Code CLI (it runs models such as [Kimi K3](/models/kimi-k3)) takes a PreToolUse hook in `~/.kimi-code/config.toml`:

```toml
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
command = "node ~/.kimi-code/hooks/delete-guard.mjs"
```

The script exits 2 with the same message and, under yolo, blocked all six shapes. Hooks live only in the user config, never in the project. The native rule `Bash(rm *)` never matched a path with a slash; `Bash(rm */**)` does.

Whichever tool you run, the order is the same: push, deny, hook, test.

## Steps

1. **Commit and push your work**: Run git status, commit everything and push before you start Claude Code. Run risky work in a git worktree or an isolated run that starts from the remote. Deliverable: nothing you care about exists only on your disk.
2. **Add deny rules to Claude Code**: Paste a permissions.deny block into the project's .claude/settings.json covering rm, rmdir, git rm, git clean, git reset, git restore, git checkout on a path, git stash drop and clear, and force push. Deliverable: Claude Code refuses those commands, even under --dangerously-skip-permissions.
3. **Add a PreToolUse hook that names the safe path**: Save delete-guard.py in .claude/hooks/ and wire it as a PreToolUse hook with the Bash matcher. Deliverable: a matching command exits 2 and the agent reads a message telling it to move the files to _archive/ in this project.
4. **Test the hook with sample JSON**: Pipe a sample payload for each delete shape into the script and check the exit code: 2 for deletes, 0 for docker run --rm and for commit messages. Then trigger one live block in a scratch folder. Deliverable: a fixture list that also records the known miss and the known over-block.
5. **Isolate unattended runs**: Run scheduled agent jobs in an isolated environment that starts from pushed commits, with a sign-off step before merge. Deliverable: a delete during a run reaches neither your copy nor main without a person's yes. Create a free ConvOps account at https://my.convops.app/register to run them as tasks.

## FAQ

### What stops Claude Code from deleting files?

Four layers stop Claude Code from deleting files: an approval prompt, a deny rule in .claude/settings.json, a PreToolUse hook that checks every Bash call, and a way back made of committed, pushed work and isolated runs. Only the last three work in unattended runs, which skip prompts. A deny rule matches command text, so find with -delete and /bin/rm slip past it, and a hook blocks only the patterns it names.

### Who needs to stop Claude Code from deleting files?

Developers and teams who run Claude Code on real repositories need a delete guard, most of all when agent jobs run unattended or on a schedule. The outcome is that a delete is blocked, or the work still exists on the remote. ConvOps runs those jobs as tasks with an isolated run and a sign-off step before merge, so a delete made during a run reaches neither your local copy nor main without a person's yes.

### Did Claude delete files?

To tell whether Claude Code deleted files, read the session's Bash tool calls for delete shapes: rm, rmdir, find with -delete, git checkout on a path, git reset --hard, git clean, or a python3 -c one-liner. Then run git status to see which tracked files are missing. A file a Bash command removed is gone from disk unless it was committed (pushed or local) or a hook moved it into an _archive/ folder instead.

### How to prevent Claude from deleting files?

Commit and push your work before Claude Code starts. Then add deny rules for rm, rmdir and destructive git commands to .claude/settings.json, add a PreToolUse hook that exits with code 2 and tells the agent to move files to _archive/ instead, and test the hook by piping sample JSON into it. Finally, run unattended jobs in isolated runs. Deny rules match command text only, so the hook and pushed work cover what they miss.

### Why did Claude delete my projects?

Claude Code deletes files when a task sounds like a reset: asked to start fresh, it clears the folder first, and in our test it chose find with -delete on its own. Git commands that revert work, such as git checkout on a path, git reset --hard or git clean, delete uncommitted changes too. Without a deny rule or a hook, nothing sits between the command and the disk.

### Can Claude recover deleted files?

Claude Code gets back only what exists outside the working files: commits, local or pushed (only pushed ones survive a lost or wiped folder), and files a hook redirected into an _archive/ folder instead of deleting them. Uncommitted work removed by a Bash delete or a git revert stays gone. ConvOps isolated runs start from pushed commits in their own environment, so a delete inside one cannot reach your local copy.

### Do deny rules still work in bypassPermissions mode?

Yes. In our scratch runs on Claude Code 2.1.295 with --dangerously-skip-permissions, the deny rule Bash(rm *) blocked rm, and a PreToolUse hook exiting with code 2 blocked it too, which matches Anthropic's permission modes docs. The constraint: the same deny rule let find with -delete, /bin/rm and a Python one-liner delete the file, because deny rules match command text, not what a command does.

Canonical: https://neomanex.com/learn/guides/stop-claude-code-deleting-files
