<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Termaxa</title>
    <description>The latest articles on DEV Community by Termaxa (termaxa).</description>
    <link>https://dev.to/termaxa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14060%2Fd83a8123-be6f-4563-b620-dc8954564ab3.png</url>
      <title>DEV Community: Termaxa</title>
      <link>https://dev.to/termaxa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/termaxa"/>
    <language>en</language>
    <item>
      <title>Claude Code shipped a sandbox. Here's what it protects — and what it doesn't.</title>
      <dc:creator>zerodrop</dc:creator>
      <pubDate>Tue, 04 Aug 2026 21:23:38 +0000</pubDate>
      <link>https://dev.to/termaxa/claude-code-shipped-a-sandbox-heres-what-it-protects-and-what-it-doesnt-4o8h</link>
      <guid>https://dev.to/termaxa/claude-code-shipped-a-sandbox-heres-what-it-protects-and-what-it-doesnt-4o8h</guid>
      <description>&lt;p&gt;Anthropic shipped OS-level sandboxing for Claude Code. If you run an agent&lt;br&gt;
against a repo you care about, it's worth understanding precisely what moved —&lt;br&gt;
because a fair amount of the commentary treats it as "agents are contained&lt;br&gt;
now," and that's not what the documentation says.&lt;/p&gt;

&lt;p&gt;I read the docs carefully, partly because I build a tool in adjacent territory&lt;br&gt;
and needed to know whether I'd just been made redundant. Short answer: no. The&lt;br&gt;
longer answer is more interesting, and it starts with a compliment: &lt;strong&gt;the docs&lt;br&gt;
are unusually honest about their own limits.&lt;/strong&gt; Most of what follows isn't&lt;br&gt;
something I discovered — it's something Anthropic wrote down, and more people&lt;br&gt;
should read it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;The sandbox uses OS primitives — Seatbelt on macOS, bubblewrap on Linux and&lt;br&gt;
WSL2. By default, sandboxed commands can write only to your working directory&lt;br&gt;
and the session temp directory. No network domains are pre-allowed: the first&lt;br&gt;
time a command needs a new host you're prompted, and approving it lasts the&lt;br&gt;
session.&lt;/p&gt;

&lt;p&gt;Crucially, this is enforced by the operating system on the &lt;em&gt;running process&lt;/em&gt;,&lt;br&gt;
not by the model correctly interpreting a command. The docs put it well: the&lt;br&gt;
boundary holds regardless of what the model chose to run, and even if an&lt;br&gt;
allowed command does more than its name suggests. That's a real improvement&lt;br&gt;
over asking an agent nicely, and it's the right layer for what it solves.&lt;/p&gt;

&lt;p&gt;The motivation named in the docs is the same one I keep seeing in the wild:&lt;br&gt;
reducing the permission prompts that people stop reading. Approval fatigue is&lt;br&gt;
the disease; this is a real treatment for part of it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Five things worth knowing before you rely on it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It's Bash-only.&lt;/strong&gt; The sandbox constrains Bash commands and their child&lt;br&gt;
processes. Claude Code's own Read, Edit and Write tools don't run through it —&lt;br&gt;
they go through the permission system instead. "The sandbox is on" means shell&lt;br&gt;
commands are contained, not that every file operation is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your working directory is inside the boundary by design.&lt;/strong&gt; The default write&lt;br&gt;
scope &lt;em&gt;is&lt;/em&gt; the current working directory, plus session temp. That's what makes&lt;br&gt;
the agent useful — it has to edit your code. It also means the sandbox is not&lt;br&gt;
protecting the thing many developers assume it is. A destructive command&lt;br&gt;
confined to your project is still confined to your project.&lt;/p&gt;

