Deletion Tripwire
Overview
A mechanical guard, not a persona: a PreToolUse hook that intercepts destructive shell
commands and refuses to run them until the enumerate → confirm → ledger → approve protocol has
been followed. It exists because the catastrophic-deletion class strikes during casual,
un-reviewed operations, when no review panel is in session — so the guard must fire EVERY time,
without anyone remembering to ask.
The two lessons it enforces, burned in by a public field disaster (an agent cleaned up
Windows.old after answering "safe" when asked directly; leftover junctions inside it pointed at
the user's LIVE Documents/Pictures; the deletion followed the links and emptied the real folders):
- The blast radius of a deletion is everything REACHABLE from the target — links and
junctions included — not the folder that was named.
- "Is it safe?" answered by prediction is the weapon. Safety requires enumeration (a dry
run that lists what will actually be touched), never reasoning about what should be there.
This is the deletion-direction sibling of the push/publish/send boundary: two irreversible
directions, leaving the machine (can't recall) and leaving existence (can't recover).
The protocol (what the block message demands)
ENUMERATE, don't predict. Dry-run the reachable set: count, total size, sample paths.
Hunt links/junctions resolving OUTSIDE the target
(PowerShell: Get-ChildItem <target> -Recurse -Force -Attributes ReparsePoint;
bash: find <target> -type l). Anything escaping the target is a hard stop to report.
⚠️ The -Attributes ReparsePoint check FALSE-POSITIVES on cloud-sync folders. Dropbox,
OneDrive and iCloud mark synced/placeholder files with the reparse-point bit, so inside those
folders nearly every file trips this test. Observed 2026-07-27 in a Dropbox workspace:
25 of 36 files flagged, including plain CLAUDE.md — taken literally, the hard stop would
block every deletion forever, which trains people to ignore the guard.
Disambiguate before treating it as an escape: a real link populates LinkType AND Target;
a cloud-sync placeholder leaves both EMPTY.
Get-ChildItem <target> -Recurse -Force -Attributes ReparsePoint |
Where-Object { $_.LinkType } | # genuine symlink/junction only
Select-Object FullName, LinkType, Target
Only entries surviving that filter are real escapes and a genuine hard stop. On bash,
find <target> -type l does not have this problem.
CONFIRM. Show the user the manifest and anything outside-target; get an explicit yes.
If the user never asked for a deletion, stop and surface instead of proceeding.
LEDGER. Write <ledger>/<id>.md BEFORE deleting: the user's verbatim ask, the command,
the manifest summary, the confirmation. Evidence of what died, every time — experiment
artifacts and receipts have been destroyed by innocent "clean up junk" passes before.
RERUN the same command with trailing comment # tripwire-approved:<id>. The hook allows
it only while the ledger entry is fresh (15 minutes).
Prefer recoverable deletion (Recycle Bin / move-aside) over permanent when practical.
Setup (per machine, like the assumption-debt hooks)
install.sh symlinks this skill but deliberately does not touch settings.json. Wire the hook
manually as a PreToolUse entry matching Bash|PowerShell:
"PreToolUse": [{
"matcher": "Bash|PowerShell",
"hooks": [{ "type": "command",
"command": "python \"<repo>/skills/deletion-tripwire/hooks/guard_destructive.py\" \"<ledger-dir>\"" }]
}]
Ledger default: ~/.claude/deletion-ledger/. Self-check: python hooks/test_guard.py.
What it blocks / what it lets through
| Blocked (any shell) |
Allowed |
rm with -r/--recursive (any flag combo) |
single-file rm / Remove-Item |
Remove-Item -Recurse (+ aliases/abbreviations) |
non-delete commands with -r flags (grep -r) |
| `rd |
rmdir |
| `robocopy /MIR |
/PURGE` |
git clean -f*, git reset --hard |
|
find -delete / find -exec rm, shutil.rmtree |
|
dd of=/dev/*, mkfs, format X: |
|
Common Mistakes
- Self-serving the approval token without doing the protocol. The token is not a bypass;
writing the ledger entry REQUIRES the enumeration and the user's confirmation to already
exist. Skipping to step 4 defeats the guard and re-arms the exact disaster it prevents.
- Predicting instead of enumerating. "That folder only contains X" is the sentence that
destroyed a stranger's family photos. Run the dry-run; read the real list.
- Growing the ephemeral allowlist casually. Every generic name added (
build, dist,
temp) widens the silent path. Expand only deliberately, for names that are unambiguous.
- Treating a quiet tripwire as a broken one. It is pattern-based and stays silent on normal
work by design (a guard that fires constantly gets tuned out). Known ceilings are named in
the design spec; grow the pattern list with incidents, not speculation.
- Weakening the matcher because prose tripped it. The guard cannot tell running a
destructive command from talking about one — a commit message or echo that mentions the
patterns gets blocked (observed twice on day one, on this very skill's own commit messages).
That over-blocking is deliberate: quote-stripping would open a
bash -c "<destructive>"
bypass. The durable workaround is to keep such prose OUT of the shell string — write commit
messages to a file and use git commit -F <file>, print docs from files instead of echoes.
Provenance
Born 2026-07-19, test-first (RED: 35-check assert suite watched failing on the missing module;
GREEN: all pass, including the end-to-end stdin/exit-code hook contract). Motivating cases:
(1) public r/ClaudeCode data-loss incident, 2026-07 — Windows.old cleanup followed junctions
into live Documents/Pictures after the agent answered "safe" from prediction; (2) same-day local
miniature in this lab — a routine "clean up any junk" pass deleted the artifacts that were a
skill's only test evidence ([[paladin-review-works]]). Design spec:
docs/specs/2026-07-19-deletion-tripwire-design.md. Field wins to be appended per the
Provenance win rule.
1---2name: deletion-tripwire3description: Use when a TRIPWIRE block message appears after a destructive command, when wiring the deletion guard on a new machine, or before any bulk deletion or cleanup — "remove junk", "clean up", "delete old files", "wipe this folder", clearing an old installation, or any recursive/force delete — so the enumerate-confirm-ledger protocol runs before anything leaves existence.4---56# Deletion Tripwire78## Overview910A **mechanical** guard, not a persona: a PreToolUse hook that intercepts destructive shell11commands and refuses to run them until the enumerate → confirm → ledger → approve protocol has12been followed. It exists because the catastrophic-deletion class strikes during casual,13un-reviewed operations, when no review panel is in session — so the guard must fire EVERY time,14without anyone remembering to ask.1516The two lessons it enforces, burned in by a public field disaster (an agent cleaned up17`Windows.old` after answering "safe" when asked directly; leftover junctions inside it pointed at18the user's LIVE Documents/Pictures; the deletion followed the links and emptied the real folders):19201. **The blast radius of a deletion is everything REACHABLE from the target** — links and21 junctions included — not the folder that was named.222. **"Is it safe?" answered by prediction is the weapon.** Safety requires enumeration (a dry23 run that lists what will actually be touched), never reasoning about what should be there.2425This is the deletion-direction sibling of the push/publish/send boundary: two irreversible26directions, leaving the machine (can't recall) and leaving existence (can't recover).2728## The protocol (what the block message demands)29301. **ENUMERATE, don't predict.** Dry-run the reachable set: count, total size, sample paths.31 Hunt links/junctions resolving OUTSIDE the target32 (PowerShell: `Get-ChildItem <target> -Recurse -Force -Attributes ReparsePoint`;33 bash: `find <target> -type l`). Anything escaping the target is a hard stop to report.3435 ⚠️ **The `-Attributes ReparsePoint` check FALSE-POSITIVES on cloud-sync folders.** Dropbox,36 OneDrive and iCloud mark synced/placeholder files with the reparse-point bit, so inside those37 folders *nearly every file* trips this test. Observed 2026-07-27 in a Dropbox workspace:38 **25 of 36 files flagged, including plain `CLAUDE.md`** — taken literally, the hard stop would39 block every deletion forever, which trains people to ignore the guard.4041 **Disambiguate before treating it as an escape: a real link populates `LinkType` AND `Target`;42 a cloud-sync placeholder leaves both EMPTY.**43 ```powershell44 Get-ChildItem <target> -Recurse -Force -Attributes ReparsePoint |45 Where-Object { $_.LinkType } | # genuine symlink/junction only46 Select-Object FullName, LinkType, Target47 ```48 Only entries surviving that filter are real escapes and a genuine hard stop. On bash,49 `find <target> -type l` does not have this problem.502. **CONFIRM.** Show the user the manifest and anything outside-target; get an explicit yes.51 If the user never asked for a deletion, stop and surface instead of proceeding.523. **LEDGER.** Write `<ledger>/<id>.md` BEFORE deleting: the user's verbatim ask, the command,53 the manifest summary, the confirmation. Evidence of what died, every time — experiment54 artifacts and receipts have been destroyed by innocent "clean up junk" passes before.554. **RERUN** the same command with trailing comment `# tripwire-approved:<id>`. The hook allows56 it only while the ledger entry is fresh (15 minutes).5758Prefer recoverable deletion (Recycle Bin / move-aside) over permanent when practical.5960## Setup (per machine, like the assumption-debt hooks)6162`install.sh` symlinks this skill but deliberately does not touch `settings.json`. Wire the hook63manually as a PreToolUse entry matching `Bash|PowerShell`:6465```json66"PreToolUse": [{67 "matcher": "Bash|PowerShell",68 "hooks": [{ "type": "command",69 "command": "python \"<repo>/skills/deletion-tripwire/hooks/guard_destructive.py\" \"<ledger-dir>\"" }]70}]71```7273Ledger default: `~/.claude/deletion-ledger/`. Self-check: `python hooks/test_guard.py`.7475## What it blocks / what it lets through7677| Blocked (any shell) | Allowed |78|---|---|79| `rm` with `-r`/`--recursive` (any flag combo) | single-file `rm` / `Remove-Item` |80| `Remove-Item -Recurse` (+ aliases/abbreviations) | non-delete commands with `-r` flags (`grep -r`) |81| `rd|rmdir|del /s` | `rm -rf node_modules` / `__pycache__` (single simple target) |82| `robocopy /MIR|/PURGE` | approved rerun with fresh ledger entry |83| `git clean -f*`, `git reset --hard` | |84| `find -delete` / `find -exec rm`, `shutil.rmtree` | |85| `dd of=/dev/*`, `mkfs`, `format X:` | |8687## Common Mistakes8889- **Self-serving the approval token without doing the protocol.** The token is not a bypass;90 writing the ledger entry REQUIRES the enumeration and the user's confirmation to already91 exist. Skipping to step 4 defeats the guard and re-arms the exact disaster it prevents.92- **Predicting instead of enumerating.** "That folder only contains X" is the sentence that93 destroyed a stranger's family photos. Run the dry-run; read the real list.94- **Growing the ephemeral allowlist casually.** Every generic name added (`build`, `dist`,95 `temp`) widens the silent path. Expand only deliberately, for names that are unambiguous.96- **Treating a quiet tripwire as a broken one.** It is pattern-based and stays silent on normal97 work by design (a guard that fires constantly gets tuned out). Known ceilings are named in98 the design spec; grow the pattern list with incidents, not speculation.99- **Weakening the matcher because prose tripped it.** The guard cannot tell running a100 destructive command from *talking about one* — a commit message or echo that mentions the101 patterns gets blocked (observed twice on day one, on this very skill's own commit messages).102 That over-blocking is deliberate: quote-stripping would open a `bash -c "<destructive>"`103 bypass. The durable workaround is to keep such prose OUT of the shell string — write commit104 messages to a file and use `git commit -F <file>`, print docs from files instead of echoes.105106## Provenance107108Born 2026-07-19, test-first (RED: 35-check assert suite watched failing on the missing module;109GREEN: all pass, including the end-to-end stdin/exit-code hook contract). Motivating cases:110(1) public r/ClaudeCode data-loss incident, 2026-07 — `Windows.old` cleanup followed junctions111into live Documents/Pictures after the agent answered "safe" from prediction; (2) same-day local112miniature in this lab — a routine "clean up any junk" pass deleted the artifacts that were a113skill's only test evidence ([[paladin-review-works]]). Design spec:114`docs/specs/2026-07-19-deletion-tripwire-design.md`. Field wins to be appended per the115Provenance win rule.