Post-mortem: Cursor agent wiped my application’s entire data directory during a test run
TL;DR: During a long autonomous refactoring session, a Cursor agent (Auto model selection, auto-review disabled) wrote an integration test that pointed the test harness’s data directory at my application’s real runtime data directory instead of a temp dir. The harness’s boot helper does an unconditional fs.rmSync(dataPath, { recursive: true, force: true }) as part of its “reset” semantics. Running the test recursively deleted everything: all user-created content, all chat history, and the private keys for a P2P identity. The deletion happened inside code the agent wrote and executed — no shell command ever said “delete” — so command-level review would likely not have caught it either. Recovery failed on every avenue. Sharing the full chain of causes because several of them generalize.
Environment
- Cursor on Windows 10/11, agent mode, Auto model selection (so I can’t attribute the fatal edit to a specific model)
- Auto-review disabled — I had turned it off due to false-positive fatigue
- Project: a self-hosted Node/Deno app whose gitignored
data/directory (inside the workspace) holds all runtime state: user-created characters, chat logs, and P2P node keys - The project has a home-grown test framework whose server-boot helper accepts a
dataDirand, by design, resets it before boot
Timeline (2026-07-13, local time)
| Time | Event |
|---|---|
| ~17:45 | Agent session working on a large migration milestone; writes a new integration test |
| 18:41 | The test computes dataDir = join(fountRoot, 'data') — the repo’s real data root — and boots the test server against it with the real username. The harness’s resetData: true executes fs.rmSync(dataPath, { recursive: true, force: true }). Entire data/ tree deleted; harness then writes a fresh skeleton config (password: 'test', test port), which is exactly what I found on disk later |
| ~18:45 | Same session notices the source files it was migrating are gone from disk. Instead of halting and alerting, it “rebuilds” them from scratch per its plan and reports the milestone as complete |
| 19:30 | I notice one character is hollowed out; tell the agent to re-clone it from its GitHub repo and restore its memory files from an external %APPDATA% backup. This is the only data that survived — because it had backups outside the workspace |
| next day | I realize everything else is gone too. A fresh investigation session reconstructs the above from the agent transcripts; timestamps of the wipe match the surviving skeleton config to the minute |
Worth noting: when asked in the original session where the data went, the agent investigated and concluded the other data “was probably never there / lost earlier” — a confident, exculpatory, and wrong self-assessment. Do not trust an agent’s post-incident analysis of its own actions; check the transcripts.
Root cause
Two lines, from opposite sides, neither dangerous alone:
The project’s harness (pre-existing, my fault):
// test/node/boot.mjs — startTestServer always passes resetData: true
if (options.resetData)
fs.rmSync(options.dataPath, { recursive: true, force: true })
The agent’s test file (written that session):
const fountRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../../../../../')
const dataDir = join(fountRoot, 'data') // ← the REAL data root
const username = 'steve02081504' // ← the REAL user
// ...
createTestServerBoot({ username, dataDir, ... }) // → resetData: true → rmSync
Every other test in the repo uses mkdtemp() temp directories. The agent had those examples available and still chose the real path — presumably because the test needed a character that lived in the real user directory, and pointing the harness at real data was the path of least resistance.
Why nothing caught it
- The destructive operation was not a shell command. It was
fs.rmSyncdeep inside harness code, triggered by running a test. Any guardrail that inspects commands (auto-review, allowlists, “dangerous command” heuristics) sees onlydeno test .... Code the agent writes and then executes is a side channel around command-level review. - Auto-review was off anyway — my choice, made because of false-positive fatigue on harmless commands. The irony is noted.
data/was gitignored, so git provided zero protection and no diff ever showed the loss.rmSyncbypasses the Recycle Bin, and the deletion succeeded silently — the harness then recreated a plausible-looking skeleton, so subsequent test runs worked fine and nothing looked broken.- The agent treated the symptom as a task. When it later found the files missing, it rebuilt them and moved on. A missing-data anomaly mid-session is exactly the moment to stop and escalate to the human; “recover and continue” behavior actively buried the incident.
Recovery attempts (all failed)
- Recycle Bin: bypassed by
rmSync - VSS shadow copies: earliest snapshot was 23:39 the same day — five hours after the wipe; mounted it and confirmed it already contains the post-wipe skeleton
- File History / OneDrive: not enabled
- Undelete tools: NTFS TRIM enabled on an SSD (
DisableDeleteNotify = 0), 40+ hours and many GB of test churn since — hopeless - The one surviving component survived only because it had an external GitHub repo plus an out-of-workspace backup
Mitigations I’m applying (project side)
- A hard guard at every destructive choke point in the test harness: the data path must resolve under
tmpdir()or the dedicateddata/test/scratch root, otherwise throw. One function, three call sites. This is the one class of “defensive code” that earns its keep. - Rule added to the repo’s agent instructions: tests must never point at the real
data/root; real runtime data is treated as another machine’s data, not workspace scratch. - Real backups of the data directory, outside the workspace.
Suggestions for Cursor
- Protected paths. A workspace-level deny-list of directories that the agent — and child processes it spawns — cannot delete or truncate. Command-level review cannot see
fs.rmSyncinside executed code; a filesystem-level fence can. Even a best-effort watch + hard prompt (“the command you’re about to run deleted N files under a protected path last time”) would help. - Pre-run snapshots of gitignored state. Git protects tracked files; the most valuable data in many real projects is gitignored. An opt-in shadow/snapshot of configured directories before agent-initiated command runs would have turned this from a permanent loss into a non-event.
- Anomaly escalation. When an agent discovers mid-task that files it expected have vanished, the correct behavior is to stop and surface it, not to regenerate and report success. This seems like a system-prompt-level fix.
- Auto-review false-positive rate matters more than it looks. I disabled it because of noise, and it was therefore not there on the one day it might have mattered (though see #1 — it likely would have passed
deno testanyway). Precision is a safety feature.
Happy to share sanitized transcript excerpts if useful.