Building @Termaxadev — execution safety for AI coding agents.
Rust • OSS
@Zerodropdev — email verification infrastructure for developers and AI agents.
Receipts : what termaxa has caught, missed and fixed; in order, with the exact command each time. This thread is the record. New entries go underneath as they happen; nothing here is a mockup and nothing predates its date.
Jul : rm -rf x/$SID emptied a stranger's repo. The preview said UNRESOLVED: a shell variable, a delete that lands somewhere the preview cannot predict, not recoverable. It still says that.
Jul : repo-owned audit logs: git reset --hard could erase the record. State moved to ~/.termaxa, out of git's reach (v0.8).
Aug : Cursor 3.11 renamed its hook events; four releases went ungated with every test green. Caught by payload capture, fixed the same night; the real payloads are tests now (v0.11.4).
Aug : a registered hook whose command path was mangled failed non-blocking, one warning line, session ungated, doctor said "configured" in green. The liveness probe exists because of it (v0.15).
Sep : rm -rf ./cache > /dev/null 2>&1: the insurance read /dev/null as a target, failed to copy a device, and the directory was deleted uninsured. A segment now knows its own words; a redirect is never a target (v0.18, #61).
Sep : sh -c "cat /dev/null > src/main.rs" was asked, previewed nothing, took no backup: the wrapper hid the command. The string inside is read as the command it runs (v0.18, #62).
Sep : wrap had never executed a command: the runner's own shell resolved back through the shims. An allowed command recursed until killed. Fixed; the happy-path test that was never written now exists (v0.18, #65).
Sep : the first live Codex session - the hook fired on every command and Codex rejected every answer ; allow, ask and a deny that exited 2 all failed the hook and fell open to Codex's own prompt. Codex honours one verdict, deny, on stdout with exit 0. Fixed; a hard stop now lands in Codex's UI as "Blocked by hook" with the blast radius (v0.18, #74).
The rule for this thread : the command, what the gate did, what changed. No adjectives.
The command-level versus effect-level line, written out. A sandbox founder told me my tool could not see the thing it was built for. He was half right, and the half he was right about is the useful part.
Named with permission, with the measurements that settled it.
https://t.co/M6XxSWgLIP
Receipts : what termaxa has caught, missed and fixed; in order, with the exact command each time. This thread is the record. New entries go underneath as they happen; nothing here is a mockup and nothing predates its date.
Jul : rm -rf x/$SID emptied a stranger's repo. The preview said UNRESOLVED: a shell variable, a delete that lands somewhere the preview cannot predict, not recoverable. It still says that.
Jul : repo-owned audit logs: git reset --hard could erase the record. State moved to ~/.termaxa, out of git's reach (v0.8).
Aug : Cursor 3.11 renamed its hook events; four releases went ungated with every test green. Caught by payload capture, fixed the same night; the real payloads are tests now (v0.11.4).
Aug : a registered hook whose command path was mangled failed non-blocking, one warning line, session ungated, doctor said "configured" in green. The liveness probe exists because of it (v0.15).
Sep : rm -rf ./cache > /dev/null 2>&1: the insurance read /dev/null as a target, failed to copy a device, and the directory was deleted uninsured. A segment now knows its own words; a redirect is never a target (v0.18, #61).
Sep : sh -c "cat /dev/null > src/main.rs" was asked, previewed nothing, took no backup: the wrapper hid the command. The string inside is read as the command it runs (v0.18, #62).
Sep : wrap had never executed a command: the runner's own shell resolved back through the shims. An allowed command recursed until killed. Fixed; the happy-path test that was never written now exists (v0.18, #65).
Sep : the first live Codex session - the hook fired on every command and Codex rejected every answer ; allow, ask and a deny that exited 2 all failed the hook and fell open to Codex's own prompt. Codex honours one verdict, deny, on stdout with exit 0. Fixed; a hard stop now lands in Codex's UI as "Blocked by hook" with the blast radius (v0.18, #74).
The rule for this thread : the command, what the gate did, what changed. No adjectives.
This one was found by reading, not by losing anything.
An open issue on the Claude Code tracker: an agent composed
find … -exec sh -c "rm -rf "$1"/* 2>/dev/null" _ {} ;
$1 was empty at execution. The shell ran rm -rf /* as root on a production host, detached, stderr silenced. WSL2, so it reached the Windows drives through /mnt. TRIM SSDs. Gone.
I ran that command through termaxa expecting it to hold up. The verdict was right: rm -rf is a hard stop either way. The preview underneath it said:
target : $1/* contains : (path does not exist — nothing to delete)
Nothing to delete. About rm -rf /*.
The variable detector tested whether the character after $ was a letter. $SID, $HOME, ${1} flagged. $1 not. So the resolver took $1 for a directory name, didn't find one, and reassured.
That's the incident's own mistake one layer up: something read $1, assumed it meant what it said, and was wrong in the comfortable direction.
v0.18.2, out today. $1..$9, $@, $*, $#, $?, $$, $!, $0 are variables now. Same command:
target : $1/* ⚠ UNRESOLVED: contains a shell variable — if empty at execution, the delete lands on a different path than written ✗ insurance : cannot back up a path unknown until the shell expands it
One test assertion was wrong and got corrected instead of worked around: rm -rf costs$5 was asserted safe as "a price in a filename." The shell disagrees; costs$5 expands to costs.
What it doesn't do, stated plainly: nothing pre-exec can evaluate post-expansion. The expansion happens after the decision. What a gate can do is refuse to be confident about a target it can't resolve. That is a warning, not a guarantee, and it would not have saved this one — the copies live on the filesystem that was being deleted.
Third release in three days. Each one a fix with someone else's receipt behind it.
Yes, termaxa check "<command>" is exactly that, and it spawns nothing. Just ran it on a real directory:
-------------------------
$ termaxa check "rm -rf ./build"
decision deny
reason Recursive force delete blocked by default policy.
delete impact
target : /tmp/dry/build
contains : 13 files across 2 directories
insurance : copy 1 path(s) to .termaxa/backups before deletion
---------------------------
The 13 files are still there. When an agent is driving rather than you, the hook delivers the same preview as the approval prompt before the command runs, and takes the copy first.
Your plan-only checkpoint is the better habit and the gate doesn't replace it — git covers tracked files, the copy covers the untracked ones and the ones git never sees. The case check can't help with is the one in that post: $SID is empty at execution, so it refuses to guess the path rather than printing a confident count.
@FastfixAI Agreed, and it's my top open issue. Codex deleted a file through apply_patch last week with no shell command in the trace. A shell gate sees none of that. The interface is the tool call, and that's where this has to end up.
This one is mine.
On Saturday I widened termaxa's default policy to let the ordinary dev loop through without asking. npx * went in with it, on the reasoning that a build runs the project's own code either way.
Someone on r/ClaudeCode had already posted, the day before, that Claude Code, given a UI task, ran npx supabase db reset and wiped a local database holding months of their projects, notes and deadlines.
I read it yesterday and checked it against my own starter policy expecting a deny. It was an allow. Silently.
The reasoning holds for cargo test, where the head names the action. It fails for npx, which is a placeholder for anything on npm, and for npm run, which is a placeholder for anything in package.json.
v0.18.1, out today: the run-anything wildcards are gone, and db reset / migrate reset / --force-reset / drizzle-kit drop are hard stops.
The rule that came out of it: widen an allow list by the head that names the action, never by the head that names a runner.
If you installed v0.18.0, init never rewrites an existing policy, so the wildcards are still in yours. termaxa check "npx supabase db reset" should say deny.
https://t.co/9hSIBpfDcg
Wrote this after realizing the failure modes that hurt most — rotated API keys, broken SPF, provider-side suppression — all ship a green build when a fake SMTP server is in the loop.
Includes a "when to keep Mailtrap" section, because it's genuinely good at its actual job.
Mailtrap catches your email before it ever leaves the building. That's the feature — and it's exactly why your CI never tests the thing that actually breaks.
The two tests people conflate: does the mail render, and did the provider accept it.
https://t.co/HYeUGoqOP1
Measured, in case it helps anyone: under termaxa's default policy, rm .env, rm -f .env, mv .env .env.bak and cat /dev/null > .env are all refused — "destroys credentials that are not in the repo" — and anything it lets delete gets a copy taken first. One spelling it only asks about, git rm .env; that's a bug on my side, filed today. Sorry it happened.
Sorry — this is the worst version of it, the recovery attempt taking the last backup with it.
If you haven't already: stop writing to that drive. rmdir /s /q skips the Recycle Bin, but on NTFS the data usually stays on disk until something overwrites it; an undelete tool run from another drive or a boot USB can get a lot back. The shadow copy going is the real loss, and it's why "recover it for me" is the one job an agent shouldn't be given.
For what it's worth, I ran your exact command through the gate I'm building (https://t.co/JL9yQv3GPy): denied outright — rmdir /s is a hard stop in the default policy, the $(...) is flagged as unreadable, and the backup directory is off limits to the agent by the same rule. The honest limit: that's Codex on Windows, and I haven't verified the Codex hook fires there yet. If you're willing to say how the subagent was launched, that's exactly the case I'd want to test.
Adult supervision, yes - and the supervising part can be mechanical. rm -f doesn't go to a trash; once it runs there's nothing to recover after, so the only moment that matters is before.
A hook on the agent's shell commands sees rm -f tetherpant.png, names the file, copies it aside, and then asks. I ran your exact command through termaxa's default policy just now: ask, destructive flag flagged, one file named, copy-before-delete. https://t.co/JL9yQv3GPy
The honest limit: "processed in place" - if that was Claude's own file tool rather than a shell command, a shell hook never saw the overwrite, only the delete.
@buger point is the one to keep: git only gives back what was committed. Untracked files and everything since the last commit are gone, and a wipe that got committed isn't a checkout away.
The other half is that the deletion moment itself is interceptable. A pre-execution hook on the agent's shell commands can show what a delete would remove (files, directories, anything outside the repo), copy it first, and then ask, with recursive deletes outside the file you asked for denied by default. That's what I'm building in termaxa (https://t.co/JL9yQv3GPy).
Honest limit: if Codex removed the files through its own file tool rather than a shell command, a shell hook never sees it. Do you know which it was? That decides which gate would have caught it.
The most important line in your screenshot isn't the deletion - it's "no rm, trash, reset, or deletion command was found from our agents." That's a record problem, and it narrows things: either the deletion took a non-shell path (a file tool, or /app being a mount that detached - /app vanishing while .git survived reads more like a volume event than an rm), or the agent's own log is incomplete. From inside the agent's account you can't tell which, which is the actual failure.
Two checks that can: (1) grep the Codex session log for apply_patch / file-tool operations touching /app around 17:27 - deletions via file tools don't show up as "commands"; (2) if /app is a bind mount or Docker volume, check docker events / mount logs for that minute - unmounts look exactly like this.
Glad Time Machine + edit replay got you to 17:27; that's one of three recoveries this month rebuilt from session records.
For what it's worth, I build an open-source gate for agent shell commands (https://t.co/JL9yQv3GPy) that keeps its own record independent of the agent's - it would have proven "no gated command deleted this," which is exactly the question you can't answer right now. Honest limits: Codex support is my open help-wanted issue #10, and if this went through a file tool or a mount, a command gate never saw it - that boundary is documented, not hidden.
Stop the agent first - right now. You said you're telling it to "recover everything": that means the same agent is actively WRITING to that disk, and every write permanently overwrites deleted data. Deleted files are recoverable until their sectors get reused. Pause Codex, stop downloads/installs on that drive, and flip those Full-access / Auto-review toggles off before anything else.
Then, in this order:
1. Recycle Bin. cmd's rd/del bypasses it, but the PowerShell-refused attempts may have gone through paths that don't - it's a free check.
2. Android Studio Local History - the one most people miss: IntelliJ/AS keeps its own edit history OUTSIDE your project folders, in the IDE's system directory (%LOCALAPPDATA%\Google\AndroidStudio<version>\system). Recent source edits often survive even when the whole project folder is gone.
3. winfr (Microsoft's free File Recovery tool) or Recuva - run it from a DIFFERENT drive or a USB stick, pointed at the wiped one. With writes stopped, NTFS recovery odds are real.
4. Fragments: git remotes, Gradle caches, and your Codex session logs - they contain the code the agent read and wrote, and people have rebuilt from exactly those this week. Your 1-3 month backup is the floor, not the ceiling.
5. AVDs / Android instances regenerate - spend zero recovery effort there. Source first.
If you post the exact delete command from the session log, I can tell you more precisely what's recoverable.
For afterwards: I build an open-source gate for this class (https://t.co/JL9yQv3GPy) - destructive commands denied by default, blast radius shown before y/N, backup taken before execution. Honest limit: it's tested live on Claude Code and Cursor; Codex hook support is my open help-wanted issue #10. Once your machine is back, that issue could use exactly your setup.
The detail most of these 600 replies will miss: the guard didn't fail passively - it was the trigger. PIN_CANARY assigned before validation; validation fails; the EXIT trap fires; cleanup rm -rf's the unvalidated value. The refusal path performed the deletion.
That's the fourth public incident in six weeks with the same root - a variable reaching rm -rf before anything checked what it held: the $TEMP_WT worktree wipe, OpenAI's $HOME cleanup postmortem, a /d/website drive deletion last week, now $PIN_CANARY. Different guards each time, same class: validate-then-assign, never assign-then-validate, and cleanup paths are production paths.
I build an open-source gate for this class (https://t.co/JL9yQv3GPy) - rm -rf denies by default with blast radius shown and a backup taken before y/N, and a delete target carrying an unresolved variable refuses to resolve rather than printing a confident wrong path. But honest limit first, because this thread is literally about a guard that oversold: a deletion buried inside a script the agent wrote and then ran is opaque to a command gate. That gap is documented in my issue tracker, not hidden in my README. Your interrupt was the last line of defense, and it worked - sorry it had to.
The decomposition theory matches what I've watched live - and your "each node permission restricted" line is the interesting seam. Permissions bound ACCESS: which tools a node may use. They don't bound CONSEQUENCE: what an allowed tool's command does. Your deploy node, correctly permitted to run shell, can still run the malformed cleanup that just ate someone's $HOME in OpenAI's own postmortem.
That's the layer I build - a pre-execution gate that previews blast radius (rows a DROP hits, commits a force-push loses), takes the backup BEFORE the y/N, and catches an agent retrying the same destructive goal in different syntax. On your snapshot point: backup-before beats snapshot-schedule because it fires at the moment of maximum information - the backup and the triggering command point at each other in the audit log.
Open source, and wiring it under a new harness is mostly a hook-format adapter: https://t.co/JL9yQv3GPy. If your nodes shell out, happy to help you slot it under every node - your checkpoints for workflow state, the gate for execution consequence.
The instinct is right, and one refinement from building this exact thing: snapshot-AFTER and backup-BEFORE are different guarantees. A snapshot schedule can miss the window between last snapshot and the bad command. A pre-execution hook backs up at the moment of maximum information - you know exactly which command triggered it, so the backup and the audit entry point at each other. Made that mistake before landing on the hook. Open source if useful for comparison: https://t.co/JL9yQv3GPy
The repo-as-plugin fix is right, and I'd keep it. But notice all three of your reasons are about recovering AFTER the loss. The deletion itself was interceptable: a PreToolUse hook sees `rm -rf ~/.claude` before it runs - recursive deletes deny by default, the prompt shows the blast radius (150 skills, every file counted), and a backup is taken before you even answer y/N.
I built an open-source gate for exactly this after watching the same failure live (https://t.co/JL9yQv3GPy). Honest limit, documented as issue #1: agents' native file-write tools bypass shell hooks, so your version control still matters. It's layers - insurance before the command, git after. Yours had neither that day.
The “instructions explicitly say to verify” part is the bit I'd focus on. Instructions are a request, not a constraint. An agent that ignores them once can ignore them again — no amount of prompt tightening changes that. You need something outside the agent's control to enforce the boundary.