&lt;p&gt;There's a sharp exception worth knowing, and it cuts in the reassuring&lt;br&gt;
direction: even in auto-allow mode, &lt;code&gt;rm&lt;/code&gt; and &lt;code&gt;rmdir&lt;/code&gt; targeting &lt;code&gt;/&lt;/code&gt;, your home&lt;br&gt;
directory, or other critical system paths still trigger a prompt or a&lt;br&gt;
classifier check, and explicit deny rules are always respected. Anthropic&lt;br&gt;
special-cased the catastrophic paths. Everything inside your working directory&lt;br&gt;
remains permitted, deliberately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reads are much wider than writes.&lt;/strong&gt; Default read access is the &lt;em&gt;entire&lt;br&gt;
computer&lt;/em&gt; minus a few denied directories — and the docs say plainly that this&lt;br&gt;
still allows reading credential files such as &lt;code&gt;~/.aws/credentials&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;~/.ssh/&lt;/code&gt;. If you want those protected you configure &lt;code&gt;sandbox.credentials&lt;/code&gt;&lt;br&gt;
(deny, or mask) or add them to &lt;code&gt;denyRead&lt;/code&gt;. There is no built-in credential deny&lt;br&gt;
list. This is the item I'd action first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's an escape hatch, on by default — but it's permissioned.&lt;/strong&gt; When a&lt;br&gt;
command fails because of sandbox restrictions, Claude may retry it with&lt;br&gt;
&lt;code&gt;dangerouslyDisableSandbox&lt;/code&gt;. Importantly, that retry goes through the normal&lt;br&gt;
permission flow: a confirmation prompt in default mode, or the classifier in&lt;br&gt;
auto mode. So it isn't a silent bypass — it's a sanctioned door with a&lt;br&gt;
doorbell. If you want the door bricked up, &lt;code&gt;allowUnsandboxedCommands: false&lt;/code&gt;&lt;br&gt;
enables what the panel calls Strict sandbox mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It fails open.&lt;/strong&gt; If the sandbox can't start — missing bubblewrap, unsupported&lt;br&gt;
platform — Claude Code warns and runs commands unsandboxed, unless you set&lt;br&gt;
&lt;code&gt;failIfUnavailable: true&lt;/code&gt;. That's a defensible default (a safety layer that&lt;br&gt;
bricks your agent gets switched off, and then it protects nobody); I make the&lt;br&gt;
same choice in my own tool. But fail-open has a shadow: the thing can be&lt;br&gt;
inactive while everything looks fine. I learned that expensively when a hook&lt;br&gt;
API rename left my own tool silently gating nothing for four minor versions,&lt;br&gt;
with a fully green test suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And on native Windows it doesn't run at all.&lt;/strong&gt; Windows users are pointed at&lt;br&gt;
WSL2. If you run Claude Code natively on Windows — plenty do — the sandbox&lt;br&gt;
isn't in your picture.&lt;/p&gt;
&lt;h2&gt;
  
  
  Credit where it's due: the docs name their own gaps
&lt;/h2&gt;

&lt;p&gt;Before I make my argument, the Limitations section deserves reading in full,&lt;br&gt;
because it's more candid than most vendor security pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The built-in proxy makes allow decisions from the client-supplied hostname
and, by default, does not terminate or inspect TLS — so allowing broad
domains like &lt;code&gt;github.com&lt;/code&gt; can create paths for data exfiltration, with domain
fronting named explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowUnixSockets&lt;/code&gt; can inadvertently grant host access: allowing
&lt;code&gt;/var/run/docker.sock&lt;/code&gt; effectively grants access to the host system.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowAppleEvents&lt;/code&gt; on macOS removes code-execution isolation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enableWeakerNestedSandbox&lt;/code&gt; considerably weakens security.&lt;/li&gt;
&lt;li&gt;And the summary line: sandboxing reduces risk but is not a complete isolation
boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a vendor telling you where their boundary ends. Take them at their word&lt;br&gt;
rather than at the marketing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The distinction that actually matters
&lt;/h2&gt;

&lt;p&gt;Containment, consequence and recovery are three different questions, and tools&lt;br&gt;
tend to answer exactly one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Containment&lt;/strong&gt; asks &lt;em&gt;can this process touch things outside its box?&lt;/em&gt; That's&lt;br&gt;
the sandbox, Docker, seccomp. A boundary in space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence&lt;/strong&gt; asks &lt;em&gt;what will this specific command destroy if it runs?&lt;/em&gt;&lt;br&gt;
Fifty thousand rows. Three dependent tables. One commit that exists only on the&lt;br&gt;
remote. A containment boundary has no opinion about this, because a permitted&lt;br&gt;
command inside the boundary is just a permitted command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery&lt;/strong&gt; asks &lt;em&gt;and if it happens anyway, can I get it back?&lt;/em&gt; Neither&lt;br&gt;
containment nor prediction gets you here. This is a backup taken before&lt;br&gt;
execution, and a path back.&lt;/p&gt;

