{"slug": "show-hn-demo-cli-snapshot-before-your-ai-agent-runs-rm-rf-one-command-undo", "title": "Show HN: Demo_CLI – snapshot before your AI agent runs rm -RF, one-command undo", "summary": "A developer released demo_cli, an open-source tool that snapshots files, directories, and local databases before AI agents execute destructive commands like rm -rf or DROP TABLE, enabling one-command undo. Unlike existing safety hooks that only block dangerous commands, demo_cli defaults to recovery by capturing pre-deletion state and only blocks actions it cannot reverse, such as terraform destroy or remote resource deletion. The tool runs as a hook for Claude Code and Cursor, is MIT-licensed, and operates in observe-only mode by default.", "body_md": "**Claude Code and Cursor ask you to confirm dangerous commands, or block them. Neither snapshots first, and their undo doesn't cover shell commands or databases.**\n\ndemo_cli snapshots the real target **before** a destructive command runs, so a wrong call is reversible with one command. **Confirming ≠ recovering.**\n\nBefore `rm -rf`\n\n, `rmdir /s /q`\n\n, `Remove-Item -Recurse -Force`\n\n, `git reset --hard`\n\n, or `DROP TABLE`\n\nexecutes, demo_cli captures what's about to be destroyed and writes a tamper-evident receipt of the decision. If the agent gets it wrong, `demo_cli undo`\n\nbrings it back. When it *can't* prove recovery (`terraform destroy`\n\n, `git push --force`\n\n, remote/cloud resources, or a recursive-force delete with no recoverable target), it hard-blocks instead of faking safety.\n\nThe whole design in one line: **recovery is the default; blocking is the fallback for the truly unrecoverable, not the default for everything.**\n\nDefault mode is observe-only: it logs what it *would* have caught and touches nothing. Run it a week on a low-stakes project, read the receipts, then decide whether to let it act.\n\n```\npipx install git+https://github.com/WePwn/demo_cli.git@beta\ndemo_cli init        # shadow mode by default, observes, never blocks\ndemo_cli doctor      # verify the install\n```\n\n**No telemetry, it phones nobody.** Verify it yourself:`grep -rn \"requests\\|urllib\\|http\\|socket\" src/`\n\n**One small, readable, MIT-licensed codebase**, read exactly what it does before you run it.** Tamper-evident receipts**, every decision is hash-chained and independently verifiable (`demo_cli verify`\n\n).\n\ngit and Claude Code's rewind can't recover an `rm -rf`\n\noutside the repo, a dropped database, or an overwrite of an untracked file, they don't snapshot before shell commands run. That's the exact gap demo_cli fills.\n\nThreat model:cooperative agents making mistakes, not adversarial evasion. An agent actively trying to evade protection is out of scope, no hook solves that.\n\n`0.4.0b5`\n\n, public beta.\n\nBuilt around one invariant:\n\nA mutating action must be recoverable\n\nandmatch its declared context, otherwise it is escalated, never silently allowed, and never falsely reported as \"recovered\".\n\nMost safety hooks for AI agents do one thing: pattern-match a dangerous command\nand **block** it. That stops the obvious disasters, but it also stops the agent\nmid-task, so you either loosen the rules until they stop catching things, or\nyou babysit the session.\n\ndemo_cli starts from the opposite default: **recovery, not blocking.**\n\n- For anything it can prove it captured (a file, a directory, a local DB), it\n**snapshots first and lets the agent keep working.** If the agent gets it wrong,`demo_cli undo <id>`\n\nbrings it back. The work finishes; the mistake is reversible. - It only\n**blocks** when something genuinely*can't*be recovered: an external or irreversible effect (`terraform destroy`\n\n,`git push --force`\n\n, an object-store delete), or a recursive-force delete that leaves no recoverable target (`Remove-Item -Recurse -Force`\n\n,`rmdir /s`\n\n,`del /s`\n\n). There, it escalates honestly instead of pretending it captured a recovery point.\n\nBlocking is the fallback for the un-recoverable, not the default for everything. That's the whole design.\n\nClaude Code supports [PreToolUse hooks](https://docs.claude.com/en/docs/claude-code/hooks):\na shell command that runs **before each tool call**, receives the full tool input\nas JSON on stdin, and returns a permission decision. demo_cli is that command.\n\n```\nClaude Code is about to run: Bash(rm -rf dist/)\n                              ↓\n              demo_cli hook  (fires automatically)\n                              ↓\n         classify → resolve target → snapshot → decide\n                              ↓\n        { \"permissionDecision\": \"allow\" }   ← allow, with a recovery point\n        { \"permissionDecision\": \"deny\"  }   ← blocked, reason surfaced to agent\n        { \"permissionDecision\": \"ask\"   }   ← paused, human decides\n```\n\n**One property worth knowing:** a PreToolUse hook returning `deny`\n\nstops the tool\n**even when the session is running --dangerously-skip-permissions or\nbypassPermissions**, that mode skips the interactive prompts, not the hooks.\nSo the recovery-and-escalation layer holds even in a fully autonomous run. (The\none way it does\n\n*not*fire: if the\n\n`demo_cli`\n\nbinary isn't on PATH in the shell\nwhere `claude`\n\nlaunched, Claude Code silently proceeds with no check, see\n[Known issues](#known-issues-beta). Run\n\n`demo_cli doctor`\n\nto confirm.)demo_cli gates **both** tool categories Claude Code uses to modify your project:\n\n`Bash`\n\n- shell commands (`rm`\n\n,`git reset --hard`\n\n,`terraform destroy`\n\n, …)`Edit`\n\n/`Write`\n\n/`MultiEdit`\n\n/`NotebookEdit`\n\n- direct file mutations\n\nA file or directory is snapshotted **before** it is touched. If the agent\ndestroys something, `demo_cli undo <id>`\n\nbrings it back.\n\nCursor supports [hooks](https://cursor.com/docs/hooks): scripts it runs at named\npoints in the agent loop. demo_cli registers a `beforeShellExecution`\n\nhook that\nreceives the command as JSON on stdin (`command`\n\n, `cwd`\n\n, …) and returns a\npermission decision.\n\n```\ndemo_cli install-hook --cursor          # writes .cursor/hooks.json (project)\ndemo_cli install-hook --cursor --scope global   # or ~/.cursor/hooks.json\ndemo_cli install-hook --cursor --print  # inspect the snippet without writing\n```\n\nThe installed hook looks like this:\n\n```\n{\n  \"version\": 1,\n  \"hooks\": {\n    \"beforeShellExecution\": [\n      { \"command\": \"demo_cli hook-cursor\", \"failClosed\": true }\n    ]\n  }\n}\n```\n\nTwo Cursor-specific properties, both handled deliberately:\n\nBy default Cursor`failClosed: true`\n\nis mandatory.*fails open*: a hook that crashes, times out, or emits invalid JSON lets the command through.`failClosed: true`\n\ninverts that, a guard that cannot run blocks instead. The adapter also fails closed*inside*the script: if it holds a real command it cannot evaluate, it returns`deny`\n\n(the opposite of the Claude Code adapter's fail-open on internal error). The one exception is Cursor's known empty-stdin defect on some remote workspaces, where there is no command to judge, so it steps aside rather than brick the session.**Only** Cursor's own allow-list can override an`deny`\n\nis reliably honored today.`allow`\n\n/`ask`\n\nfrom a hook. That is fine here: the whole move is to*deny the unrecoverable*, which is exactly the decision Cursor respects. So a recursive-force delete on Cursor is stopped by the same rule that stops it in Claude Code.\n\nScope for this beta: the Cursor adapter gates **shell commands** only\n(`beforeShellExecution`\n\n). File-edit gating is Claude Code only for now.\n\n**With pipx (recommended)**, puts `demo_cli`\n\nin your global PATH so Claude\nCode can find it from any project directory:\n\n```\npipx install git+https://github.com/WePwn/demo_cli.git@beta\ndemo_cli --version\n```\n\n**From a clone:**\n\n```\ngit clone -b beta https://github.com/WePwn/demo_cli.git\ncd demo_cli\npipx install -e .\ndemo_cli --version\n```\n\nNote:if you install inside a virtualenv, that venv must be active in every shell where you launch`claude`\n\n. Otherwise the`demo_cli hook`\n\ncommand is not found and Claude Code silently proceeds without a safety check. Use`demo_cli doctor`\n\nto verify the install.\n\nRequires Python 3.9+.\n\n```\n# inside your project\ndemo_cli init            # write .demo_cli.toml (shadow mode by default)\ndemo_cli install-hook    # register the PreToolUse hook in .claude/settings.json\ndemo_cli status          # confirm: hook installed, mode shadow, 0 receipts\n```\n\nNow launch Claude Code and ask it to do something destructive. demo_cli fires automatically, no extra commands needed. Afterwards:\n\n```\ndemo_cli log             # see every recovery point: id, when, kind, size, action\ndemo_cli diff <id>       # what exactly changed\ndemo_cli undo <id>       # restore to the state before that action\ndemo_cli verify          # confirm the receipt log was not tampered with\n```\n\nTo evaluate a command manually (useful for testing or CI):\n\n```\ndemo_cli check \"DELETE FROM users WHERE plan = 'free'\" --db app.db\ndemo_cli check \"rm -rf dist/\" --quiet\ndemo_cli check \"terraform destroy\" --actual-env production --json\n```\n\n**shadow** (default), observe, snapshot, and record, but never block. The\nrecommended way to start: prove the value with zero workflow disruption. In\nshadow mode the hook writes to stderr when it captures a snapshot or sees a\nblocking decision, so the value is visible without affecting Claude Code's flow.\n\n**enforce** - the hook actively gates tool calls:\n\n| Disposition | Hook response | When |\n|---|---|---|\n`ESCALATE` |\n`deny` |\nnon-recoverable blast radius (infra destroy, force push, …) |\n`CONTEXT_MISMATCH` |\n`ask` |\ndeclared env does not match the resolved env |\n`REVERSIBLE` / `DRY_RUN` |\n`allow` |\nsnapshotted first, recoverable |\n`ALLOW` |\n`allow` |\nnon-mutating, no action needed |\n\nSwitch mode in `.demo_cli.toml`\n\nor per call:\n\n```\ndemo_cli check \"rm -rf dist\" --mode enforce\n```\n\nStart in shadow. Move to enforce on a project once you trust what it's doing.\n\nRun `demo_cli init`\n\nto scaffold `.demo_cli.toml`\n\nat the project root.\n\n```\nmode = \"shadow\"\n\n[workspace]\ndir = \".demo_cli\"          # receipts + recovery points (gitignored)\n\n[approval]\nkey_env = \"DEMO_CLI_APPROVER_KEY\"\n\n[[target]]\nmatch = \"production\"       # substring matched against the resolved target ref\nenv = \"production\"\nrecovery = \"snapshot\"      # snapshot | none\n```\n\nEnvironment is resolved in priority order: an explicit `--actual-env`\n\nflag,\nthen a `[[target]]`\n\nmatch in this file, then a heuristic over the command text.\nA production database is reached via a connection string, not by a file called\n`prod.db`\n\n, the declared target is always the source of truth.\n\n| Command | What it does |\n|---|---|\n`check \"<cmd>\"` |\nevaluate a command; flags: `--db` , `--db-url` , `--target` , `--mode` , `--intent-env` , `--actual-env` , `--reason` , `--approval-token` , `--json` , `--quiet` |\n`log` |\nlist captured recovery points (id, when, kind, size, action) |\n`undo [id]` |\nrestore a recovery point by id, or the latest |\n`diff [id]` |\nshow what changed since a recovery point |\n`verify` |\nwalk the receipt hash-chain → INTACT or TAMPERED |\n`report` |\nsummarise recorded decisions |\n`receipt [id]` |\nprint a copy-pasteable, tamper-evident proof card for a receipt (latest, or by id); `--list` shows recent receipt ids |\n`status` |\nmode, hook state, receipts, chain integrity, recovery count |\n`doctor` |\ncheck python version, config, pg tools, hook registration, PATH |\n`prune` |\ndelete old recovery artefacts (`--keep N` , `--older-than DAYS` ); receipts are never pruned |\n`init` |\nscaffold `.demo_cli.toml` |\n`install-hook` |\nwrite PreToolUse entries into `.claude/settings.json` (add `--cursor` for `.cursor/hooks.json` ) |\n`hook` |\n(internal) called by Claude Code; reads tool JSON on stdin, writes permission decision on stdout |\n`hook-cursor` |\n(internal) called by Cursor; reads `beforeShellExecution` JSON on stdin, writes permission decision on stdout |\n\nExit codes for `check`\n\n: `0`\n\nallow, `1`\n\ncontext mismatch, `2`\n\nescalate.\n\n**Snapshot targets:** sqlite files, postgres (via `pg_dump`\n\n/`pg_restore`\n\n),\nindividual files, directories (capped at `DEMO_CLI_MAX_SNAPSHOT_MB`\n\n, default 256 MB).\n\n**Escalated honestly, never falsely snapshotted:** terraform/kubectl/cloud\ndestroy commands, `git push --force`\n\n, remote filesystem changes, object-storage\ndeletions, external side effects (email, payments, webhooks), credential rotation,\nand any database reached over a non-local connection string. demo_cli will not\nclaim a recovery it cannot provide.\n\n**Recursive-force deletes are a hard-stop in every environment:**\n`Remove-Item -Recurse -Force`\n\n(PowerShell, including the `ri`\n\nalias and\nabbreviated `-r`\n\n/`-fo`\n\nflags in any order), `rmdir /s`\n\n, and `del /s|/f`\n\n. These\nwipe a whole tree with no recycle bin and expose no target the snapshot layer\ncan capture, so there is no honest recovery point to stand behind. They are\ndenied even in a `development`\n\n/`staging`\n\nworkspace, where an ordinary,\nsnapshottable delete (`rm -rf ./build`\n\n) would instead be captured and allowed.\nA human structural-approval token is the one legitimate override; an agent\ncannot forge it.\n\n**Out of scope for this beta:**\n\n- Adversarial agents deliberately evading classification\n- Reversing already-sent external effects\n- Multi-agent concurrent sessions (receipt locking is POSIX; Windows degrades to best-effort)\n- Adapters for agents other than Claude Code and Cursor (Aider, Cline, planned)\n\n**PATH:**`demo_cli`\n\nmust be resolvable in the shell where`claude`\n\nruns. Install with`pipx`\n\n, or keep the virtualenv active. Run`demo_cli doctor`\n\nto check. If the binary is not found, Claude Code silently skips the hook.**Single-path**`rm`\n\n:`rm a.py b.py`\n\n(multiple targets) escalates rather than partially snapshotting one file. This is intentional - partial recovery is not honest recovery.most two-argument`mv`\n\nis conservative:`mv`\n\ncommands are flagged. Safe renames inside the project workspace will be narrowed in a future release.\n\n``` python\nfrom demo_cli import Guard\n\nresult = Guard(mode=\"enforce\").evaluate(\"DELETE FROM users\", explicit_db=\"app.db\")\nprint(result.decision.decision)   # REVERSIBLE\nprint(result.permission)          # allow\npip install -e \".[dev]\"\npytest -q\n# 85 tests, passing on Python 3.9 – 3.14\n```\n\nThis is a public beta. The most useful reports are **real commands from real\nagent sessions** that produced a wrong decision (false block or missed snapshot).\nOpen an issue with the command, your `.demo_cli.toml`\n\n(redact credentials), and\nwhat you expected. Bug reports found through dogfooding, like the `rm app.db`\n\ncase that shaped 0.4.0b3, are exactly what the project needs right now.\n\nTo make that trivial, an in-context feedback prompt fires on a wrong-call-worthy\ndecision (a block, a context mismatch, or a snapshot): the tool prints a\n`Wrong call? → report it`\n\nline with a **prefilled** GitHub issue (decision,\nreason, matched rule, command already filled in). It is consent-based and\none-directional, a link handed to you, nothing phones home. It stays silent on\nplain `ALLOW`\n\ns so it never becomes noise. `demo_cli receipt --share`\n\nturns any\nreceipt into a plain-text proof card you can paste into an issue or thread.", "url": "https://wpnews.pro/news/show-hn-demo-cli-snapshot-before-your-ai-agent-runs-rm-rf-one-command-undo", "canonical_source": "https://github.com/WePwn/demo_cli", "published_at": "2026-07-09 20:56:31+00:00", "updated_at": "2026-07-09 21:36:54.461087+00:00", "lang": "en", "topics": ["ai-safety", "developer-tools", "ai-agents"], "entities": ["demo_cli", "Claude Code", "Cursor", "WePwn", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/show-hn-demo-cli-snapshot-before-your-ai-agent-runs-rm-rf-one-command-undo", "markdown": "https://wpnews.pro/news/show-hn-demo-cli-snapshot-before-your-ai-agent-runs-rm-rf-one-command-undo.md", "text": "https://wpnews.pro/news/show-hn-demo-cli-snapshot-before-your-ai-agent-runs-rm-rf-one-command-undo.txt", "jsonld": "https://wpnews.pro/news/show-hn-demo-cli-snapshot-before-your-ai-agent-runs-rm-rf-one-command-undo.jsonld"}}