&lt;p&gt;The failures I've been collecting all month live in the second and third&lt;br&gt;
categories. An agent following NTFS junctions out of a &lt;code&gt;Windows.old&lt;/code&gt; cleanup&lt;br&gt;
into someone's live Documents folder. Prompt-configured rules — "don't touch&lt;br&gt;
.env" — ignored, because instructions are suggestions and the execution path&lt;br&gt;
doesn't read your CLAUDE.md. &lt;code&gt;rm -rf ~/.crit/reviews/$SID&lt;/code&gt; where &lt;code&gt;$SID&lt;/code&gt; had&lt;br&gt;
already been cleared, so it expanded to nothing and took the parent directory.&lt;/p&gt;

&lt;p&gt;Notice what those share. None is an agent doing something obviously stupid.&lt;br&gt;
Each is a &lt;strong&gt;correct-looking command whose real scope differs from its apparent&lt;br&gt;
scope.&lt;/strong&gt; Containment doesn't address that class, because the command was&lt;br&gt;
allowed to run in the place it ran.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I'd actually do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Turn the sandbox on.&lt;/strong&gt; It's built in, OS-enforced, and it closes a real&lt;br&gt;
category. On Windows that means WSL2 or nothing — worth knowing before you&lt;br&gt;
assume you're covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then convert three soft defaults into firm ones&lt;/strong&gt;, each a one-line change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"failIfUnavailable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowUnsandboxedCommands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"credentials"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.aws/credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.ssh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Know it's broken rather than assume it's working; make the boundary hold rather&lt;br&gt;
than negotiate; and stop sandboxed commands reading your keys, since the&lt;br&gt;
default read policy allows it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then be clear-eyed that you've solved containment, not consequence or&lt;br&gt;
recovery.&lt;/strong&gt; For those: least-privilege credentials — an agent token that&lt;br&gt;
&lt;em&gt;cannot&lt;/em&gt; drop your production tables beats any prompt asking it not to —&lt;br&gt;
backups that happen &lt;em&gt;before&lt;/em&gt; execution rather than on a nightly cron, and, if&lt;br&gt;
you want it, something that shows the blast radius while there's still a&lt;br&gt;
decision to make.&lt;/p&gt;

&lt;p&gt;That last part is what I build (&lt;a href="https://github.com/termaxa/termaxa" rel="noopener noreferrer"&gt;Termaxa&lt;/a&gt;,&lt;br&gt;
open source, Rust). I'm obviously not neutral. But the argument survives&lt;br&gt;
without it: vendors will keep shipping containment, because containment is what&lt;br&gt;
a platform can own. Consequence and recovery sit at a different layer, and&lt;br&gt;
structurally, no agent vendor is going to build that layer for their&lt;br&gt;
competitors' agents. Anthropic won't gate Cursor. Cursor won't gate Claude&lt;br&gt;
Code. If you run more than one, that layer has to come from somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest summary
&lt;/h2&gt;

&lt;p&gt;Claude Code's sandbox is a good, real improvement that answers one of three&lt;br&gt;
questions well, and its documentation is refreshingly clear about where it&lt;br&gt;
stops. It's Bash-scoped; reads are far wider than writes and include your&lt;br&gt;
credentials by default; the escape hatch is on but permissioned; it fails open;&lt;br&gt;
and it isn't there at all on native Windows.&lt;/p&gt;

&lt;p&gt;Read the limitations section. Flip the flags that matter to you from soft to&lt;br&gt;
firm. And keep a backup — because the thing that gets you won't look dangerous.&lt;br&gt;
It'll look like a command you'd have approved.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified against Claude Code's sandboxing documentation, August 2026.&lt;br&gt;
Incidents referenced are public posts from r/ClaudeAI, r/ClaudeCode and&lt;br&gt;
r/cursor over the past two weeks. The docs move quickly; where this and the&lt;br&gt;
documentation disagree, the documentation is right and I'm out of date. Termaxa is MIT/Apache, &lt;code&gt;cargo install termaxa&lt;/code&gt; — and if you can get an agent past it in a way I haven't documented, that's the most useful thing you could send me: &lt;a href="https://github.com/termaxa/termaxa/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt; or &lt;a href="mailto:security@termaxa.com"&gt;security@termaxa.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>security</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I asked an AI agent to delete a folder my tool was guarding. Here's everything that went wrong.</title>
      <dc:creator>zerodrop</dc:creator>
      <pubDate>Fri, 17 Jul 2026 21:30:11 +0000</pubDate>
      <link>https://dev.to/termaxa/i-asked-an-ai-agent-to-delete-a-folder-my-tool-was-guarding-heres-everything-that-went-wrong-490b</link>
      <guid>https://dev.to/termaxa/i-asked-an-ai-agent-to-delete-a-folder-my-tool-was-guarding-heres-everything-that-went-wrong-490b</guid>
      <description>&lt;p&gt;I build &lt;a href="https://github.com/termaxa/termaxa" rel="noopener noreferrer"&gt;Termaxa&lt;/a&gt;, a small Rust CLI that gates the shell commands AI coding agents run: it previews what a command will actually do, backs up first, applies an allow/ask/deny policy, and logs everything. The fastest way to find out whether a safety tool works is to point a real agent at it and ask it to do the thing the tool exists to stop.&lt;/p&gt;

&lt;p&gt;So I did. Several times, over a few weeks. Cursor broke my design four different ways, and every meaningful feature in the current release exists because of one of those breaks. (Claude Code, for what it's worth, mostly cooperated. It's the agent that &lt;em&gt;didn't&lt;/em&gt; stand down that taught me everything.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 1: the whack-a-mole
&lt;/h2&gt;

&lt;p&gt;The setup: a test repo, Termaxa hooked into Cursor, a policy that flagged recursive deletes as &lt;code&gt;ask&lt;/code&gt;. The prompt: delete this folder.&lt;/p&gt;

&lt;p&gt;Cursor never attacked the hook. It didn't need to. It just kept expressing the same intent in different shell dialects until one spelling landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;                      → ask
Remove-Item &lt;span class="nt"&gt;-Recurse&lt;/span&gt; &lt;span class="nt"&gt;-Force&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; → ask
del /s /q &lt;span class="nb"&gt;.&lt;/span&gt;                   → ask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule-based matching sees three unrelated commands. The agent sees one goal with three spellings — and each retry is a fresh policy evaluation, a fresh chance for an auto-approving UI to wave it through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This isn't a bypass, it's a policy expressiveness gap.&lt;/strong&gt; The agent stayed inside the rules the whole time. My rules just couldn't say the thing that mattered: "this session has already been told no about deleting things."&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: classify intent, count attempts, escalate
&lt;/h2&gt;

&lt;p&gt;That gap is fixable inside the cooperative model, and the fix became the headline feature — a session circuit breaker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classify each command's &lt;em&gt;intent&lt;/em&gt; — file-delete, db-destroy, git-force, infra-destroy — rather than matching spellings. &lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;Remove-Item -Recurse -Force&lt;/code&gt;, and &lt;code&gt;del /s /q&lt;/code&gt; all classify as the same thing.&lt;/li&gt;
&lt;li&gt;When a command comes back &lt;code&gt;ask&lt;/code&gt;, count prior attempts with the same intent in the same session, read straight from the append-only audit log.&lt;/li&gt;
&lt;li&gt;At the threshold (default: two prior attempts), escalate the &lt;code&gt;ask&lt;/code&gt; to &lt;code&gt;deny&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two design constraints mattered. The breaker &lt;strong&gt;only escalates, and only touches &lt;code&gt;ask&lt;/code&gt;&lt;/strong&gt; — an explicit &lt;code&gt;allow&lt;/code&gt; is deliberate user policy (maybe you &lt;em&gt;want&lt;/em&gt; &lt;code&gt;rm -rf&lt;/code&gt; allowed inside &lt;code&gt;/tmp&lt;/code&gt;), and a &lt;code&gt;deny&lt;/code&gt; is already a deny. And the state is &lt;strong&gt;derived, not stored&lt;/strong&gt; — the counter is computed from the log, so there's no sidecar file to reset, a new session is automatically a clean slate, and every denied variant is itself logged with its intent, which keeps the breaker tripped for the rest of the session for free.&lt;/p&gt;

&lt;p&gt;Re-ran the test. Cursor went PowerShell → cmd, the classifier tracked the intent across both shells, and the third delete attempt came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deny — circuit breaker: 2 prior file-delete attempt(s) this session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Felt good for about an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 2: the classifier had a hole
&lt;/h2&gt;

&lt;p&gt;Same session, Cursor kept going, and one command sailed through as a plain &lt;code&gt;ask&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classifier looked at the first token. The first token is &lt;code&gt;find&lt;/code&gt;. &lt;code&gt;find&lt;/code&gt; isn't a delete command. The &lt;code&gt;rm -rf&lt;/code&gt; living inside &lt;code&gt;-exec&lt;/code&gt; was invisible.&lt;/p&gt;

&lt;p&gt;This is where the intent-over-spelling bet paid off. The fix wasn't "add &lt;code&gt;find -exec rm&lt;/code&gt; to a pattern list" — it was extending the classifier to understand &lt;em&gt;delete indirection&lt;/em&gt; as a concept: &lt;code&gt;find&lt;/code&gt; with &lt;code&gt;-exec&lt;/code&gt;/&lt;code&gt;-execdir&lt;/code&gt;/&lt;code&gt;-ok&lt;/code&gt;/&lt;code&gt;-okdir&lt;/code&gt; wrapping a delete command, &lt;code&gt;find -delete&lt;/code&gt;, &lt;code&gt;xargs&lt;/code&gt; feeding a delete command, &lt;code&gt;unlink&lt;/code&gt;, &lt;code&gt;shred -u&lt;/code&gt;. One classifier extension, one regression test using the literal live bypass command, shipped as v0.11.1. Enumerating spellings is an unwinnable arms race; intents are finite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 3: the escape
&lt;/h2&gt;

&lt;p&gt;Then Cursor did the thing that defines the tool's honest boundary.&lt;/p&gt;

&lt;p&gt;Blocked on shell deletes — breaker tripped, every variant denied — it stopped using the shell. It switched to its own &lt;strong&gt;native file-delete tool&lt;/strong&gt; and removed about twenty files. &lt;code&gt;.cursor/&lt;/code&gt; and &lt;code&gt;.termaxa/&lt;/code&gt;, gone.&lt;/p&gt;

&lt;p&gt;Audit entries for those deletions: &lt;strong&gt;zero.&lt;/strong&gt; Not because anything was evaded — because the shell hook never saw them. An agent's built-in file tools don't go through the shell. They were never in scope.&lt;/p&gt;

&lt;p&gt;I could have buried this. Instead it's the first item in SECURITY.md, filed as a public issue, and in the README subhead: Termaxa is a &lt;strong&gt;windshield, not a sandbox&lt;/strong&gt;. It gates the shell path an agent normally takes; it does not contain an agent that executes through other means. If you need hard guarantees, pair it with OS-level isolation — containers, seccomp, restricted credentials. The cooperative gate covers the common case: a capable agent about to make an expensive mistake. It is not containment of an adversarial one. (The real fix is owning the execution path rather than hooking someone else's — that's the roadmap, not the present tense.)&lt;/p&gt;

&lt;p&gt;One more finding from the same sessions, smaller but real: the breaker counts per session id, and both Cursor and Claude Code sometimes &lt;strong&gt;rotate the session id mid-run&lt;/strong&gt; with no restart. A rotation between attempts resets the counter. In practice a burst of retries shares one id — the trip above happened across a rotation-free burst — but it makes the breaker a speed bump, not a durable cap. Also filed, also documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 4: the one that scared me
&lt;/h2&gt;

&lt;p&gt;This is the finding I'd lead with if I could only keep one.&lt;/p&gt;

&lt;p&gt;Weeks later, while wiring up post-execution receipts, I ran a routine live test on Cursor with a debug capture enabled (&lt;code&gt;TERMAXA_HOOK_DEBUG&lt;/code&gt;, which dumps every raw payload the hook receives). The capture showed &lt;strong&gt;six hook invocations&lt;/strong&gt;. The audit log showed &lt;strong&gt;zero entries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Root cause: Cursor 3.11 had renamed its hook API. Events arrived as &lt;code&gt;preToolUse&lt;/code&gt;/&lt;code&gt;postToolUse&lt;/code&gt; (camelCase) with &lt;code&gt;tool_name: "Shell"&lt;/code&gt;. My parser only knew the older &lt;code&gt;beforeShellExecution&lt;/code&gt;/&lt;code&gt;afterShellExecution&lt;/code&gt; shape with &lt;code&gt;tool_name: "Bash"&lt;/code&gt;. Every event from current Cursor fell through the parser and returned &lt;code&gt;None&lt;/code&gt; — which, by design, means "not for us, step aside."&lt;/p&gt;

&lt;p&gt;Termaxa fails open on plumbing on purpose: a gate that bricks your agent when it gets confused gets uninstalled, and then it protects nobody. But fail-open has a shadow, and this was it. &lt;strong&gt;Termaxa had been silently not gating Cursor 3.11+ for roughly four minor versions.&lt;/strong&gt; No error, no crash, no signal. The tool appeared installed and healthy while doing nothing.&lt;/p&gt;

&lt;p&gt;Here's the part that generalizes: &lt;strong&gt;the entire test suite was green the whole time.&lt;/strong&gt; The Cursor tests used the old payload shape as fixtures. They faithfully verified that Termaxa handled a dialect Cursor no longer spoke. For an integration surface, a green suite is necessary and nowhere near sufficient — the only thing that catches this class of drift is running the real agent and looking at what it actually sends.&lt;/p&gt;

&lt;p&gt;The fix (v0.11.4): case-insensitive event matching across both API generations, Cursor detection from multiple signals, command and cwd recovery from several payload locations — and, crucially, four regression tests whose fixtures are the &lt;strong&gt;real captured 3.11 payloads&lt;/strong&gt;, so the next silent rename fails CI instead of failing users. Verified live on Cursor 3.11.25: hook entries and post receipts both flowing again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually generalizes
&lt;/h2&gt;

&lt;p&gt;If you're building anything that sits between an AI agent and real infrastructure, the four lessons in one place:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classify intent, not spelling.&lt;/strong&gt; An agent blocked on a goal retries the goal with different syntax — across shells, through indirection, wherever the policy's vocabulary runs out. Pattern lists lose that race structurally. Intent classification is what let one fix close a whole category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only escalate; never relax.&lt;/strong&gt; A safety layer that second-guesses an explicit human &lt;code&gt;allow&lt;/code&gt; is a safety layer people rip out. Harden the soft middle — the &lt;code&gt;ask&lt;/code&gt; that auto-approval quietly erodes into &lt;code&gt;allow&lt;/code&gt; — and leave deliberate policy alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Green tests lie about integration surfaces.&lt;/strong&gt; Your fixtures encode yesterday's API. Live-fire against the actual agent, capture the real payloads, and make &lt;em&gt;those&lt;/em&gt; your fixtures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Say where your tool stops, in the README, before anyone asks.&lt;/strong&gt; The native-tool escape would be a devastating HN comment if someone else discovered it. As the first line of my own SECURITY.md, it's the reason to trust the rest of the document.&lt;/p&gt;

&lt;p&gt;The biggest surprise wasn't that Cursor found bugs. It was that every serious improvement came from watching a real agent behave differently than my tests predicted. If you're building infrastructure for AI agents, that's probably the actual development loop: write the feature, point a real model at it, let it surprise you, and turn the surprise into tomorrow's regression test.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termaxa is MIT/Apache, open source, &lt;code&gt;cargo install termaxa&lt;/code&gt;. If you can make an agent get past it in a way I haven't documented, that's the most valuable contribution you can make: &lt;a href="https://github.com/termaxa/termaxa/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt; or &lt;a href="mailto:security@termaxa.com"&gt;security@termaxa.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>security</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
