<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ai-Jail | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/ai-jail/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/ai-jail/index.xml" rel="self" type="application/rss+xml"/><item><title>AI-Jail: Security Update, Docker Goes Opt-In</title><link>https://akitaonrails.github.io/en/2026/07/25/ai-jail-security-update-docker-opt-in/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/07/25/ai-jail-security-update-docker-opt-in/</guid><pubDate>Sat, 25 Jul 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Someone opened an important issue on the &lt;a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener"&gt;ai-jail&lt;/a&gt; repo this morning: &lt;a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener"&gt;issue #88&lt;/a&gt;, reported by &lt;a href="https://github.com/mdindoffer"target="_blank" rel="noopener"&gt;@mdindoffer&lt;/a&gt;, titled &amp;ldquo;Sandbox escape via a docker socket passthrough (effective host root)&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The report checked out, and the fix is already available in &lt;strong&gt;v1.16.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Update to v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="update-to-v1160"&gt;&lt;/span&gt;
&lt;a href="#update-to-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;If you use ai-jail, the update is the usual drill:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Arch Linux (AUR)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yay -Syu ai-jail-bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Homebrew (macOS / Linux)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew upgrade ai-jail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# crates.io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cargo install ai-jail --force
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# mise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mise cache clear &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mise upgrade github:akitaonrails/ai-jail&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2&gt;What changed in v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="what-changed-in-v1160"&gt;&lt;/span&gt;
&lt;a href="#what-changed-in-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Up through v1.15.x, ai-jail mounted the Docker socket inside the jail automatically whenever &lt;code&gt;/var/run/docker.sock&lt;/code&gt; existed on the host. Read-write. No warning, no flag, no asking for your opinion. I documented this in the README as &amp;ldquo;favors usability,&amp;rdquo; and it was true: a coding agent often needs to run &lt;code&gt;docker compose&lt;/code&gt; to bring up a test database, and the automatic passthrough saved some configuration.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Someone opened an important issue on the <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a> repo this morning: <a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener">issue #88</a>, reported by <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>, titled &ldquo;Sandbox escape via a docker socket passthrough (effective host root)&rdquo;.</p>
<p>The report checked out, and the fix is already available in <strong>v1.16.0</strong>.</p>
<h2>Update to v1.16.0<span class="hx:absolute hx:-mt-20" id="update-to-v1160"></span>
    <a href="#update-to-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you use ai-jail, the update is the usual drill:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Arch Linux (AUR)</span>
</span></span><span class="line"><span class="cl">yay -Syu ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Homebrew (macOS / Linux)</span>
</span></span><span class="line"><span class="cl">brew update <span class="o">&amp;&amp;</span> brew upgrade ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># crates.io</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail --force
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># mise</span>
</span></span><span class="line"><span class="cl">mise cache clear <span class="o">&amp;&amp;</span> mise upgrade github:akitaonrails/ai-jail</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>What changed in v1.16.0<span class="hx:absolute hx:-mt-20" id="what-changed-in-v1160"></span>
    <a href="#what-changed-in-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Up through v1.15.x, ai-jail mounted the Docker socket inside the jail automatically whenever <code>/var/run/docker.sock</code> existed on the host. Read-write. No warning, no flag, no asking for your opinion. I documented this in the README as &ldquo;favors usability,&rdquo; and it was true: a coding agent often needs to run <code>docker compose</code> to bring up a test database, and the automatic passthrough saved some configuration.</p>
<p>Starting with v1.16.0, the behavior flipped:</p>
<ul>
<li>Socket passthrough is now <strong>off by default</strong>. It only goes in if you explicitly ask for it with the <code>--docker</code> flag or <code>no_docker = false</code> in <code>.ai-jail</code>.</li>
<li>When you enable it and a host socket exists, ai-jail prints a launch warning spelling out that this amounts to giving host root to the process inside the jail.</li>
<li><code>ai-jail status</code> now shows Docker as <code>disabled (default)</code>, so nobody thinks it got turned on by accident.</li>
<li>In <code>--lockdown</code> and browser profile modes the socket never goes in, same as before.</li>
</ul>
<p>A behavior change, yes, and a deliberate one. If your workflow depends on Docker inside the jail (mine does, in a few projects), opting in is one line in the project&rsquo;s <code>.ai-jail</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">no_docker</span> <span class="p">=</span> <span class="kc">false</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The field name is ugly (<code>no_docker = false</code> to turn it on, I know), but old configs keep parsing the same way, and <code>--no-docker</code> / <code>no_docker = true</code> work as before. Only the default changed. The details are in the <a href="https://github.com/akitaonrails/ai-jail/blob/master/releases/v1.16.0.md"target="_blank" rel="noopener">v1.16.0 release notes</a>.</p>
<h2>What issue #88 showed<span class="hx:absolute hx:-mt-20" id="what-issue-88-showed"></span>
    <a href="#what-issue-88-showed" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>mdindoffer&rsquo;s report is the kind every maintainer wants to get: precise summary, a repro in a few lines, a fix proposal. The gist:</p>
<p>ai-jail was mounting the <strong>raw</strong> Docker socket inside the sandbox, read-write. The Docker daemon runs as root on the host. So an agent inside the jail could run this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v /:/host alpine sh -c <span class="s1">&#39;cat /host/etc/shadow&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s the ballgame. Read any file on the host, as root. It defeats the tmpfs <code>$HOME</code>, <code>--mask</code>, <code>--deny-path</code>, and Landlock in one move, because the action no longer happens inside the sandbox: it happens in the daemon, which lives outside and above any namespace bwrap created. The agent doesn&rsquo;t even need to escape the jail when the jail has a door straight into the engine room.</p>
<p>In retrospect, shipping this on by default was a design mistake. I knew the passthrough was &ldquo;dangerous&rdquo; in the abstract, and I wrote as much in the README. What I hadn&rsquo;t internalized: dangerous like this, with this default, is a vulnerability.</p>
<p>mdindoffer also proposed the definitive hardening path: instead of mounting the raw socket, put a filtered proxy in front of it (in the style of <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a>) that only accepts bind mounts from paths the agent can already write to inside the jail. It&rsquo;s on the radar for a future release. To close the hole now, opt-in with an explicit warning fixes the default, which is where the problem lived.</p>
<h2>The vulnerability class: docker.sock is root<span class="hx:absolute hx:-mt-20" id="the-vulnerability-class-dockersock-is-root"></span>
    <a href="#the-vulnerability-class-dockersock-is-root" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is not the first time I&rsquo;ve run into this story, and I&rsquo;d bet it isn&rsquo;t yours either. &ldquo;Whoever has access to the Docker socket has root on the host&rdquo; is one of the classics of container security, documented by Docker itself on the <a href="https://docs.docker.com/engine/security/"target="_blank" rel="noopener">daemon attack surface</a> page: only trusted users should control the daemon, because Docker lets you share any host directory with a container, with no access restriction whatsoever.</p>
<p>The technical reason is simple. <code>dockerd</code> is a daemon that runs as root and obeys commands arriving through the API on the <code>/var/run/docker.sock</code> Unix socket. The <code>docker</code> CLI is just a client of that API. When you ask for <code>docker run -v /:/host</code>, the daemon is the one creating the container and mounting the host&rsquo;s entire filesystem inside it, with full privilege. And a process inside a container runs as uid 0 by default, which the kernel sees as real uid 0 (barring user namespace remapping, which almost nobody turns on). The math checks out: write access to the socket equals root on the host. The <code>docker</code> group is passwordless sudo by another name.</p>
<h2>The demo, on your machine<span class="hx:absolute hx:-mt-20" id="the-demo-on-your-machine"></span>
    <a href="#the-demo-on-your-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you have Docker installed and your user in the <code>docker</code> group, reproduce it right now. No sudo, no exploiting any bug:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># confirm you&#39;re a regular user</span>
</span></span><span class="line"><span class="cl">$ id
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>1000<span class="o">(</span>akitaonrails<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>1000<span class="o">(</span>akitaonrails<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>...,docker
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># try reading /etc/shadow directly: denied, as expected</span>
</span></span><span class="line"><span class="cl">$ cat /etc/shadow
</span></span><span class="line"><span class="cl">cat: /etc/shadow: Permission denied
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># now ask the daemon to do it for you</span>
</span></span><span class="line"><span class="cl">$ docker run --rm -v /:/host alpine sh -c <span class="s1">&#39;head -3 /host/etc/shadow&#39;</span>
</span></span><span class="line"><span class="cl">root:<span class="nv">$6</span>$...:...
</span></span><span class="line"><span class="cl">bin:!:...
</span></span><span class="line"><span class="cl">daemon:!:...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And if you want the whole nine yards, a root shell on your own host:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ docker run --rm -it -v /:/host alpine chroot /host /bin/bash
</span></span><span class="line"><span class="cl"><span class="c1"># id</span>
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No exploit, no 0-day. You used the official API, the documented way, and went from regular user to root in one command. That&rsquo;s exactly what an agent inside ai-jail could do until today, even with every layer (bwrap, Landlock, seccomp, rlimits) turned on.</p>
<h2>Why does this still exist?<span class="hx:absolute hx:-mt-20" id="why-does-this-still-exist"></span>
    <a href="#why-does-this-still-exist" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If everyone has known about this for over a decade, why hasn&rsquo;t anyone &ldquo;fixed&rdquo; it? Short answer: this is architecture, and architecture doesn&rsquo;t get patched.</p>
<ul>
<li>The root daemon with an all-powerful API was the design that made Docker simple to operate. Granular per-request authorization exists in the form of <a href="https://docs.docker.com/engine/extend/plugins_authorization/"target="_blank" rel="noopener">authorization plugins</a>, but it&rsquo;s opt-in, annoying to configure, and I can count on one hand the setups I&rsquo;ve seen using it.</li>
<li><a href="https://docs.docker.com/engine/security/userns-remap/"target="_blank" rel="noopener">userns-remap</a> has been around since Docker 1.10 and maps container root to an unprivileged user on the host. It ships turned off, because it breaks compatibility with images and volumes that assume uid 0.</li>
<li><a href="https://docs.docker.com/engine/security/rootless/"target="_blank" rel="noopener">Rootless mode</a> runs the whole daemon as your user, with the socket at <code>$XDG_RUNTIME_DIR/docker.sock</code>. It works, but it has network and storage limitations, and the entire internet of tutorials assumes the root daemon at the classic path.</li>
<li><a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> was born rootless and daemonless precisely because of this criticism. There&rsquo;s a whole section about it below.</li>
</ul>
<p>Bottom line: this is here to stay. Every tool that mounts <code>/var/run/docker.sock</code> into an environment &ldquo;for convenience&rdquo; opens the same hole, knowingly or not. CI mounting the socket for image builds, remote IDEs, code-server, AI agent sandboxes (hi, me), web dashboard plugins. The fix always lives on the side of whoever builds the environment.</p>
<h2>Fun fact: this is anything but new<span class="hx:absolute hx:-mt-20" id="fun-fact-this-is-anything-but-new"></span>
    <a href="#fun-fact-this-is-anything-but-new" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you&rsquo;ve been following me for a while, this whole story should give you déjà vu. Back in 2023 I recorded <a href="/2023/03/02/akitando-139-entendendo-como-containers-funcionam/">[Akitando #139] - Understanding How Containers Work</a>, where I explain what a container actually is: an ordinary Linux process, throttled by cgroups, fooled by namespaces, with its capabilities trimmed. No magic and no virtual machine. And look at what was already sitting in that episode&rsquo;s link list: a walkthrough of <a href="https://flast101.github.io/docker-privesc/"target="_blank" rel="noopener">privilege escalation via Docker</a>, demonstrating the exact <code>docker run -v /:/host</code> trick. The hole issue #88 exploited inside ai-jail is the same one I was already pointing at in a 2023 video, and it was old news long before that.</p>
<p>Nobody knows this better than Red Hat. <a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> was born there in 2018 as a direct answer to that architecture: no central daemon, rootless by default. Every container becomes a direct child of your user, via fork-exec, with no all-powerful process running as root brokering anything. The whole &ldquo;docker.sock is root&rdquo; problem doesn&rsquo;t exist in that model, because there&rsquo;s no docker.sock, no daemon, and no root.</p>
<p>And before you ask: yes, I run Podman for some things and I recommend it. But it&rsquo;s no perfect solution either, and it&rsquo;s worth understanding why.</p>
<p>What Podman does well:</p>
<ul>
<li><strong>The right architecture.</strong> Daemonless and rootless from day zero. The entire vulnerability class from this article stops making sense.</li>
<li><strong>CLI compatibility.</strong> <code>alias docker=podman</code> covers the overwhelming majority of day-to-day commands. Build, run, push, pull: same commands, same flags.</li>
<li><strong>systemd integration.</strong> <a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"target="_blank" rel="noopener">Quadlets</a> are, in my opinion, the cleanest way to run a container as a service on Linux. Docker never came close.</li>
</ul>
<p>Where it stumbles:</p>
<ul>
<li><strong>The compatibility socket isn&rsquo;t 100%.</strong> Podman offers a socket compatible with the Docker API (<code>podman.socket</code>), and plenty of tools work on top of it. But &ldquo;plenty&rdquo; isn&rsquo;t &ldquo;all&rdquo;: Testcontainers, some IDE plugins, more exotic CI tooling trip over behavioral differences. It works until the day it doesn&rsquo;t, and then you lose an afternoon debugging.</li>
<li><strong>Compose is a second-class citizen.</strong> The official <code>docker compose</code> does talk to the Podman socket, and <code>podman-compose</code> exists, but neither has the polish of the original pairing. Projects with a complicated compose file are where migrations usually get stuck.</li>
<li><strong>Rootless has a price.</strong> Rootless networking (slirp4netns, and pasta these days) has limits: no ping by default, weird source IPs, lower throughput. Images that assume uid 0 and volumes with wrong permissions need fiddling. Nothing fatal, but it&rsquo;s friction.</li>
<li><strong>Docker Desktop is a product.</strong> On macOS and Windows, Docker Desktop delivers a polished experience that Podman Desktop is still catching up to. For a lot of people, that&rsquo;s the only contact with containers they&rsquo;ll ever have.</li>
</ul>
<p>Add it all up and you get the answer to why the world stays on Docker: ecosystem inertia. Every tutorial, every CI pipeline, every example image, every <code>docker run</code> pasted from Stack Overflow assumes the root daemon at the classic path. Docker became the name of the category, the Kleenex of containers. Moving to Podman is technically easy and politically expensive: it&rsquo;s you against the accumulated knowledge of the entire internet.</p>
<p>One detail that matters for sandbox users: mounting the rootless Podman socket inside a jail is far less catastrophic than mounting the Docker one. The equivalent &ldquo;daemon&rdquo; runs as your user, so a malicious agent would gain your privileges, not root. Still bad (it could overwrite your <code>~/.ssh</code>, for one), but a whole different ballgame. Even so, ai-jail&rsquo;s default stands: don&rsquo;t mount any socket, from any runtime. Opt-in is opt-in.</p>
<h2>Best practices so this doesn&rsquo;t bite you<span class="hx:absolute hx:-mt-20" id="best-practices-so-this-doesnt-bite-you"></span>
    <a href="#best-practices-so-this-doesnt-bite-you" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The list I apply and recommend:</p>
<ol>
<li><strong>Treat the <code>docker</code> group as passwordless sudo.</strong> Before adding any user or service to it, ask whether you&rsquo;d give that thing unrestricted sudo. Same thing.</li>
<li><strong>Never mount <code>/var/run/docker.sock</code> into untrusted environments.</strong> AI agents, CI jobs running code from a stranger&rsquo;s pull request, third-party containers. Run <code>grep -r docker.sock</code> over your docker-compose files, CI manifests, and tool configs. It shows up in more places than you remember.</li>
<li><strong>Need to expose it to something semi-trusted? Use a socket proxy.</strong> <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a> and <a href="https://github.com/Tecnativa/docker-socket-proxy"target="_blank" rel="noopener">Tecnativa&rsquo;s docker-socket-proxy</a> sit between the client and the daemon with an endpoint allowlist and blocking of arbitrary bind mounts. Read-only by default; you enable only what you need.</li>
<li><strong>Prefer rootless whenever possible.</strong> Rootless Podman on Linux is the cleanest path; Docker&rsquo;s own rootless mode is the second option. The daemon stops being root and this entire class of problem loses its bite.</li>
<li><strong>In CI, build images without a privileged daemon.</strong> <a href="https://github.com/GoogleContainerTools/kaniko"target="_blank" rel="noopener">Kaniko</a> and <a href="https://buildah.io/"target="_blank" rel="noopener">Buildah</a> build images rootless, with no socket mounted in the job at all.</li>
<li><strong>Can&rsquo;t go rootless? Turn on userns-remap.</strong> It costs an afternoon of testing with your volumes and buys real isolation between container root and host root.</li>
<li><strong>In ai-jail, let the default work for you.</strong> Docker off, and <code>--docker</code> only in projects where you trust the workload the way you&rsquo;d trust a sudo. Rule of thumb: if you wouldn&rsquo;t blindly hand <code>sudo</code> to the agent in that directory, don&rsquo;t hand it <code>--docker</code> either.</li>
</ol>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A well-built sandbox loses much of its value with a back door left open for convenience. bwrap, Landlock, seccomp, and rlimits in ai-jail keep doing their jobs, but none of them can see what happens when the process inside politely asks the host&rsquo;s root daemon to mount the whole filesystem into a container. A security layer you don&rsquo;t audit becomes decoration.</p>
<p>My public thanks to <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>: clean report, minimal repro, correct severity, and a solution proposal on top. That&rsquo;s how you report a vulnerability to an open source project.</p>
<p>If you want the full context of how I use sandboxes day to day, I wrote about it in <a href="/en/2026/07/11/how-to-protect-yourself-from-agents-deleting-your-stuff/">How Do I Protect Myself From My Agents Deleting My Stuff?</a>. The ai-jail story is in <a href="/en/2026/01/10/ai-agents-locking-down-your-system/">AI Agents: Locking Down Your System</a> and in the <a href="/en/2026/03/01/ai-jail-sandbox-for-ai-agents-from-shell-script-to-real-tool/">Rust rewrite</a>.</p>
<p>Now go run the upgrade.</p>
]]></content:encoded><category>ai-jail</category><category>containers</category><category>security</category></item><item><title>What's New in My AI-MEMORY: Switch AI Agents Without Losing the Session</title><link>https://akitaonrails.github.io/en/2026/07/20/whats-new-ai-memory-switch-agents-without-losing-session/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/07/20/whats-new-ai-memory-switch-agents-without-losing-session/</guid><pubDate>Mon, 20 Jul 2026 21:00:00 -0300</pubDate><description>&lt;p&gt;It&amp;rsquo;s been a little over a month since I published &lt;a href="https://akitaonrails.github.io/en/2026/06/16/ai-memory-long-term-memory-karpathy-wiki-self-improvement-hermes-projects/"&gt;ai-memory: long-term memory (Karpathy Wiki) and self-improvement (Hermes) for your projects&lt;/a&gt;. That was June 16. In that post I explained how sessions became Markdown pages, how auto-improve promoted lessons, and why bad memory can be worse than amnesia.&lt;/p&gt;
&lt;p&gt;Version 1.1.0 shipped that same day. Today, July 20, we&amp;rsquo;re at &lt;strong&gt;1.17.1&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Version numbers alone don&amp;rsquo;t mean much, so I counted what actually happened after that post went live: &lt;strong&gt;31 releases, 55 merged pull requests, and 46 closed issues&lt;/strong&gt;. I counted &lt;strong&gt;24 groups of new user-facing features&lt;/strong&gt; in the &lt;a href="https://github.com/akitaonrails/ai-memory/blob/main/CHANGELOG.md"target="_blank" rel="noopener"&gt;CHANGELOG&lt;/a&gt;, ignoring fixes, documentation, and internal details from the same delivery. If I split the new &lt;code&gt;ai-memory run&lt;/code&gt; family into auto-selection, session adoption, Crush support, &lt;code&gt;--yolo&lt;/code&gt;, search, and recovery, we&amp;rsquo;d blow past that number.&lt;/p&gt;</description><content:encoded><![CDATA[<p>It&rsquo;s been a little over a month since I published <a href="/en/2026/06/16/ai-memory-long-term-memory-karpathy-wiki-self-improvement-hermes-projects/">ai-memory: long-term memory (Karpathy Wiki) and self-improvement (Hermes) for your projects</a>. That was June 16. In that post I explained how sessions became Markdown pages, how auto-improve promoted lessons, and why bad memory can be worse than amnesia.</p>
<p>Version 1.1.0 shipped that same day. Today, July 20, we&rsquo;re at <strong>1.17.1</strong>.</p>
<p>Version numbers alone don&rsquo;t mean much, so I counted what actually happened after that post went live: <strong>31 releases, 55 merged pull requests, and 46 closed issues</strong>. I counted <strong>24 groups of new user-facing features</strong> in the <a href="https://github.com/akitaonrails/ai-memory/blob/main/CHANGELOG.md"target="_blank" rel="noopener">CHANGELOG</a>, ignoring fixes, documentation, and internal details from the same delivery. If I split the new <code>ai-memory run</code> family into auto-selection, session adoption, Crush support, <code>--yolo</code>, search, and recovery, we&rsquo;d blow past that number.</p>
<p>Fifteen people had PRs merged during that period, fourteen besides me. Special thanks go to <a href="https://github.com/djalmajr"target="_blank" rel="noopener">Djalma Júnior</a>, who alone had 24 PRs merged, <a href="https://github.com/matheus-rodrigues00"target="_blank" rel="noopener">Matheus Rodrigues</a>, with five, <a href="https://github.com/lhzapata"target="_blank" rel="noopener">lhzapata</a>, with four, <a href="https://github.com/rthiago"target="_blank" rel="noopener">Thiago Silva</a> and <a href="https://github.com/cristianodewes"target="_blank" rel="noopener">Cristiano Dewes</a>, with two each. Nine more contributors had a PR merged. This stopped being &ldquo;a little program I made over a weekend&rdquo; a while ago.</p>
<p>I&rsquo;m not going to dump 55 PRs here. I want to talk about the change that affects my daily use the most: the new <strong><code>ai-memory run</code></strong>.</p>
<h2>The session is part of the project too<span class="hx:absolute hx:-mt-20" id="the-session-is-part-of-the-project-too"></span>
    <a href="#the-session-is-part-of-the-project-too" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Code is the result that survived. It doesn&rsquo;t preserve the entire investigation.</p>
<p>Git shows that you replaced Redis with SQLite. Maybe the commit explains part of it. But where is the Redis experiment? Where is the intermittent bug that only showed up with two workers? Who remembered that the first solution broke on Windows? Why did the team change its mind after three hours? Which workaround was temporary, and which one became the decision?</p>
<p>Much of that exists only in the agent session.</p>
<p>When I program with Claude Code or Codex, the session accumulates the project&rsquo;s operational reasoning: questions, corrections, discarded attempts, test results, surprises, decisions, and changes in direction. The final code shows what stayed. The session explains how we got there.</p>
<p>And an LLM session, at the end of the day, is text. Claude Code writes JSONL. Codex does too. OpenCode uses SQLite. Pi, OMP, and Crush have their own formats and directories. Each harness packages the conversation differently, but the visible content is still messages and tool results.</p>
<p>Why should I leave the most expensive part of the work trapped inside the harness?</p>
<p>Anthropic can change limits, pricing, models, or formats tomorrow. OpenAI can too. An agent can be excellent this week and unbearable the next. I want to swap the engine without throwing away the whole trip.</p>
<p>That has been ai-memory&rsquo;s thesis from the start: <strong>the programmer must remain independent of provider whims</strong>. Models and harnesses are replaceable. The project memory is mine.</p>
<h2>Handoffs solved only half the problem<span class="hx:absolute hx:-mt-20" id="handoffs-solved-only-half-the-problem"></span>
    <a href="#handoffs-solved-only-half-the-problem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>ai-memory already handled handoffs. I would close Claude Code, the hooks would consolidate the session, and the next Codex would receive a short summary with what had been done, open questions, and next steps.</p>
<p>That still exists and is still useful. Anyone who opens <code>claude</code>, <code>codex</code>, or <code>opencode</code> directly keeps the previous behavior. The new launcher is opt-in.</p>
<p>But a handoff is deliberately lossy compression. It preserves what seems most important at that moment. It isn&rsquo;t meant to carry the entire session or resume each harness&rsquo;s native session. If the summary left out an old attempt that became relevant again two hours later, you have to search the wiki or the records.</p>
<p><code>ai-memory run</code> adds another layer: a <strong>managed workstream</strong>, one line of work that crosses harnesses.</p>
<h2>Claude now, Codex in a little while<span class="hx:absolute hx:-mt-20" id="claude-now-codex-in-a-little-while"></span>
    <a href="#claude-now-codex-in-a-little-while" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Basic usage looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/Projects/meu-projeto
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ai-memory run claude
</span></span><span class="line"><span class="cl"><span class="c1"># work, then exit Claude Code normally</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ai-memory run codex --yolo
</span></span><span class="line"><span class="cl"><span class="c1"># Codex continues the same line of work</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ai-memory run claude --model opus
</span></span><span class="line"><span class="cl"><span class="c1"># return to Claude&#39;s previous native session</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ai-memory run
</span></span><span class="line"><span class="cl"><span class="c1"># or let ai-memory choose the right harness to continue</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You don&rsquo;t need <code>--</code> to separate arguments. Everything after the harness name goes to it, with one deliberate exception: <code>--yolo</code> belongs to the wrapper and is translated into the native dangerous option for Claude Code, Codex, OpenCode, Pi, or Crush.</p>
<p>Under the hood, ai-memory doesn&rsquo;t try to convert a Codex file into a Claude file. That would be fragile and would probably break on the next update. Each harness keeps its own native session. The workstream connects those sessions to a portable ledger containing the visible part of the conversation.</p>
<p>Think of it this way:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Claude native session ─┐
</span></span><span class="line"><span class="cl">Codex native session  ─┼─ workstream ─ portable context
</span></span><span class="line"><span class="cl">OpenCode session      ─┤                 + full search
</span></span><span class="line"><span class="cl">Pi native session     ─┘</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The first time Claude enters that workstream, it gets a native session. When you switch to Codex, Codex gets its own native session and receives the portable delta it hasn&rsquo;t seen yet. When you return to Claude, the launcher uses Claude Code&rsquo;s own <code>--resume</code> and delivers only what happened in the other harnesses since the last visit.</p>
<p>This is very different from starting a new chat with a summary pasted into the prompt. Claude returns to its real session. Codex returns to its real session. ai-memory handles continuity between the two.</p>
<p>Managed mode currently supports <strong>Claude Code, Codex, OpenCode, Pi, Crush, and OMP</strong>. General ai-memory support is broader, with MCP and hooks for other clients, but that doesn&rsquo;t mean every client already has a native-session adapter for <code>run</code>. Those are different contracts, and I prefer to make that explicit.</p>
<h2>What goes into the workstream<span class="hx:absolute hx:-mt-20" id="what-goes-into-the-workstream"></span>
    <a href="#what-goes-into-the-workstream" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When the harness exits, ai-memory reads the tail of the native session without modifying the original store. It imports visible user and assistant messages, completed tool calls with their results, compaction summaries, and a non-mutating Git checkpoint. Every event keeps its origin: Claude, Codex, OpenCode, Pi, Crush, or OMP.</p>
<p>Provider credentials, encrypted records, system and developer prompts, and hidden reasoning stay out. Private formats the adapter doesn&rsquo;t understand stay out too and produce a loss annotation instead of letting the system pretend it imported everything.</p>
<p>Records pass through the sanitizer before entering ai-memory&rsquo;s searchable ledger and immutable JSONL segments. The adapter opens native stores read-only; the harness itself remains the writer.</p>
<p>I don&rsquo;t cram the entire session into the next prompt either. That would recreate the problem the project is trying to solve: too much raw context, too much cost, and too much noise. The next agent gets a size-limited recent delta. If an old decision matters again, the full ledger remains searchable:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-memory workstream-search <span class="s2">&#34;por que desistimos do Redis&#34;</span>
</span></span><span class="line"><span class="cl">ai-memory workstream-search --limit <span class="m">50</span> --json <span class="s2">&#34;migration que falhou&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Inside an <code>ai-memory run</code>, the workstream ID is already in the environment. The agent doesn&rsquo;t have to discover some UUID.</p>
<p>And the ledger doesn&rsquo;t replace the wiki. It handles operational continuity. Decisions, rules, procedures, and gotchas that need to survive for months still deserve consolidated Markdown pages. A raw session is evidence. A wiki is organized knowledge.</p>
<h2>Can I keep more than one line of work?<span class="hx:absolute hx:-mt-20" id="can-i-keep-more-than-one-line-of-work"></span>
    <a href="#can-i-keep-more-than-one-line-of-work" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Yes. The default workstream is called <code>default</code>, selected by repository and worktree. If I want an independent line for an investigation without contaminating the main work:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-memory run --new investigar-race claude
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># later, continue that line with another harness</span>
</span></span><span class="line"><span class="cl">ai-memory run --workstream investigar-race codex</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A lease prevents two windows from writing to the same workstream at once. On a normal exit, the launcher imports the tail of the session and releases it immediately. If someone kills the process without cleanup, the lease expires within 90 seconds and the next run resumes from the last confirmed cursor without duplicating imported events.</p>
<p>One stupid annoyance kept coming back: I would reopen a project after a few days and have no idea whether the newest session was in Claude or Codex. I&rsquo;d guess, open one, realize the context was in the other, close it, and try again.</p>
<p>Now I just run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-memory run
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># or keep the same auto-selection in YOLO mode</span>
</span></span><span class="line"><span class="cl">ai-memory run --yolo</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In an empty workstream, ai-memory looks for local sessions from that checkout in Claude Code, Codex, OpenCode, Pi, and Crush, finds the newest one, and opens the right harness for me. In an established workstream, server memory beats the file timestamp: it returns to the last harness attached to that work instead of accidentally adopting an old session that happened to receive a newer write. OMP remains available when selected explicitly, but it doesn&rsquo;t participate in auto-selection yet.</p>
<p>On the first explicit run, the launcher can also offer existing sessions from the same checkout for adoption. You can start using it without abandoning the chat that was already open before you updated ai-memory.</p>
<h2>Other new features I actually use<span class="hx:absolute hx:-mt-20" id="other-new-features-i-actually-use"></span>
    <a href="#other-new-features-i-actually-use" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I promised I wouldn&rsquo;t read the entire changelog out loud, but a few changes from this month deserve a mention because they reduce work for the programmer.</p>
<h3>A briefing before the first question<span class="hx:absolute hx:-mt-20" id="a-briefing-before-the-first-question"></span>
    <a href="#a-briefing-before-the-first-question" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A project can request an automatic briefing at the start of every session:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">briefing</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">inject_on_session_start</span> <span class="p">=</span> <span class="s2">&#34;true&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">max_chars</span> <span class="p">=</span> <span class="mi">4000</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>ai-memory assembles a package with pinned pages, <code>_rules/</code>, <code>_slots/</code>, and recent titles. The agent starts with the project&rsquo;s rules and basic state instead of spending the first conversation rediscovering the architecture. It&rsquo;s opt-in because it consumes context on every SessionStart, including after a Claude <code>/clear</code>.</p>
<h3>Global preferences and cross-project search<span class="hx:absolute hx:-mt-20" id="global-preferences-and-cross-project-search"></span>
    <a href="#global-preferences-and-cross-project-search" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Preferences that apply to all my work can now live in the reserved <code>_global</code> scope: coding style, preferred tools, personal rules, and conventions that don&rsquo;t belong to one specific repository. Normal queries already combine that scope with the current project.</p>
<p>For meta-repos that need to query several sibling projects all the time, there is another opt-in:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">recall</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">default_global</span> <span class="p">=</span> <span class="s2">&#34;true&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With that, <code>memory_query</code> and <code>memory_recent</code> without an explicit scope search every project. I don&rsquo;t turn it on in every repo because gratuitous global search only adds noise.</p>
<h3>Less static instruction, more Agent Skills<span class="hx:absolute hx:-mt-20" id="less-static-instruction-more-agent-skills"></span>
    <a href="#less-static-instruction-more-agent-skills" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The routing block in <code>CLAUDE.md</code> or <code>AGENTS.md</code> got smaller. Detailed instructions for when to search memory, consolidate, save a decision, or create a handoff moved into managed Agent Skills. One command installs or updates both without trampling the rest of the file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-memory install-instructions</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That reduces drift too. When the protocol changes, I update the managed package instead of hunting down an old prompt copied across forty repositories.</p>
<h3>Privacy before the network<span class="hx:absolute hx:-mt-20" id="privacy-before-the-network"></span>
    <a href="#privacy-before-the-network" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Each repo can now exclude paths from capture:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">capture</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">ignore_paths</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;private/**&#34;</span><span class="p">,</span> <span class="s2">&#34;~/personal-notes/**&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>When an integration recognizes a file tool and the path matches, the event is discarded locally before spool, queue, network, or server. The limits are clear: this isn&rsquo;t magic DLP, it doesn&rsquo;t understand every shell command, and it doesn&rsquo;t track content quoted in free-form text. But it handles the verifiable case without selling a fake guarantee.</p>
<p>There are also new integrations with Kimi Code, Grok Build CLI, Devin, and Zero, an OpenCode Zen/Go provider, <code>finalize-session</code> for Codex, and workspace-administration improvements. Again: having MCP and a hook doesn&rsquo;t automatically mean having <code>ai-memory run</code>. The <a href="https://github.com/akitaonrails/ai-memory#support-matrix"target="_blank" rel="noopener">README</a> separates those levels.</p>
<h2>Bonus: ai-jail on the outside, YOLO on the inside<span class="hx:absolute hx:-mt-20" id="bonus-ai-jail-on-the-outside-yolo-on-the-inside"></span>
    <a href="#bonus-ai-jail-on-the-outside-yolo-on-the-inside" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Just over a week ago I wrote about <a href="/en/2026/07/11/how-to-protect-yourself-from-agents-deleting-your-stuff/">how I protect myself from agents deleting my stuff</a>. My recommendation remains the same: YOLO mode gives the best experience, provided the system limits the possible damage.</p>
<p><a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a> reached version 1.15.0 and now understands the ai-memory launcher. My favorite way to work has become:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail ai-memory run codex --yolo</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Or, when I want ai-memory to choose which harness should continue:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail ai-memory run --yolo</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Order matters. <code>ai-jail</code> stays on the outside so the launcher and child process remain inside the same sandbox. <code>ai-memory run</code> by itself manages the session, but it doesn&rsquo;t create a sandbox.</p>
<p><code>--yolo</code> gets rid of the harness&rsquo;s approval ceremony. ai-jail handles containment: the current project is read-write, home is replaced with tmpfs, necessary parts of agent state are mounted selectively, and sensitive directories such as <code>.ssh</code>, <code>.gnupg</code>, and <code>.aws</code> stay outside the jail. The new support also keeps ai-memory&rsquo;s local state writable so hooks can maintain spools and cursors between runs.</p>
<p>ai-jail recognizes both the <code>ai-memory</code> layer and the selected harness. If you have Codex-specific global configuration, for example, it still applies when the actual command is <code>ai-memory run codex</code>. Even the status-bar redraw adjustment understands that the child is Codex.</p>
<p>This combination takes care of both annoyances at once: the agent stops asking for confirmation on every command, and the session stops belonging to the agent. You get the speed without handing over your entire home directory.</p>
<h2>One more thing: ai-usagebar grew too<span class="hx:absolute hx:-mt-20" id="one-more-thing-ai-usagebar-grew-too"></span>
    <a href="#one-more-thing-ai-usagebar-grew-too" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When I published the <a href="/en/2026/05/24/i-built-a-waybar-widget-for-omarchy-to-monitor-llm-usage-ai-usagebar/">original ai-usagebar</a>, it monitored Claude, Codex, Z.AI, and OpenRouter in a Waybar widget or TUI. Version 0.14.0 already covers <strong>eleven usage, spending, or balance integrations</strong>.</p>
<p>Kimi got the two pieces of information that really matter for coding: weekly subscription quota and the rolling five-hour window. Kilo, Novita, Moonshot, and Grok show remaining balance. A separate Anthropic Console integration shows API spending for the month without conflating it with the Claude Code subscription. It makes clear that the Cost API doesn&rsquo;t include Priority Tier.</p>
<p>You can also register multiple Anthropic accounts, see weekly limits for specific models such as Fable, and get a pace marker. The bar doesn&rsquo;t turn red just because it reached 40%; it compares the percentage spent with how much of the window has passed. If I used 40% in 20% of the week, I&rsquo;m moving too fast. That&rsquo;s the useful information.</p>
<p>The project outgrew Waybar. It has a native macOS menu bar app, a GNOME extension, and a standalone TUI. The newest addition is a local Claude Code context monitor: I press <code>c</code>, choose one of the recent sessions, and see how much of the window it has consumed. It reads limited tails from local JSONL files and doesn&rsquo;t invent a percentage when it can&rsquo;t determine the window size.</p>
<p>In my workflow, the three end up working together. <code>ai-usagebar</code> shows which subscription still has room. <code>ai-memory run</code> switches harnesses without throwing away the work. <code>ai-jail</code> lets the chosen one run in YOLO without unrestricted access to the machine.</p>
<h2>Updating<span class="hx:absolute hx:-mt-20" id="updating"></span>
    <a href="#updating" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The installation pages for all three projects cover every method, including AUR, Homebrew, and release binaries:</p>
<ul>
<li><a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a></li>
<li><a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a></li>
<li><a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a></li>
</ul>
<p>After updating ai-memory, reinstall the hooks for the harnesses you plan to use in managed mode. The three main ones in my case:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-memory install-hooks --agent claude-code --apply
</span></span><span class="line"><span class="cl">ai-memory install-hooks --agent codex --apply
</span></span><span class="line"><span class="cl">ai-memory install-hooks --agent opencode --apply</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Crush doesn&rsquo;t need a hook in managed mode; it receives context through a temporary file configured only for the launched process. The <a href="https://github.com/akitaonrails/ai-memory/blob/main/docs/managed-workstreams.md"target="_blank" rel="noopener">managed workstreams documentation</a> covers installation, privacy, recovery, and native arguments for each adapter.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A month ago, ai-memory could already turn a session into a wiki and hand off to the next agent. Now it can manage the line of work itself: adopt an existing session, keep one native session per harness, carry visible history, resume each client in its own format, and keep the entire ledger searchable.</p>
<p>For me, that&rsquo;s the value. The code contains the implementation that won. The session contains the decisions, failed experiments, gotchas, unexpected bugs, and changes of mind that produced that implementation. Throwing all of it away every time you change providers is wasteful.</p>
<p>A raw transcript doesn&rsquo;t solve the problem by itself either. It&rsquo;s large, repetitive, and expensive to stuff into every context. ai-memory keeps the evidence, delivers only the recent delta, and uses consolidation to turn what deserves to survive into short Markdown pages.</p>
<p>There are still new formats to support, platform rough edges, and plenty of testing ahead. The difference is that there is now a real foundation, with an acceptance test that calls actual harnesses, 14 outside contributors with merged PRs in a little over a month, and daily use pushing the design forward.</p>
<p>I rent LLMs and subscriptions from whoever is delivering the best service today. My project&rsquo;s session stays with me.</p>
]]></content:encoded><category>ai-memory</category><category>coding-agents</category><category>ai-jail</category><category>ai-usagebar</category></item><item><title>How Do I Protect Myself From My Agents Deleting My Stuff?</title><link>https://akitaonrails.github.io/en/2026/07/11/how-to-protect-yourself-from-agents-deleting-your-stuff/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/07/11/how-to-protect-yourself-from-agents-deleting-your-stuff/</guid><pubDate>Sat, 11 Jul 2026 21:00:00 -0300</pubDate><description>&lt;p&gt;This week the talk on Twitter was LLMs deleting other people&amp;rsquo;s files. &lt;a href="https://x.com/mattshumer_/status/2075657271401390161"target="_blank" rel="noopener"&gt;Matt Shumer posted&lt;/a&gt; that GPT-5.6-Sol accidentally deleted almost ALL the files on his Mac. A review subagent expanded the &lt;code&gt;$HOME&lt;/code&gt; variable incorrectly and ran &lt;code&gt;rm -rf /Users/mattsdevbox&lt;/code&gt;. He killed the process midway, but the damage was already done.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/tweet-mattshumer.webp" alt="Matt Shumer’s tweet: GPT-5.6-Sol accidentally deleted almost all the files on his Mac, running rm -rf /Users/mattsdevbox because of a bad $HOME expansion." loading="lazy" /&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>This week the talk on Twitter was LLMs deleting other people&rsquo;s files. <a href="https://x.com/mattshumer_/status/2075657271401390161"target="_blank" rel="noopener">Matt Shumer posted</a> that GPT-5.6-Sol accidentally deleted almost ALL the files on his Mac. A review subagent expanded the <code>$HOME</code> variable incorrectly and ran <code>rm -rf /Users/mattsdevbox</code>. He killed the process midway, but the damage was already done.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/tweet-mattshumer.webp" alt="Matt Shumer’s tweet: GPT-5.6-Sol accidentally deleted almost all the files on his Mac, running rm -rf /Users/mattsdevbox because of a bad $HOME expansion."  loading="lazy" /></p>
<p>The next day, <a href="https://x.com/goodalexander/status/2076019436767584347"target="_blank" rel="noopener">goodalexander reproduced</a> the same behavior on a disposable devnet: an <code>rsync --delete-delay</code> against <code>/</code> swept away <code>/var</code>, <code>/etc</code> and his validators&rsquo; state, corrupting the filesystem to the point of needing an OS reinstall. His conclusion: &ldquo;you can&rsquo;t use this thing in prod&rdquo;.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/tweet-goodalexander.webp" alt="goodalexander’s tweet: reproduced GPT 5.6 Sol’s destructive behavior on a disposable devnet, with rsync –delete-delay wiping /var and /etc and corrupting the filesystem."  loading="lazy" /></p>
<p>Cases like these pop up every so often since coding agents went mainstream. And every time, the discussion degenerates into the same two extremes: on one side &ldquo;see? AI is dangerous, never let it run anything on its own&rdquo;, on the other &ldquo;whoever lets an agent loose on their system deserves what they get&rdquo;. I find both sides lazy. Let me tell you my actual experience and what I do about it.</p>
<h2>Five months of marathoning, zero accidents<span class="hx:absolute hx:-mt-20" id="five-months-of-marathoning-zero-accidents"></span>
    <a href="#five-months-of-marathoning-zero-accidents" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Since February I&rsquo;ve been marathoning Claude Code, Codex and OpenCode. Almost every day, almost all the time, in very long sessions that sometimes span multiple days. Blowing past the limits of my Anthropic Max plan and my OpenAI Plus plan with room to spare. The concrete result so far: almost 40 new repositories on my GitHub and more than 500 thousand lines of code produced.</p>
<p>And for the past two months I&rsquo;ve been running everything exclusively in &ldquo;YOLO mode&rdquo;. For those unfamiliar with the jargon: by default, these agents ask for permission before each command they&rsquo;re about to execute in your terminal. YOLO mode turns that safety off. In Claude Code it&rsquo;s the <code>--dangerously-skip-permissions</code> flag, in Codex it&rsquo;s <code>--yolo</code>, in OpenCode you configure the permissions to approve everything. The agent runs whatever it wants, whenever it wants, without asking me anything.</p>
<p>In five months under this regime, not once has an agent deleted a file I didn&rsquo;t ask for. Not once has it run a destructive command out of scope. The frustrations I have with agents are of a different nature (stubbornness, lazy solutions, context getting lost), annoying things but miles away from an <code>rm -rf</code> in my home.</p>
<p>So when I see these reports, my first hypothesis always goes to the same place: the prompt. For an agent to &ldquo;go berserk&rdquo; like that, in general the user wrote very bad instructions. Vague, ambiguous, asking for cleanup without defining what counts as trash, telling the agent to &ldquo;fix it&rdquo; without giving context. Lack of communication skills combined with lack of technical knowledge to notice the request was poorly formulated. The agent executes the instruction you wrote, with all the ambiguities you left in it.</p>
<p>That said, I do NOT trust LLMs completely. Nobody should.</p>
<blockquote>
  <p>The difference is that my distrust turns into engineering instead of turning into a tweet.</p>

</blockquote>
<p>Layer by layer, this is how I sleep well at night.</p>
<h2>Layer 1: sandboxing with ai-jail<span class="hx:absolute hx:-mt-20" id="layer-1-sandboxing-with-ai-jail"></span>
    <a href="#layer-1-sandboxing-with-ai-jail" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When I genuinely suspect a session might do something stupid (I&rsquo;m about to touch system scripts, I&rsquo;m going to let the agent run for hours unsupervised on code that touches the filesystem), I open the harness inside <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>, a tool of mine, open source, written in Rust.</p>
<p>ai-jail is a sandbox wrapper for coding agents. On Linux it uses <code>bwrap</code> (bubblewrap, the same foundation as Flatpak), on macOS it uses <code>sandbox-exec</code>. The idea: the process sees a filesystem that looks normal, but only the current project directory is real and persistent. The rest of <code>$HOME</code>, the sibling directories, <code>/tmp</code>, everything lives in tmpfs and evaporates when the session ends. The agent&rsquo;s state (<code>~/.claude</code>, <code>~/.codex</code> etc) is mounted for real so it stays logged in and configured, but <code>~/.ssh</code>, <code>~/.gnupg</code>, <code>~/.aws</code> and browser profiles never enter the jail.</p>
<p>Installing is one command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Arch Linux</span>
</span></span><span class="line"><span class="cl">yay -S ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># macOS / Linux via Homebrew</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># or straight from crates.io</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And using it is even simpler:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/Projects/my-app
</span></span><span class="line"><span class="cl">ai-jail claude       <span class="c1"># Claude Code in the jail</span>
</span></span><span class="line"><span class="cl">ai-jail codex        <span class="c1"># Codex in the jail</span>
</span></span><span class="line"><span class="cl">ai-jail --dry-run claude  <span class="c1"># shows the policy without running</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If the agent decides to run <code>rm -rf ~</code> in there, it deletes a tmpfs that was going to vanish anyway. My real home was never even mounted. For anyone starting out with YOLO mode without battle scars yet, it&rsquo;s the cheapest way to make mistakes without consequences.</p>
<h2>Layer 2: BTRFS and snapshots (the real safety net)<span class="hx:absolute hx:-mt-20" id="layer-2-btrfs-and-snapshots-the-real-safety-net"></span>
    <a href="#layer-2-btrfs-and-snapshots-the-real-safety-net" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the main point of this post. I&rsquo;ve been advocating for YEARS that Linux users should use BTRFS instead of EXT4. I made <a href="/2023/10/19/akitando-146-protegendo-e-recuperando-dados-perdidos-git-backup-btrfs/">an entire Akitando episode about it in 2023</a>, long before coding agents existed to take the blame. The argument was the same: your data needs to survive human error. An LLM running <code>rm -rf</code> in the wrong place is just a new category of human error, outsourced.</p>
<p>Why is BTRFS superior for desktops? Copy-on-write (CoW): the filesystem never overwrites a block in place, it writes the new version to another block and updates the pointers. Checksums for data and metadata, so silent bitrot gets detected instead of quietly corrupting your file. Transparent compression. Subvolumes, which work like flexible partitions without fixed sizes. And the most valuable consequence of CoW: <strong>lightweight snapshots</strong>.</p>
<p>A BTRFS snapshot is instantaneous and costs close to zero space the moment it&rsquo;s created. It freezes the pointers to the current blocks instead of copying data. Only what diverges from that point on (new or modified blocks) starts taking up extra space. In practice you can take dozens of snapshots a day without feeling it. At this very moment my machine keeps 23 system snapshots, and the cost is the difference between them, a few gigs.</p>
<h3>Scheduling snapshots<span class="hx:absolute hx:-mt-20" id="scheduling-snapshots"></span>
    <a href="#scheduling-snapshots" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The two classic tools are <strong>Timeshift</strong> and <strong>snapper</strong>. Timeshift is the friendlier one: graphical interface, you set &ldquo;how many snapshots to keep&rdquo; per hour/day/week/month and it handles the rest. It expects the <code>@</code> (root) and <code>@home</code> subvolume layout, which is the default for most distros that install on BTRFS today. snapper (born at openSUSE) is more granular: per-subvolume configs, automatic timeline, package manager integration to snapshot before and after every install.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Timeshift: manual snapshot before a suspicious session</span>
</span></span><span class="line"><span class="cl">sudo timeshift --create --comments <span class="s2">&#34;antes de soltar o agente&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># snapper: create a config for /home and enable the timeline</span>
</span></span><span class="line"><span class="cl">sudo snapper -c home create-config /home
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now snapper-timeline.timer</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>My setup runs Timeshift on a schedule, plus <code>grub-btrfsd</code> on standby (I&rsquo;ll explain why in a moment).</p>
<h3>Recovering deleted files<span class="hx:absolute hx:-mt-20" id="recovering-deleted-files"></span>
    <a href="#recovering-deleted-files" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Here&rsquo;s the magic that everyone who lost files to an agent wishes they had. A BTRFS snapshot is browsable like a regular directory. The agent deleted your project at 3pm? The 2pm snapshot still has everything:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># with Timeshift, snapshots are mounted under /run/timeshift</span>
</span></span><span class="line"><span class="cl">sudo ls /run/timeshift/backup/timeshift-btrfs/snapshots/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># found the snapshot from before the damage? copy it back and done</span>
</span></span><span class="line"><span class="cl">sudo cp -a /run/timeshift/backup/timeshift-btrfs/snapshots/2026-07-11_14-00-01/@home/akitaonrails/Projects/meu-app ~/Projects/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With snapper it&rsquo;s the same thing via <code>.snapshots</code> inside the subvolume, and it also has <code>snapper undochange</code> to revert specific changes between two snapshots. Recovery in seconds, no drama, no forensic tooling.</p>
<h3>What if the system won&rsquo;t even boot?<span class="hx:absolute hx:-mt-20" id="what-if-the-system-wont-even-boot"></span>
    <a href="#what-if-the-system-wont-even-boot" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>goodalexander&rsquo;s case went beyond losing files: the agent swept <code>/var</code> and <code>/etc</code> and the system became unusable, requiring an OS reinstall. With BTRFS not even that would be necessary. This is the scenario <a href="https://github.com/Antynea/grub-btrfs"target="_blank" rel="noopener">grub-btrfs</a> exists for: a daemon that watches your snapshots and regenerates the GRUB menu with a boot entry for each one.</p>
<p>System won&rsquo;t come up anymore? Reboot, hold the GRUB menu, pick &ldquo;snapshots&rdquo; and boot straight into yesterday&rsquo;s snapshot, read-only. From inside it, run <code>timeshift --restore</code>, pick the good snapshot, reboot again. Entire system back to its pre-disaster state, in less time than it would take you to find the installation USB stick. In the absolute worst case (GRUB itself went down with the ship), any live USB mounts the BTRFS partition, points the default subvolume back to the good snapshot and that&rsquo;s that.</p>
<h3>What about macOS? And Windows?<span class="hx:absolute hx:-mt-20" id="what-about-macos-and-windows"></span>
    <a href="#what-about-macos-and-windows" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mac users have the direct equivalent: <strong>APFS</strong>, the default filesystem since High Sierra (2017), is also copy-on-write and also has lightweight snapshots. Time Machine is what manages them: with it enabled, macOS takes automatic local snapshots every hour and keeps the last 24 hours on the disk itself, independent of the external backup. And you can create one by hand before letting the agent loose:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># create an APFS local snapshot right now</span>
</span></span><span class="line"><span class="cl">tmutil localsnapshot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># list the existing snapshots</span>
</span></span><span class="line"><span class="cl">tmutil listlocalsnapshots /</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To recover, open Time Machine itself (&ldquo;Browse Time Machine Backups&rdquo; in the menu bar): it browses the local snapshots even without any external disk plugged in, and you restore the file from there. If you prefer the terminal, mount the snapshot read-only with <code>mount_apfs -s</code> and copy it back. In other words, Matt Shumer&rsquo;s case from the beginning of this post had a native solution on his own Mac: a snapshot from one hour earlier would have brought almost everything back.</p>
<p>On Windows the story is weaker. NTFS has the Volume Shadow Copy service (VSS), which feeds restore points and the &ldquo;Previous Versions&rdquo; tab, but it&rsquo;s aimed at system files and rarely happens to be covering your data at the moment you need it. Nothing with the lightness and predictability of a scheduled CoW snapshot. If you develop on Windows, treat external backup as mandatory, no discussion (and the restic from the next section runs fine on Windows).</p>
<h2>Layer 3: external backups with restic<span class="hx:absolute hx:-mt-20" id="layer-3-external-backups-with-restic"></span>
    <a href="#layer-3-external-backups-with-restic" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Snapshots live on the same disk as the data. If the NVMe dies, they die together. Which is the other thing I&rsquo;ve been repeating for years: <strong>everybody needs external backups</strong>, whether to an external hard drive or a NAS. I use a Synology on the local network, mounted via NFS, and <a href="/en/2025/04/17/configuring-my-synology-nas-with-nfs-on-linux/">documented the complete setup here</a>.</p>
<p>On top of it runs <a href="https://restic.net/"target="_blank" rel="noopener">restic</a>, which in my opinion is the best backup tool for this purpose: deduplication, encryption, incremental snapshots and simple restores. My actual configuration is a script fired by a systemd timer every day at 3am. It checks the NAS is mounted, backs up <code>/home</code> and <code>/mnt/data/Projects</code> skipping caches (<code>node_modules</code>, Rust&rsquo;s <code>target/</code>, <code>__pycache__</code> and friends, all in an excludes file), and then applies the retention policy: 7 daily, 4 weekly, 6 monthly, 1 yearly.</p>
<p>Reproducing the essentials looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># once: create the repository on the NAS</span>
</span></span><span class="line"><span class="cl">restic init --repo /mnt/nas/desktop-backup
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># every day (via cron or a systemd timer):</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup backup ~ /mnt/data/Projects <span class="se">\
</span></span></span><span class="line"><span class="cl">    --exclude-file<span class="o">=</span>~/.config/restic/excludes --exclude-caches
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup forget <span class="se">\
</span></span></span><span class="line"><span class="cl">    --keep-daily <span class="m">7</span> --keep-weekly <span class="m">4</span> --keep-monthly <span class="m">6</span> --keep-yearly <span class="m">1</span> --prune</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And if the truly bad day arrives (destructive agent, dead disk, ransomware, the cause doesn&rsquo;t matter):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># list the available snapshots</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup snapshots
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># restore a specific file or directory</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup restore latest <span class="se">\
</span></span></span><span class="line"><span class="cl">    --target /tmp/recuperado --include /home/akitaonrails/Projects/meu-app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># or mount the whole repository as a filesystem and browse at your leisure</span>
</span></span><span class="line"><span class="cl">restic -r /mnt/nas/desktop-backup mount /mnt/restic</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice the layered architecture: the BTRFS snapshot solves the accident from minutes ago in seconds. restic solves the disaster from days ago, including dead hardware. An AI agent would have to be very dedicated to punch through both layers, and even then there would still be yesterday&rsquo;s copy on the NAS, which it can&rsquo;t even reach.</p>
<h2>What about companies? Professionalism<span class="hx:absolute hx:-mt-20" id="what-about-companies-professionalism"></span>
    <a href="#what-about-companies-professionalism" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Everything above is about my personal machine. Now let&rsquo;s raise the bar: corporate environments.</p>
<p>Rule number one is simple and non-negotiable: <strong>no AI agent should ever have access to production. Ever.</strong> No direct SSH into production servers, no production database credentials, no admin tokens for production services. If your agent can run a <code>DELETE</code> against the database that serves customers, the problem stopped being the AI a long time ago: any intern with the same access is the same time bomb, just with a slower typing speed.</p>
<p>Agents in <strong>staging</strong>, on the other hand, I advocate for. That&rsquo;s what staging is for. But even in staging there&rsquo;s a right way: you resist the temptation of prompting one-shot actions (&ldquo;run an <code>apt install</code> over there&rdquo;, &ldquo;do a <code>systemctl restart</code>&rdquo;, &ldquo;connect to postgres and execute this&rdquo;). A loose command executed by hand is knowledge that evaporates. Your infrastructure needs to be automated and reproducible: Ansible, Kubernetes, Terraform, whatever makes sense for your stack. The agent&rsquo;s role becomes writing and refining those recipes, which live versioned in git.</p>
<h3>A small example, right here from my desk<span class="hx:absolute hx:-mt-20" id="a-small-example-right-here-from-my-desk"></span>
    <a href="#a-small-example-right-here-from-my-desk" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I have a gaming environment on my main PC, but I refuse to mix emulator packages with my software development environment. So I isolated gaming inside a <a href="https://github.com/89luca89/distrobox"target="_blank" rel="noopener">distrobox</a>. For those who don&rsquo;t know it: distrobox creates containers (via podman or docker) that integrate with your desktop as if they were native. They share your home (or a dedicated home), GPU access, Wayland, audio. In practice it&rsquo;s a complete distro inside your distro, with a separate package space.</p>
<p><a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">distrobox-gaming</a> is a repository of Ansible playbooks that builds an Arch box called <code>gaming</code> from scratch: ES-DE as the frontend, standalone emulators (shadPS4, RPCS3, PCSX2, Dolphin, DuckStation, Eden and a dozen more), RetroArch cores, per-game optimized configs, launcher shortcuts on the host. And <a href="https://github.com/akitaonrails/distrobox-llm"target="_blank" rel="noopener">distrobox-llm</a> does the same for a local LLM work box: CUDA, cuDNN, Ollama, whisper.cpp and LM Studio isolated in a box with <code>--nvidia</code>, while the host only carries the driver. A CUDA major version upgrade no longer rattles my development environment.</p>
<p>Who wrote the overwhelming majority of these playbooks? The agents. Instead of asking &ldquo;install RPCS3 for me&rdquo;, my prompt is &ldquo;write the idempotent Ansible recipe that installs and configures RPCS3, run it, verify the result and fix it until it passes&rdquo;. Here&rsquo;s a real excerpt from the role that creates the LLM box:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Check if distrobox exists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ansible.builtin.shell</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;distrobox list 2&gt;/dev/null | awk &#39;{print $3}&#39; | grep -qx {{ dl_box_name }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l">_box_exists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">changed_when</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">failed_when</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Create distrobox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ansible.builtin.command</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    distrobox create
</span></span></span><span class="line"><span class="cl"><span class="sd">    --name {{ dl_box_name }}
</span></span></span><span class="line"><span class="cl"><span class="sd">    --image {{ dl_image }}
</span></span></span><span class="line"><span class="cl"><span class="sd">    --home {{ dl_box_home }}
</span></span></span><span class="line"><span class="cl"><span class="sd">    {{ &#39;--nvidia&#39; if dl_nvidia_enabled else &#39;&#39; }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">when</span><span class="p">:</span><span class="w"> </span><span class="l">_box_exists.rc != 0</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Idempotent: it checks whether the box exists before creating it, running it twice gives the same result. If my PC catches fire tomorrow, I recreate both environments in full with one <code>ansible-playbook site.yml</code> in each repo. The knowledge lives in the recipe, versioned, auditable. Zero commands lost in shell history.</p>
<h3>Scaling the same principle to production<span class="hx:absolute hx:-mt-20" id="scaling-the-same-principle-to-production"></span>
    <a href="#scaling-the-same-principle-to-production" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>At a company, swap distrobox for Kubernetes and the principle is identical. The agent writes the manifest, tests it on the staging cluster, you promote it to production manually:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">minha-api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.exemplo.com/minha-api:v42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">512Mi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The professional flow: the agent produces and adjusts the manifest (or the Helm chart, or the Terraform module), applies it to staging (<code>kubectl apply -n staging</code>), runs the tests, watches for side effects, bugs, unintended behavior, and iterates until you&rsquo;re satisfied. Then YOU, a human, promote it to production (via a reviewed PR in a GitOps flow, or a manual and deliberate <code>kubectl apply</code>), monitoring every step. The agent did all the heavy lifting: the artifacts, the procedures, the checklists. You can even ask it to generate the audit documentation at the end. But the finger that presses the production button has a name on a payroll.</p>
<h3>Even the &ldquo;simple&rdquo; maintenance SQL<span class="hx:absolute hx:-mt-20" id="even-the-simple-maintenance-sql"></span>
    <a href="#even-the-simple-maintenance-sql" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A case that looks harmless and bothers me a lot: maintenance SQL commands in production. Plenty of people have always run those by hand, straight into the production client, and are now asking the agent to write AND execute them. That is unacceptable, with or without AI. Your system should have a <strong>migrations</strong> workflow: the change becomes versioned code, runs in staging first, gets validated, and only then executes in production inside the normal deploy process.</p>
<p>And &ldquo;production&rdquo;, by definition, means two things many companies pretend to have. First: real backups, tested, with rehearsed restores.</p>
<blockquote>
  <p>Never run anything in production that you can&rsquo;t recover from.</p>

</blockquote>
<p>Second: monitoring. If you have no visibility into the state of the system, you don&rsquo;t have production, you have a server plugged into an outlet and a prayer.</p>
<h2>Monitoring: even on my humble PC<span class="hx:absolute hx:-mt-20" id="monitoring-even-on-my-humble-pc"></span>
    <a href="#monitoring-even-on-my-humble-pc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And monitoring is so much a part of the discipline that I apply it even to my personal machine. I recently developed a system health widget for my <a href="https://github.com/akitaonrails/clock-tui"target="_blank" rel="noopener">clock-tui</a> (my maintained fork of the original clock-tui, with command widgets in clock mode). It sits open in a corner showing this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/11/agentes-apagando/system-health-widget.webp" alt="clock-tui’s system health widget: restic backups ok 19h ago, 23 timeshift snapshots, dev-cache cleanup ok, btrfs scrub and trim up to date, zero IO errors, load, memory and storage usage per filesystem."  loading="lazy" /></p>
<p>One verdict line (&ldquo;all systems healthy&rdquo;) and the state of everything this post described: the restic backup ran 19 hours ago and the next one is at 03:03; Timeshift keeps 23 snapshots; the development cache cleanup ran 32 hours ago; the BTRFS scrub is up to date on each filesystem, with a recent trim, healthy allocation and zero I/O errors; plus zombies, load, memory and each disk&rsquo;s usage. The widget (<code>tclock-system-health</code>) ships with the repository and the AUR package, and detects most of this on its own.</p>
<p>Behind it are the automated maintenance jobs, all systemd timers: the daily restic at 3am; <code>dev-cache-clean</code> (on the 1st and the 15th) sweeping orphaned Rust <code>target/</code> dirs and the yay cache, which together give back dozens of gigs; the monthly per-filesystem <code>btrfs-scrub</code> validating checksums; the weekly <code>fstrim</code>. None of it requires me to remember anything. When one of them is late or fails, the widget changes color right in my face.</p>
<p>This is the home-grade level, proportional to a personal PC. At a company you go full-blown: Prometheus collecting metrics, Grafana drawing the dashboards, Alertmanager waking up the on-call person at 3am. The scale changes, the principle is the same: the state of the system visible at all times, and someone (or something) getting notified when it goes off the rails.</p>
<h2>Conclusion: discipline<span class="hx:absolute hx:-mt-20" id="conclusion-discipline"></span>
    <a href="#conclusion-discipline" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Go back to the two tweets at the beginning. In both cases, a single wrong command caused real loss, and the public discussion became &ldquo;who do I trust more&rdquo;. The right question is a different one: <strong>why does a single wrong command, from any origin, have the power to cause unrecoverable loss on your machine?</strong></p>
<p>Being a professional is being disciplined, and discipline starts at home. My personal PC operates with the same rigor I would demand from a corporate environment: a filesystem with lightweight scheduled snapshots, recoverable boot via GRUB, daily external backups with a retention policy, a sandbox for suspicious sessions, isolated environments rebuildable from versioned recipes, automated maintenance and a monitor that warns me when any piece falls behind. Monthly maintenance cost of all this: close to zero. It was set up once (in good part by the agents themselves) and runs on its own.</p>
<p>With these layers, the question in the title almost loses its meaning. The agent deleted a directory? Snapshot from an hour ago, two minutes of recovery. Destroyed the system? Boot into a snapshot via GRUB, restore, ten minutes. Fried the disk along with it? restic on the NAS, yesterday&rsquo;s backup. In five months of YOLO mode I never needed any of these nets for an AI accident. But I sleep well precisely because they&rsquo;re there.</p>
<p>An LLM running loose on your system is just the most recent stress test of your discipline. If an out-of-control agent can cause permanent loss to your data, the vulnerability was always there, waiting for a mistake of yours, a bad disk or a ransomware. The agent was just the messenger that arrived first.</p>
]]></content:encoded><category>ai-jail</category><category>coding-agents</category><category>security</category><category>storage-and-backup</category></item><item><title>Akita's AI Tips and Toolkit: ai-jail, ai-memory, ai-usagebar</title><link>https://akitaonrails.github.io/en/2026/05/24/akita-ai-tips-toolkit-ai-jail-ai-memory-ai-usagebar/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/05/24/akita-ai-tips-toolkit-ai-jail-ai-memory-ai-usagebar/</guid><pubDate>Sun, 24 May 2026 10:30:00 -0300</pubDate><description>&lt;p&gt;In last week&amp;rsquo;s post, &lt;a href="https://akitaonrails.github.io/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/"&gt;&amp;ldquo;Wrapping Up My AI Marathon: Success or Failure?&amp;rdquo;&lt;/a&gt;, I closed the first phase of my marathon. More than 600 hours of heavy coding-agent use, dozens of projects, hundreds of thousands of lines of code and documentation, and a pile of practical conclusions. Not marketing. Practical.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll repeat something important before someone starts cloning all my repositories thinking they are 1.0 products: most of the &lt;code&gt;frank-*&lt;/code&gt; projects on my GitHub are proofs of concept. Experiments. Some work well for me, in my environment, in my workflow. That does not mean they are ready to become a critical dependency in your company tomorrow morning. Want to play, contribute, study? Great. Just don&amp;rsquo;t confuse &amp;ldquo;I published it on GitHub&amp;rdquo; with &amp;ldquo;this is production ready.&amp;rdquo;&lt;/p&gt;</description><content:encoded><![CDATA[<p>In last week&rsquo;s post, <a href="/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/">&ldquo;Wrapping Up My AI Marathon: Success or Failure?&rdquo;</a>, I closed the first phase of my marathon. More than 600 hours of heavy coding-agent use, dozens of projects, hundreds of thousands of lines of code and documentation, and a pile of practical conclusions. Not marketing. Practical.</p>
<p>I&rsquo;ll repeat something important before someone starts cloning all my repositories thinking they are 1.0 products: most of the <code>frank-*</code> projects on my GitHub are proofs of concept. Experiments. Some work well for me, in my environment, in my workflow. That does not mean they are ready to become a critical dependency in your company tomorrow morning. Want to play, contribute, study? Great. Just don&rsquo;t confuse &ldquo;I published it on GitHub&rdquo; with &ldquo;this is production ready.&rdquo;</p>
<p>There are exceptions. <a href="https://github.com/akitaonrails/frankmd"target="_blank" rel="noopener">FrankMD</a>, for example, received a decent amount of contributions, tests, polish, real bug fixes, real use. Today it is much more stable than it was in the first post. Still, the point stands: these projects were born as a vibe-coding lab, not as a 3-year product plan with roadmap, enterprise support, and SLA.</p>
<p>This post is a roundup of what I learned in that lab. A few of my tools, a few habits, a few recommendations. Nothing mystical. No &ldquo;revolutionary new way to program.&rdquo; Old engineering with new tooling.</p>
<h2>First: stop collecting agents<span class="hx:absolute hx:-mt-20" id="first-stop-collecting-agents"></span>
    <a href="#first-stop-collecting-agents" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you are a normal developer, you do not need 12 agents, 40 wrappers, 7 orchestrators, and a Kubernetes diagram to choose who gets to write an <code>if</code>.</p>
<p>In practice, focus on <strong>Claude Code</strong>, <strong>Codex</strong>, or <strong>opencode</strong>. That&rsquo;s it.</p>
<p>Yes, Gemini CLI exists. Yes, Cursor exists. Yes, Windsurf, Crush, OpenClaw, Pi, Aider exist, and tomorrow another half-dozen will show up. I tested a bunch. Read my LLM benchmark posts, especially <a href="/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/">&ldquo;LLM Benchmarks Part 3: DeepSeek, Kimi, MiMo&rdquo;</a>. The conclusion is boring and obvious: today, the best cost-benefit options for heavy programming are <strong>Opus 4.7</strong> and <strong>GPT 5.5</strong> on subsidized Pro/Plus/Max plans. Nothing comes close when the job is spending several hours a day with a real coding agent.</p>
<p>Paying directly for tokens is expensive. Paying for a subsidized monthly plan is much cheaper. That&rsquo;s why I keep alternating between Claude Code and Codex: not because I want &ldquo;multi-agent architecture,&rdquo; but because the plans have limits and I work many hours in a row.</p>
<p>Are DeepSeek v4 and Kimi 2.6 cheaper? Yes. Are they usable? Yes. I like them for several cases. But for <strong>Agile Vibe Coding</strong> the way I&rsquo;ve been advocating since <a href="/en/2026/02/20/zero-to-post-production-in-1-week-using-ai-on-real-projects-behind-the-m-akita-chronicles/">&ldquo;From Zero to Post-Production in 1 Week&rdquo;</a>, they are still lacking. They stumble more on continuity, long refactors, tests, understanding the whole project. Can you use them? Sure. But if the goal is consistent productivity, Opus and GPT are still the bar.</p>
<p>Running an open-source model locally is neat. I played with that a lot in the <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">Minisforum MS-S1 Max with AMD AI Max+ 395 review</a>. For one-shots, studying, playing with local inference, it&rsquo;s fun. For Agile Vibe Coding all day, every day, it is still not practical. Latency, context, quality, setup maintenance, VRAM, all of it collects tax.</p>
<p>Running multiple LLMs in parallel is not practical either, the whole &ldquo;planner + executor + reviewer + judge&rdquo; routine. I tested that in <a href="/en/2026/04/25/llm-benchmarks-vale-a-pena-misturar-2-modelos/">&ldquo;LLM Benchmarks: Is It Worth Mixing 2 Models?&rdquo;</a>. The idea looks nice on paper. In practice, it increases cost, latency, error surface, and the gain does not pay for the complexity.</p>
<p>The secret is not having a bunch of agents. Forget OpenClaw or Pi for day-to-day programming. The secret is engineering process. XP. Tests. <a href="/en/2026/04/20/clean-code-for-ai-agents/">Clean Code for AI Agents</a>. CI. Pair programming. Constant refactoring. Small code. Automated deploy. Fast feedback.</p>
<p>Tools help. Engineering wins.</p>
<h2>ai-jail: autonomy with a fence<span class="hx:absolute hx:-mt-20" id="ai-jail-autonomy-with-a-fence"></span>
    <a href="#ai-jail-autonomy-with-a-fence" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The first project I want to highlight is <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>. It is a sandbox wrapper to run AI agents with restricted system access. On Linux it uses <code>bubblewrap</code> (<code>bwrap</code>) and Landlock. On macOS it uses <code>sandbox-exec</code>. The idea is simple: the agent sees the environment it needs to work, but does not go rummaging through your whole system.</p>
<p>Do I use it every day? Yes. Do I use it always? No.</p>
<p>When I know I am not doing anything risky, I open <code>claude</code> or <code>codex</code> directly. If I am touching a simple project, just running tests, editing markdown, refactoring local stuff, often I do not bother. In more than 600 hours of intense Agile Vibe Coding, more than half a million lines of code and documentation, I never had one of those folklore cases of an LLM destroying my host system in some unrecoverable way.</p>
<p>And look, I explicitly used agents to configure my Omarchy. ZSH, Hyprland, Waybar, terminal configs, login scripts, all of it. They never did anything I did not ask for.</p>
<p>Those stories of &ldquo;the agent deleted my whole home by itself&rdquo; make me raise an eyebrow. In my experience, when you ask it to delete something dangerous, Claude and Codex ask for confirmation. You have to say yes. If you said yes without reading, that is not LLM black magic. That is user in zombie mode.</p>
<p>Nothing is guaranteed, of course. Hence ai-jail.</p>
<p>My favorite use is combining <code>ai-jail</code> with the harness&rsquo; no-brakes mode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail claude --dangerously-allow-permissions
</span></span><span class="line"><span class="cl">ai-jail codex --dangerously-bypass-approvals-and-sandbox</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It sounds contradictory, but it makes sense. I remove the confirmation friction inside the agent, but put a fence at the operating-system level. The agent can work fast inside the project, but the host filesystem is essentially read-only. The current directory is read-write. Whatever I explicitly map with <code>--rw-map</code> is also read-write. The rest is not to be touched.</p>
<p>The ai-jail README sums it up well: it is a sandbox wrapper for agents like Claude Code, GPT Codex, OpenCode, and Crush. On the first run it creates a <code>.ai-jail</code> in the project, which can be committed. It supports <code>--dry-run</code> to see mounts before executing, <code>--lockdown</code> for a stricter mode, <code>--private-home</code> to hide real dotfiles, <code>--mask .env</code> to hide sensitive files inside the project itself, and isolated browser profiles for Chromium/Firefox.</p>
<p>Do not sell this as absolute security. Process sandboxing is not a VM. Kernel bugs exist. Side channels exist. macOS has limitations. The README itself is clear: if you need to isolate a truly hostile workload, use a disposable VM. I do not use ai-jail as &ldquo;military-grade armor.&rdquo; I use it as another layer, good enough for a development workflow.</p>
<p>And even inside the project, everything is Git. I push frequently to GitHub or to my private Gitea. So the realistic worst case is the agent destroying the project directory. Annoying? Yes. Catastrophic? No. I clone again. I come back from the remote. I redo what was local. If <code>.git</code> gets corrupted, I delete everything and clone again.</p>
<p>Secrets also have a copy outside. <code>.env</code>, tokens, keys, everything that matters sits in Bitwarden. <a href="/en/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Self-hosted in Vaultwarden</a>, of course. So if I lose the local directory, I did not lose the keys to the kingdom.</p>
<p>The most common risk is not the agent deleting everything. It is you leaking a secret to Git. That happened to me more than once. You run <code>git add .</code>, forget a <code>.env</code>, push to the remote, congratulations. It happens. That&rsquo;s why you must look at <code>git status</code>, <code>git diff --cached</code>, and think before pushing.</p>
<p>Even when it happens, LLMs are very competent at helping rewrite history. I ask them to clean the secret from Git, force-push, review that it disappeared, and then I rotate the key. It is not pretty, but it is manageable.</p>
<p>So yes, I recommend using Claude/Codex with dangerous permissions <strong>inside</strong> ai-jail. Is there a 0.001% chance of an unrecoverable accident? There is. But if you are a good engineer, that should not kill you. I have layers: automatic Btrfs snapshots, restic to my NAS, NAS archiving offsite to AWS Glacier, remote Git, Bitwarden. No tool replaces backups.</p>
<h2>ai-memory: context that survives the harness<span class="hx:absolute hx:-mt-20" id="ai-memory-context-that-survives-the-harness"></span>
    <a href="#ai-memory-context-that-survives-the-harness" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The second is <a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a>, which I explained in detail in <a href="/en/2026/05/23/i-built-memory-system-for-coding-agents-ai-memory/">&ldquo;I Built a Memory System for Coding Agents: ai-memory&rdquo;</a>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/23/ai-memory/ai-memory-logo.png" alt="ai-memory project logo: circular pictogram with a stylized chip opening “wings” like libraries/pages on each side, next to the italic ai-memory text."  loading="lazy" /></p>
<p>The motivation is simple: harnesses lose context. Claude Code compacts. Codex compacts. opencode compacts. The session ends. You open another agent and it does not know what happened in the last 4 hours. If you only use one harness, maybe it does not bother you much. If you alternate between Claude and Codex like I do, it gets annoying fast.</p>
<p>I wanted a long-term memory consolidator that worked across different harnesses. Something that allowed handoff between Claude, Codex, and opencode without me manually writing <code>HANDOFF.md</code> every time. Something that captured prompts, tool calls, and session events through hooks, consolidated them into markdown pages, indexed with SQLite/FTS5, and exposed everything through MCP when the agent needed to query it.</p>
<p>Another motivator was what I explained in <a href="/en/2026/04/06/rag-is-dead-long-context/">&ldquo;Is RAG Dead? Long Context, Grep, and the End of the Mandatory Vector DB&rdquo;</a>. For code projects, simple text almost always solves more than fancy vector DB architecture. Markdown, <code>grep</code>, SQLite FTS5, long context, and good files in <code>docs/</code> already take you far. ai-memory follows that line: compile memory into text, leave it on disk, index it simply, and only call an LLM when consolidation is worth it.</p>
<p>ai-memory was not made to be a user-browsable knowledge base. It has a read-only web UI, yes, but that is for auditing and inspection. The memory is optimized for LLM consumption. It is an internal wiki the harness can query when it thinks it needs to. It is a place where I can explicitly ask &ldquo;save this for the next session&rdquo; and expect it to survive longer than Claude or Codex&rsquo;s default compaction.</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/ai-memory/main/docs/web-projects-home.png" alt="ai-memory web UI homepage showing detected projects, page counts, last activity, and the warning that memory is optimized for LLM consumption, not human-curated documentation."  loading="lazy" /></p>
<p>The README sums it up well: coding agents lose all context when the session ends; ai-memory gives them a shared persistent wiki. Every prompt, tool call, and decision can be captured automatically. At the end of the session, relevant pages are rewritten as coherent narrative. In the next session, another agent can receive a handoff already prepended into the context.</p>
<p>This is useful when you close Claude at 6pm, open Codex the next day in the same directory, and want it to know you were testing session cookies because JWT rotation had run into trouble. Or when you return to a project six weeks later and do not remember why you discarded Redis.</p>
<p>But do not confuse it with canonical documentation. If you did important research you do not want to lose, ask the agent to write markdown in <code>docs/</code> inside the project. Architecture decision, benchmark, library analysis, deploy runbook, anything that must survive with guarantees should become an explicit file in the repo. That is simpler, auditable, and versioned.</p>
<p>ai-memory covers the middle band: transient information that might matter later. A weird bug that appeared. The reasoning that led to a decision. An environment gotcha. A session summary. Things you do not want to turn into official docs yet, but also do not want to throw away.</p>
<p>It is still beta. I use it in personal production, but it needs many more users beating on it. The most polished integrations today are Claude Code, Codex, and opencode. Gemini CLI, OpenClaw, and exotic MCP clients still need more testing. PRs are welcome. Bug reports too. &ldquo;I installed it and it broke on my setup&rdquo; is exactly the kind of feedback that reveals a wrong assumption.</p>
<h2>ai-usagebar: knowing when to switch horses<span class="hx:absolute hx:-mt-20" id="ai-usagebar-knowing-when-to-switch-horses"></span>
    <a href="#ai-usagebar-knowing-when-to-switch-horses" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As I said above, I prefer using Anthropic and OpenAI&rsquo;s subsidized plans. The problem is that if you do real Agile Vibe Coding, you hit limits. Daily limit. Weekly limit. Session limit. Limit that changes. Limit that does not show up clearly anywhere.</p>
<p>I was using <a href="https://github.com/mryll/claudebar"target="_blank" rel="noopener">claudebar</a>, which is good, but only covers Claude. I wanted to see the main vendors in one place. That became <a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a>, which I introduced in <a href="/en/2026/05/24/i-built-a-waybar-widget-for-omarchy-to-monitor-llm-usage-ai-usagebar/">&ldquo;I Built a Waybar Widget for Omarchy to Monitor LLM Plan Usage: ai-usagebar&rdquo;</a>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/24/ai-usagebar/ai-usagebar-waybar.png" alt="Waybar widget showing “cld 29% · 1h 12m” in the top-right corner, with the tooltip open showing the Claude Max 20x plan: Session 29%, Weekly 47%, Sonnet only 0%, Extra usage $0.00 against a $1100 limit, and the last-updated time."  loading="lazy" /></p>
<p>It is a Waybar widget and Rust TUI to monitor Anthropic Claude, OpenAI Codex/ChatGPT, Z.AI GLM, and OpenRouter usage. For Claude and OpenAI, it reads the OAuth credentials the official CLIs already wrote. For Z.AI and OpenRouter, it uses API keys. It shows session percentage, reset, limits, balance, and lets you switch the primary vendor.</p>
<p>The value is banal and therefore useful: I glance at the bar and know whether I should continue in Claude, hand off to Codex, or top up OpenRouter credit. I do not want to discover I hit the weekly limit in the middle of a large refactor.</p>
<p>There is also a standalone TUI:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/24/ai-usagebar/ai-usagebar-tui-openai.png" alt="ai-usagebar-tui on the OpenAI tab running in a terminal: Codex 5h and weekly gauges, Credits block with message-count ranges, Claude / OpenAI / GLM (Z.AI) / OpenRouter tabs at the top, key hints in the footer."  loading="lazy" /></p>
<p>If you do not use Omarchy, Hyprland, or Waybar, you can still run <code>ai-usagebar-tui</code> in any terminal. SSH, tmux, normal terminal, whatever. The bar is convenience. The TUI is the universal form.</p>
<h2>ghpending: GitHub pending stuff without the ritual<span class="hx:absolute hx:-mt-20" id="ghpending-github-pending-stuff-without-the-ritual"></span>
    <a href="#ghpending-github-pending-stuff-without-the-ritual" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After the marathon, I ended up with dozens of <code>ai-*</code> and <code>frank-*</code> repos on GitHub. That creates an annoying problem: issues and pull requests start appearing. A contributor opens a PR. Someone reports a bug. Someone else sends a suggestion. If I have to open the browser, enter repo by repo, filter tab by tab, I will postpone it. And if I postpone it, I go weeks without answering contributors.</p>
<p>Yes, I could use <code>gh</code>, the official GitHub CLI. I do use it. But I wanted something dumber and faster: a short command to print a digest of everything pending in the repos I chose to monitor. That became <a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a>, which I described in <a href="/en/2026/05/23/i-built-a-cli-to-check-my-github-pending-stuff-ghpending/">&ldquo;I Built a CLI to Check My GitHub Pending Stuff: ghpending&rdquo;</a>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/23/ghpending/ghpending-screenshot.png" alt="ghpending terminal output: compact table listing each monitored repo with open issue and pull request counts, latest activity author, and how long ago it happened."  loading="lazy" /></p>
<p>That is it: <code>ghpending add</code> to choose repos, <code>ghpending</code> to see the digest, <code>ghpending list</code>, <code>ghpending rm</code>. Without auth it works with GitHub&rsquo;s 60 req/h anonymous limit. With <code>GITHUB_TOKEN=$(gh auth token) ghpending</code>, it goes up to 5,000 req/h.</p>
<p>The point is not replacing GitHub. It is reducing friction. If I can run one command in the terminal and see &ldquo;there are 3 PRs and 2 issues to look at,&rdquo; I look. If it depends on browser ritual, notifications, and tabs, it waits.</p>
<p>And Claude/Codex are very good at this kind of work. You can ask:</p>
<blockquote>
  <p>&ldquo;Check the open pull requests, review the code, see if it compiles, run the tests, add tests if missing, rebase from master if needed, merge and close.&rdquo;</p>

</blockquote>
<p>Or:</p>
<blockquote>
  <p>&ldquo;Check the open issues, evaluate whether there is a real bug, reproduce it if possible, propose a fix, answer the user.&rdquo;</p>

</blockquote>
<p>They read the PR, understand the diff, run tests, rebase, solve simple conflicts, write the contributor response. You still keep your brain on. You are delegating mechanical work you already know how to do.</p>
<h2>Refactoring: ask explicitly, several times<span class="hx:absolute hx:-mt-20" id="refactoring-ask-explicitly-several-times"></span>
    <a href="#refactoring-ask-explicitly-several-times" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Claude and Codex are very good at refactoring, but do not assume they will do it automatically the way you would. I usually start projects or features with explicit instructions:</p>
<blockquote>
  <p>&ldquo;Use Rust best practices. Avoid unnecessary duplication. Modularize only where it makes sense. Add unit tests for every new feature. Follow <a href="/en/2026/04/20/clean-code-for-ai-agents/">Clean Code</a> principles. Do not create an abstraction before it is needed.&rdquo;</p>

</blockquote>
<p>After a large feature, a large fix, or a few hours of development, I stop and ask:</p>
<blockquote>
  <p>&ldquo;Now that we touched a lot of code, re-evaluate it carefully. Delete dead code. Refactor obvious duplication. Add missing tests. Remove magic numbers. Check if any modules are too large. Do not change behavior without tests.&rdquo;</p>

</blockquote>
<p>Before release:</p>
<blockquote>
  <p>&ldquo;Before publishing this version, do a sanity audit. Look for secret leaks, dangerous permissions, destructive commands, path traversal, unnecessary unwraps, missing validation, and anything that looks like an obvious vulnerability.&rdquo;</p>

</blockquote>
<p>Does that guarantee security? Of course not. Not even close. But it removes a lot of dumb mistakes. It is another layer. I like running this audit with Claude and then with Codex, because each one finds different things. Bugs still pass. Security is process, not a magic ritual.</p>
<h2>Disposable experiments: code got cheap<span class="hx:absolute hx:-mt-20" id="disposable-experiments-code-got-cheap"></span>
    <a href="#disposable-experiments-code-got-cheap" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before choosing a library, framework, architecture, model, or service, I like creating <code>experiments/</code>.</p>
<p>Example:</p>
<blockquote>
  <p>&ldquo;Before choosing between Qwen and DeepSeek, create a disposable prototype in <code>experiments/</code> for an A/B test. Document the analysis in <code>docs/</code> with result, cost, latency, quality, and recommendation.&rdquo;</p>

</blockquote>
<p>This changed how I decide. Before, many engineering decisions became opinion, feeling, faith in somebody else&rsquo;s benchmark. Now code is cheap. I ask the agent to make two or three versions, run tests, measure, document, throw away what does not work.</p>
<p>Prototyping got genuinely fast. <a href="/en/2026/04/05/turning-youtube-into-a-karaoke-app-frank-karaoke/">Frank Karaoke</a> is a good example: turning YouTube into a karaoke app was the kind of idea I would maybe leave in a mental list for months, waiting for &ldquo;a free weekend&rdquo; that never arrives. Now I can open <code>experiments/</code>, ask for a rough prototype, test the technical path, and decide whether it deserves to become a project. Look at my other 2026 posts about <code>frank-*</code> and <code>ai-*</code> projects: almost all of them started like this, as disposable code that proved something before earning a name.</p>
<p>But cheap prototypes do not mean one-shot. I already wrote in <a href="/en/2026/04/15/how-to-talk-to-claude-code-effectively/">&ldquo;Why LLMs Aren&rsquo;t Giving You the Result You Expect&rdquo;</a>: a good prompt is an iterative conversation. You provide context, show references, point out constraints, read the result, correct direction, ask for a rewrite. If you throw in a vague sentence and expect a finished product, that&rsquo;s on you.</p>
<p>I learned this by taking a beating too. In the <a href="/en/2026/03/05/my-first-vibe-code-failure-frank-yomik/">Frank Yomik</a> post I described my first real vibe-coding failure: when the direction is wrong, insisting on the same prompt just digs the hole deeper. Sometimes you need to reduce scope, isolate the problem, create a test, change strategy. LLM does not turn a bad plan into good engineering.</p>
<p>And there are domains where you need to study along with it. In the <a href="/en/2026/04/23/driveclub-shadps4-e-ia-como-aprender/">DriveClub and shadPS4</a> post, AI almost sent me in every wrong direction because the problem required understanding rendering, gamma, shaders, buffers. The tool helps you explore, but you still decide whether the explanation makes sense. If you do not understand enough to verify, you are just accepting a pretty guess.</p>
<p>That is also why I keep repeating the idea from <a href="/en/2026/03/01/software-is-never-done-4-projects-life-after-deploy-one-shot-prompt-myth/">&ldquo;Software Is Never Done&rdquo;</a>: deploy is not the finish line. The prototype becomes a product when it handles bug reports, maintenance, refactoring, docs, releases, rollbacks, users complaining. LLM helps a lot with that boring cycle. But the cycle still exists.</p>
<p>If it is testable, test it. If it can be measured, measure it. If it cannot be measured, at least write down the assumptions. The important thing is not choosing just because it &ldquo;seems better.&rdquo;</p>
<p>Cheap code also means detachment. Make code to throw away. Make an ugly prototype. Make a disposable benchmark. Ask the agent to document what it learned in <code>docs/decision-qwen-vs-deepseek.md</code>, then delete <code>experiments/</code> if you want. The document stays. The lesson stays.</p>
<p>LLMs are also very good at documentation. Use that. Ask it to document everything. Why we chose PostgreSQL instead of SQLite in this case. Why the deploy uses a multi-stage Docker image. Why the service runs behind Cloudflare Tunnel. Why that sysctl exists. Even if you delete half of it later, the cost of writing fell near zero.</p>
<h2>Automated deploy or you will stop maintaining it<span class="hx:absolute hx:-mt-20" id="automated-deploy-or-you-will-stop-maintaining-it"></span>
    <a href="#automated-deploy-or-you-will-stop-maintaining-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every project of mine that matters has automated build. Many run in my homelab, as I described in <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">&ldquo;Migrating my Home Server with Claude Code&rdquo;</a>. Almost every project has a <code>bin/deploy</code> that builds a Docker image, pushes it, rotates it on the server, applies migration if needed, and validates healthcheck.</p>
<p>Claude and Codex are excellent at deploy. They read <code>bin/deploy</code>, understand Dockerfile, edit GitHub Actions, adjust release workflows, fix binary signing, update Homebrew taps, generate checksums. And yes, I ask Claude/Codex to SSH into my homelab and redeploy Docker containers. They are very good at that. ai-jail, for example, has GitHub Actions to build signed binaries for Linux and macOS.</p>
<p>If you have dozens of projects, the more automated the better. I rarely type admin commands in the terminal by myself. I ask the LLM to do it, observe, approve what is dangerous, and then ask:</p>
<blockquote>
  <p>&ldquo;Document what was done and why it was done.&rdquo;</p>

</blockquote>
<p>This applies to the homelab and my workstation. My Omarchy is customized through Claude: ZSH, Hyprland, Waybar, scripts, shortcuts, themes, integrations. Even my gaming/emulation setups fall into this, as I showed in the <a href="/en/2026/04/19/my-favorite-retro-racing-games-on-distrobox/">retro racing games running on Distrobox</a> post. This is a case where I normally do not use ai-jail, because I actually want to touch the host. So I look at commands, read diffs, approve slowly. Even then, I never had the agent break my system in an unrecoverable way. Quite the opposite. My PC has never been more stable and documented.</p>
<p>Days later, when I forget why I configured something that way, the context is in markdown, Git, ai-memory, or the project history itself. I can ask and the agent has something to pull from.</p>
<h2>Even this post was written this way<span class="hx:absolute hx:-mt-20" id="even-this-post-was-written-this-way"></span>
    <a href="#even-this-post-was-written-this-way" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is how I write these posts now. I dump a huge messy block from my brain into Claude. I ask it to read recent posts to calibrate style. I ask it to check the projects directly and elaborate the technical parts. I ask it to use the latest screenshots, upload to S3 when needed, link previous posts, review the tone.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/24/ai-toolkit/writing-this-post.png" alt="Terminal screenshot with the raw prompt used to write this post: a huge block of instructions, links, opinions, and technical details dumped into the agent before final editing."  loading="lazy" /></p>
<p>Then I read and edit. I cut what sounds generic. I replace sentences that sound like release notes. I put my rants back in. The result is still my text; I just spend energy on what matters: deciding what to say, correcting nuance, cutting nonsense.</p>
<h2>&ldquo;But did you test Pi?&rdquo;<span class="hx:absolute hx:-mt-20" id="but-did-you-test-pi"></span>
    <a href="#but-did-you-test-pi" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every time I mention an LLM or LLM tool, someone shows up in the comments with the same question: &ldquo;but did you test Pi?&rdquo;, &ldquo;but did you test DeepSeek?&rdquo;, &ldquo;but what about Qwen?&rdquo;, &ldquo;but what about the new model that came out yesterday?&rdquo; Every. Single. Time.</p>
<p>It is annoying as fuck. And almost always it comes from three kinds of people: someone who tested nothing and wants to outsource the work, someone who already chose a tool and wants me to validate their choice, or a fanboy treating an LLM vendor like a soccer team. They all sound the same. NPC repeating tutorial dialogue.</p>
<p>Software engineers should not decide based on other people&rsquo;s opinions. Not even mine. If you choose a tool because &ldquo;Akita said so,&rdquo; you understood it wrong. I advocate Claude Code and Codex because I used them for hundreds of hours, in real projects, publishing code and post-mortems of what worked and what failed. And even then I did not stop at opinion.</p>
<p>I built my own automated benchmark: <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">llm-coding-benchmark</a>. I update it when a popular vendor releases a new version. I run comparable scenarios. I publish results. Then I adjust my opinion based on the data. That gives me enough ground to say that, in my workflow, Opus and GPT keep winning. Not because I woke up wanting to defend some billionaire company&rsquo;s logo.</p>
<p>If your first reaction was &ldquo;but did you test Qwen?&rdquo;, great. Test it yourself. Run the benchmark. Create a better scenario. Send a PR. Show numbers. Show cost. Show latency. Show where it failed. Show where it won. Bring new data.</p>
<p>Loose opinion is irrelevant. &ldquo;It works for me&rdquo; does not scale. &ldquo;I like it better&rdquo; does not matter. &ldquo;In my one-shot prompt it was better&rdquo; is an anecdote. Engineering starts when something can be reproduced, compared, and acted on.</p>
<p>So it is simple: bring actionable data or shut the hell up. I do not need vendor fan clubs in the comments. I need people testing, measuring, and contributing new results.</p>
<h2>Final checklist<span class="hx:absolute hx:-mt-20" id="final-checklist"></span>
    <a href="#final-checklist" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Short summary, because this post is already long:</p>
<ul>
<li>Focus on Claude Code, Codex, or opencode. Stop collecting agents.</li>
<li>Use Opus 4.7 and GPT 5.5 through Pro/Plus/Max plans when daily productivity matters.</li>
<li>DeepSeek, Kimi, and local models are useful, but still do not replace the main workflow.</li>
<li>Agile Vibe Coding is XP with LLM: tests, <a href="/en/2026/04/20/clean-code-for-ai-agents/">Clean Code</a>, CI, pair programming, and deploy.</li>
<li>Use <code>ai-jail</code> when you want agent autonomy with a filesystem fence.</li>
<li>Combine <code>ai-jail</code> with dangerous Claude/Codex permissions when the project is protected by Git.</li>
<li>Have real backup: Btrfs snapshots, restic, NAS, offsite, Git, Bitwarden.</li>
<li>Review <code>git add</code>, <code>git diff --cached</code>, and pushes so you do not leak secrets.</li>
<li>Use <code>ai-memory</code> for transient context and handoff between agents.</li>
<li>Write canonical documentation in <code>docs/</code> when information needs to survive with guarantees.</li>
<li>Use <code>ai-usagebar</code> to know when to switch from Claude to Codex, or when to top up OpenRouter.</li>
<li>Use <code>ghpending</code> so you do not abandon issues and PRs.</li>
<li>Ask for refactoring explicitly after large features.</li>
<li>Run a security audit before release, preferably with more than one model.</li>
<li>Create disposable prototypes in <code>experiments/</code> before deciding by faith.</li>
<li>If you disagree with a benchmark, run <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">llm-coding-benchmark</a> and bring new data.</li>
<li>Document decisions in markdown. Code got cheap. Documentation too.</li>
<li>Automate deploy. If it depends on a manual ritual, you will stop doing it.</li>
</ul>
<p>In the end, it is the same advice as always: tools change, engineering does not. LLM increases leverage. If you already had good habits, you will produce more. If you did not, you will just produce mess faster.</p>
<p>And yes, this changes the interface of the work. I already wrote about that in <a href="/en/2026/04/11/vs-code-is-the-new-punch-card/">&ldquo;VS Code Is the New Punch Card&rdquo;</a>: polishing pixels in an editor is now old-programmer nostalgia. The hard part is writing intent correctly, reviewing execution without being lazy, measuring results, throwing away cheap code, and not confusing opinion with data.</p>
]]></content:encoded><category>ai-jail</category><category>ai-memory</category><category>ai-usagebar</category><category>ghpending</category></item><item><title>ai-jail: Sandbox for AI Agents — From Shell Script to Real Tool</title><link>https://akitaonrails.github.io/en/2026/03/01/ai-jail-sandbox-for-ai-agents-from-shell-script-to-real-tool/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/03/01/ai-jail-sandbox-for-ai-agents-from-shell-script-to-real-tool/</guid><pubDate>Sun, 01 Mar 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;This post is a direct follow-up to &lt;a href="https://akitaonrails.github.io/en/2026/01/10/ai-agents-locking-down-your-system/"&gt;AI Agents: Locking Down Your System&lt;/a&gt;, where I showed how to use bubblewrap to build a manual jail for your AI agents. If you haven&amp;rsquo;t read it, read it before continuing.&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;In January I published a ~170-line shell script that built a sandbox with bubblewrap to run Claude Code, OpenCode, Crush and any other AI agent. It worked. It solved the problem. But it was a bash script dropped in &lt;code&gt;~/.local/bin/&lt;/code&gt; that you had to copy, paste, and pray you wouldn&amp;rsquo;t need to customize too much.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is a direct follow-up to <a href="/en/2026/01/10/ai-agents-locking-down-your-system/">AI Agents: Locking Down Your System</a>, where I showed how to use bubblewrap to build a manual jail for your AI agents. If you haven&rsquo;t read it, read it before continuing.</p>
<p>&ndash;</p>
<p>In January I published a ~170-line shell script that built a sandbox with bubblewrap to run Claude Code, OpenCode, Crush and any other AI agent. It worked. It solved the problem. But it was a bash script dropped in <code>~/.local/bin/</code> that you had to copy, paste, and pray you wouldn&rsquo;t need to customize too much.</p>
<p>Two months of using that script every day showed me its limits. I wanted per-project configuration. I wanted macOS support for the devs on my team. I wanted to stop editing bash arrays every time I needed an extra directory. And I wanted something someone could install with <code>brew install</code> or <code>cargo install</code> in 10 seconds, without reading 170 lines of script.</p>
<p>Result: <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>. A Rust tool, ~880KB, 4 dependencies, 124 tests, that does exactly what that script did and more. I&rsquo;ll explain what changed and why you should be using it.</p>
<h2>The Problem (again, for those who skipped the previous post)<span class="hx:absolute hx:-mt-20" id="the-problem-again-for-those-who-skipped-the-previous-post"></span>
    <a href="#the-problem-again-for-those-who-skipped-the-previous-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>AI coding agents need access to your filesystem. They need to run compilers, linters, grep, ls, make, cargo, npm. The minimum to be useful. The problem is that along with that access comes the ability to read <code>~/.aws/credentials</code>, exfiltrate your SSH keys, or run an <code>rm -rf</code> outside the project directory.</p>
<p>It isn&rsquo;t paranoia. Supply-chain attacks are real. Every other week some NPM, PyPI or RubyGems lib gets compromised. If the agent runs <code>npm install</code> and a malicious post-install script tries to exfiltrate your data, the only thing between the attacker and your credentials is whatever barrier you set up beforehand.</p>
<p>The answer is a sandbox. Specifically, one that lets the agent work in the project directory with the tools it needs, but makes the entire rest of the system invisible.</p>
<h2>From Script to Tool<span class="hx:absolute hx:-mt-20" id="from-script-to-tool"></span>
    <a href="#from-script-to-tool" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The shell script from the previous post already solved this with bubblewrap. ai-jail solves the same problem, but addresses the limitations that two months of daily use revealed:</p>
<table>
  <thead>
      <tr>
          <th>Shell script</th>
          <th>ai-jail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Configuration by editing bash arrays</td>
          <td>Per-project <code>.ai-jail</code> TOML file</td>
      </tr>
      <tr>
          <td>Linux only</td>
          <td>Linux + macOS</td>
      </tr>
      <tr>
          <td>Hardcoded GPU/Docker/Display</td>
          <td>Auto-detection with flags to turn things off</td>
      </tr>
      <tr>
          <td>No dry-run</td>
          <td><code>--dry-run --verbose</code> shows everything</td>
      </tr>
      <tr>
          <td>No lockdown</td>
          <td><code>--lockdown</code> for paranoid mode</td>
      </tr>
      <tr>
          <td>Copy/paste to install</td>
          <td><code>brew install</code>, <code>cargo install</code>, <code>mise</code></td>
      </tr>
      <tr>
          <td>No bootstrap</td>
          <td><code>--bootstrap</code> generates permission configs for Claude/Codex/OpenCode</td>
      </tr>
  </tbody>
</table>
<p>The core logic is the same: bubblewrap creates isolated PID, UTS and IPC namespaces, mounts <code>$HOME</code> as an ephemeral tmpfs, and only mounts the project directory with write access. The difference is that all of that is now configurable without editing code.</p>
<h2>Installation<span class="hx:absolute hx:-mt-20" id="installation"></span>
    <a href="#installation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Four ways:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Homebrew (macOS and Linux)</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Cargo</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Mise</span>
</span></span><span class="line"><span class="cl">mise use -g ubi:akitaonrails/ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Direct binary from GitHub Releases</span>
</span></span><span class="line"><span class="cl">curl -fsSL https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-linux-x86_64.tar.gz <span class="p">|</span> tar xz
</span></span><span class="line"><span class="cl">sudo mv ai-jail /usr/local/bin/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>On Linux, bubblewrap needs to be installed separately: <code>pacman -S bubblewrap</code> (Arch), <code>apt install bubblewrap</code> (Debian/Ubuntu), <code>dnf install bubblewrap</code> (Fedora). On macOS no extra dependency is needed.</p>
<h2>Basic Usage<span class="hx:absolute hx:-mt-20" id="basic-usage"></span>
    <a href="#basic-usage" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/Projects/my-app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run Claude Code inside the sandbox</span>
</span></span><span class="line"><span class="cl">ai-jail claude
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run Codex</span>
</span></span><span class="line"><span class="cl">ai-jail codex
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run OpenCode</span>
</span></span><span class="line"><span class="cl">ai-jail opencode
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Plain bash for debugging</span>
</span></span><span class="line"><span class="cl">ai-jail bash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Any command</span>
</span></span><span class="line"><span class="cl">ai-jail -- python script.py</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>On the first run, it creates an <code>.ai-jail</code> file in the project directory:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># ai-jail sandbox configuration</span>
</span></span><span class="line"><span class="cl"><span class="c"># Edit freely. Regenerate with: ai-jail --clean --init</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;claude&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">rw_maps</span> <span class="p">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="nx">ro_maps</span> <span class="p">=</span> <span class="p">[]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This file is committable to the repo. When another dev on your team clones the project and runs <code>ai-jail</code>, the same configuration applies.</p>
<p>If you want to add extra directories, you can do it from the CLI or directly in the TOML:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Extra directory with write access</span>
</span></span><span class="line"><span class="cl">ai-jail --rw-map ~/Projects/shared-lib claude
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Extra read-only directory</span>
</span></span><span class="line"><span class="cl">ai-jail --map /opt/datasets claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Want to see what the sandbox is going to do without running anything?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail --dry-run --verbose claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It shows every mount point, every isolation flag, the full bubblewrap command. No surprises.</p>
<h2>Why Bubblewrap on Linux<span class="hx:absolute hx:-mt-20" id="why-bubblewrap-on-linux"></span>
    <a href="#why-bubblewrap-on-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I evaluated the alternatives before deciding. The <a href="https://github.com/akitaonrails/ai-jail/blob/master/docs/sandbox-alternatives.md"target="_blank" rel="noopener">full analysis document</a> is in the repository, but the short version is:</p>
<p>Bubblewrap (bwrap) is the sandbox Flatpak uses to isolate every desktop app. ~50KB binary, ~4000 lines of C, maintained by the GNOME team. It runs without root using <code>CLONE_NEWUSER</code> to create namespaces without elevated privileges. It&rsquo;s packaged in every relevant Linux distro and tested at scale by millions of Flatpak installations.</p>
<p>I considered and rejected the alternatives. Firejail requires setuid root, and trusting setuid to protect against agents already running on your system is contradictory. nsjail and minijail are designed for production environments (Google uses them internally), too complex for a dev workstation. systemd-nspawn requires root and is meant for system containers, not for isolating a single process.</p>
<p>Landlock is a different case. It doesn&rsquo;t replace bubblewrap — it has nothing to do with namespaces or mount isolation. But it complements. Landlock is a Linux Security Module that controls access at the VFS level, independent of mount namespaces. That closes vectors bwrap alone doesn&rsquo;t cover: escape paths through <code>/proc</code>, symlink tricks inside permitted mounts, and it serves as a safety net against bugs in the namespace machinery itself. As of v0.4.0, ai-jail applies Landlock automatically on kernels 5.13+ as defense-in-depth. It uses ABI V3 (Linux 6.2+) with graceful degradation to V1 on older kernels, and turns into a silent no-op if the kernel doesn&rsquo;t support it. If it causes problems with some specific tool, <code>--no-landlock</code> turns it off.</p>
<p>Bubblewrap hits the exact sweet spot: real isolation without root, on every distro, and simple enough to wrap in an 880KB binary.</p>
<h2>What the Sandbox Does on Linux<span class="hx:absolute hx:-mt-20" id="what-the-sandbox-does-on-linux"></span>
    <a href="#what-the-sandbox-does-on-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When you run <code>ai-jail claude</code>, this is what happens:</p>
<p>The agent runs in isolated PID, UTS and IPC namespaces, with hostname <code>ai-sandbox</code>, and dies automatically if the parent dies (<code>--die-with-parent</code>).</p>
<p>The filesystem is mounted in a specific sequence (bubblewrap is order-dependent). <code>/usr</code>, <code>/etc</code>, <code>/opt</code>, <code>/sys</code> come in read-only for system tools. <code>/dev</code> and <code>/proc</code> are mounted for device and process access. <code>/tmp</code> and <code>/run</code> come in as fresh tmpfs. GPUs auto-detected (<code>/dev/nvidia*</code>, <code>/dev/dri</code>). Docker socket, X11/Wayland, <code>/dev/shm</code>, all auto-detected and mounted if they exist.</p>
<p>The most important part is how the home directory is handled. <code>$HOME</code> is mounted as an empty tmpfs. Then, selectively, dotfiles get layered on top. <code>.gnupg</code>, <code>.aws</code>, <code>.ssh</code>, <code>.mozilla</code>, <code>.sparrow</code> are never mounted (sensitive data). <code>.claude</code>, <code>.crush</code>, <code>.codex</code>, <code>.aider</code>, <code>.config</code>, <code>.cargo</code>, <code>.cache</code>, <code>.docker</code> come in as read-write because the agents need to write here. Everything else comes in read-only. Inside <code>~/.config</code>, browser subdirectories are hidden behind tmpfs: <code>BraveSoftware</code>, <code>Bitwarden</code>. Same in <code>~/.cache</code>: <code>BraveSoftware</code>, <code>chromium</code>, <code>spotify</code>, <code>nvidia</code>. The agent can&rsquo;t even see those directories exist.</p>
<p>The current project directory is the only place with write permission (besides the tool dotdirs). The agent modifies the code, but doesn&rsquo;t touch anything else.</p>
<h2>macOS: sandbox-exec<span class="hx:absolute hx:-mt-20" id="macos-sandbox-exec"></span>
    <a href="#macos-sandbox-exec" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>On macOS, the backend is <code>sandbox-exec</code> with SBPL (Sandbox Profile Language) profiles. It&rsquo;s a legacy Apple API, officially deprecated but with no public replacement. It works today, but Apple may remove it in the future.</p>
<p>ai-jail generates an SBPL profile at runtime that mirrors the same logic as Linux:</p>
<ul>
<li>Default deny on everything</li>
<li>Allows process operations (exec, fork, signal)</li>
<li>Allows network (except in lockdown)</li>
<li>Allows global reads, denies sensitive paths (<code>.gnupg</code>, <code>.aws</code>, <code>.ssh</code>, <code>~/Library/Keychains</code>, <code>~/Library/Mail</code>)</li>
<li>Allows writes only in the project directory, tool dotfiles, and <code>/tmp</code></li>
</ul>
<p>The limitations are real. GPU (Metal) and Display (Cocoa) are system-level on macOS, sandbox-exec can&rsquo;t restrict them. The <code>--no-gpu</code> and <code>--no-display</code> flags simply have no effect on macOS. Cross-platform parity is approximate, not exact.</p>
<p>Even with those limitations, it&rsquo;s better than running the agent completely open. sandbox-exec protects against access to sensitive filesystem areas and, in lockdown, removes write and network permissions.</p>
<h2>Windows: Not Supported (And Probably Never Will Be)<span class="hx:absolute hx:-mt-20" id="windows-not-supported-and-probably-never-will-be"></span>
    <a href="#windows-not-supported-and-probably-never-will-be" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>It&rsquo;s not for lack of interest, it&rsquo;s lack of primitives. Windows has no userspace equivalent to Linux namespaces. No mount API like bubblewrap. AppContainers exist but use a completely different security model, require admin privileges, and mapping bwrap functionality to AppContainers would effectively mean writing another project from scratch.</p>
<p>The Windows answer is WSL 2:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Inside WSL 2 (real Linux kernel)</span>
</span></span><span class="line"><span class="cl">sudo apt install bubblewrap
</span></span><span class="line"><span class="cl">cargo install ai-jail
</span></span><span class="line"><span class="cl"><span class="c1"># or: mise use -g ubi:akitaonrails/ai-jail</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /mnt/c/Users/you/Projects/my-app
</span></span><span class="line"><span class="cl">ai-jail claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>WSL 2 runs an actual Linux kernel. Bubblewrap works normally. Windows files are accessible at <code>/mnt/c/</code>. I/O performance is slower across the 9p mount, but it works. For large projects, cloning inside <code>~/Projects/</code> on the Linux side improves performance considerably.</p>
<h2>Lockdown Mode<span class="hx:absolute hx:-mt-20" id="lockdown-mode"></span>
    <a href="#lockdown-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For workloads you really don&rsquo;t trust, there&rsquo;s <code>--lockdown</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail --lockdown bash</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Lockdown does everything normal mode does, but goes further. The project gets mounted read-only (not read-write). GPU, Docker, Display and mise are disabled. <code>--rw-map</code> and <code>--map</code> flags are ignored. <code>$HOME</code> becomes pure tmpfs, no host dotfiles. On Linux, the network is cut with <code>--unshare-net</code> and the environment is wiped with <code>--clearenv</code>. On macOS, environment variables are wiped and write and network rules are removed from the SBPL.</p>
<p>It&rsquo;s the most restrictive sandbox you can build short of using a VM. Useful for auditing third-party code or running agents on projects you don&rsquo;t know.</p>
<h2>Bootstrap: Automatic Permission Configuration<span class="hx:absolute hx:-mt-20" id="bootstrap-automatic-permission-configuration"></span>
    <a href="#bootstrap-automatic-permission-configuration" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><code>ai-jail --bootstrap</code> generates permission configurations for the tools you use:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ai-jail --bootstrap</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>For <strong>Claude Code</strong>, it generates <code>~/.claude/settings.json</code> with allow/deny/ask lists. Allows git status, diff, log, ls, grep, cargo, npm, python, docker compose. Blocks rm -rf, sudo, chmod 777, git push &ndash;force. Asks before git push, rm, docker run.</p>
<p>For <strong>Codex</strong>, it generates <code>~/.codex/config.toml</code> with <code>approval_policy = &quot;on-request&quot;</code>.</p>
<p>For <strong>OpenCode</strong>, it generates <code>~/.config/opencode/opencode.json</code> with bash, edit, write permissions.</p>
<p>Before overwriting any file, it makes an automatic backup (<code>settings.json.bak</code>). And it rejects operations if the target is a symlink, to avoid path traversal.</p>
<p>It&rsquo;s exactly the content I put in manually in the <a href="/en/2026/01/10/ai-agents-locking-down-your-system/">previous post</a>, but automated and tested.</p>
<h2>But Claude Code Already Has Its Own Sandbox<span class="hx:absolute hx:-mt-20" id="but-claude-code-already-has-its-own-sandbox"></span>
    <a href="#but-claude-code-already-has-its-own-sandbox" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>It does. Since October 2025, Claude Code has offered a runtime sandbox via the <code>/sandbox</code> command. And guess what it uses underneath? Bubblewrap on Linux and sandbox-exec on macOS. The same stack.</p>
<p>But the differences matter.</p>
<p>ai-jail is tool-agnostic. It works with Claude, Codex, OpenCode, Crush, and any command. Claude&rsquo;s sandbox only protects Claude. If tomorrow you switch agents, ai-jail keeps working the same.</p>
<p>The thing that bothers me most is the escape hatch. When a command fails because of a sandbox restriction, Claude can retry with <code>dangerouslyDisableSandbox</code>, falling back to the normal permission flow. You can disable that (<code>&quot;allowUnsandboxedCommands&quot;: false</code>), but it&rsquo;s opt-out, not opt-in. In ai-jail, there is no escape hatch. The process runs inside bwrap or sandbox-exec, period. There&rsquo;s no way for the agent to decide on its own to leave the sandbox.</p>
<p>Another practical difference: <code>.ai-jail</code> lives in the project directory and can be committed to the repo. Any dev who clones the project inherits the same sandbox policy. Claude&rsquo;s sandbox depends on a global <code>settings.json</code>.</p>
<p>When run inside Docker, Claude&rsquo;s sandbox falls back to an <code>enableWeakerNestedSandbox</code> mode that, in the words of its own documentation, <em>&ldquo;considerably weakens security&rdquo;</em>. ai-jail wasn&rsquo;t designed to run inside Docker (it runs directly on the dev&rsquo;s workstation), so this problem doesn&rsquo;t exist.</p>
<p>About the network: Claude&rsquo;s sandbox routes traffic through a proxy and allows/blocks by domain. ai-jail in normal mode inherits the host network; in lockdown, it cuts the entire network with <code>--unshare-net</code>. Claude&rsquo;s approach is more granular; ai-jail&rsquo;s is simpler and harder to circumvent.</p>
<p>The two aren&rsquo;t mutually exclusive. You can run Claude&rsquo;s sandbox inside ai-jail. ai-jail handles filesystem isolation; Claude&rsquo;s sandbox adds per-domain network filtering. Security layers stack.</p>
<h2>Why Not Use &ndash;dangerously-skip-permissions Without a Jail<span class="hx:absolute hx:-mt-20" id="why-not-use-dangerously-skip-permissions-without-a-jail"></span>
    <a href="#why-not-use-dangerously-skip-permissions-without-a-jail" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ll be blunt: if you run Claude Code with <code>--dangerously-skip-permissions</code> without any sandbox, you&rsquo;re trusting blindly that the LLM will never execute anything destructive. And you&rsquo;re trusting that none of your project&rsquo;s dependencies has been compromised in a supply-chain attack.</p>
<p>Every <code>--dangerously</code> flag has that name for a reason. Claude Code is explicit: that mode exists for CI/CD and automation in environments that are already isolated (containers, throwaway VMs). Not for your personal workstation with <code>~/.aws/credentials</code>, <code>~/.gnupg/</code>, SSH keys, and your browser&rsquo;s password vault.</p>
<p>With ai-jail, the agent has total autonomy inside the sandbox. It does whatever it wants in the project directory, uses the tools it needs, and can&rsquo;t access anything outside what was explicitly permitted.</p>
<h2>ai-jail + Git: The Safety Net You Already Have<span class="hx:absolute hx:-mt-20" id="ai-jail--git-the-safety-net-you-already-have"></span>
    <a href="#ai-jail--git-the-safety-net-you-already-have" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s something I haven&rsquo;t mentioned yet that changes the risk calculus: if your project is in a Git repo, with a remote on GitHub/GitLab, and the agent doesn&rsquo;t have permission to <code>git push</code>, the damage it can cause is limited to the local directory.</p>
<p>Think about it. The worst-case scenario inside ai-jail is the agent corrupting every file in the project. Annoying? Sure. Catastrophic? No. You run <code>git checkout .</code> and you&rsquo;re back to the last commit. If it corrupts <code>.git</code> somehow (unlikely, but possible), you delete the directory and clone again. The remote was never touched.</p>
<p>That&rsquo;s why ai-jail&rsquo;s <code>--bootstrap</code> puts <code>git push</code> on the &ldquo;ask&rdquo; list (ask before running), not the &ldquo;allow&rdquo; list. And <code>git push --force</code> goes straight to &ldquo;deny&rdquo;. The agent can commit locally all it wants, can create branches, can rebase. None of that affects the remote. When it comes time to push, you review what it did and decide whether it goes live.</p>
<p>That combination, sandbox for the filesystem + Git for the code + manual push, already gives you a very reasonable security level for daily use. ai-jail protects your personal data and the system. Git protects your code. And the decision to publish stays yours.</p>
<p>If you want to go further, the next two sections cover additional layers.</p>
<h2>ai-jail vs Dev Containers<span class="hx:absolute hx:-mt-20" id="ai-jail-vs-dev-containers"></span>
    <a href="#ai-jail-vs-dev-containers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Since I wrote ai-jail, the most frequent question is: <em>&ldquo;why not use Dev Containers?&rdquo;</em>. The short answer is that one doesn&rsquo;t replace the other. They solve different problems.</p>
<p>Dev Containers (the <a href="https://containers.dev"target="_blank" rel="noopener">containers.dev</a> spec) define a complete development environment in a <code>devcontainer.json</code>. You specify base image, tools, VS Code extensions, environment variables, and the editor mounts everything for you in a Docker container. Docker also recently launched Docker Sandboxes, which go further and run each agent in a microVM with Firecracker, with hardware isolation.</p>
<p>ai-jail does none of that. It doesn&rsquo;t define an environment. It doesn&rsquo;t install tools. It doesn&rsquo;t run a Docker image. It takes the environment that already exists on your machine and restricts what the process can access.</p>
<p>In practice, the difference is:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Dev Container</th>
          <th>ai-jail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>What it does</td>
          <td>Defines and provisions a complete isolated environment</td>
          <td>Restricts process access to the existing filesystem</td>
      </tr>
      <tr>
          <td>Setup</td>
          <td><code>devcontainer.json</code> + Docker</td>
          <td><code>.ai-jail</code> TOML + bubblewrap</td>
      </tr>
      <tr>
          <td>Startup</td>
          <td>Seconds (image pull, container build)</td>
          <td>Milliseconds (fork + exec of bwrap)</td>
      </tr>
      <tr>
          <td>Tools</td>
          <td>Whatever you put in the image</td>
          <td>Whatever&rsquo;s already installed on your machine</td>
      </tr>
      <tr>
          <td>GPU</td>
          <td>Requires NVIDIA Container Toolkit configuration</td>
          <td>Auto-detects <code>/dev/nvidia*</code> and <code>/dev/dri</code></td>
      </tr>
      <tr>
          <td>Daemon</td>
          <td>Requires Docker daemon running</td>
          <td>Nothing besides bwrap</td>
      </tr>
      <tr>
          <td>Reproducibility</td>
          <td>High (fixed image)</td>
          <td>Depends on what&rsquo;s installed on the host</td>
      </tr>
      <tr>
          <td>Network isolation</td>
          <td>Docker Sandboxes: per-domain firewall</td>
          <td>Lockdown: cuts everything with <code>--unshare-net</code></td>
      </tr>
  </tbody>
</table>
<p>Dev Container makes more sense when you need the whole team to have exactly the same environment, or when the project has dependencies nobody wants to install on the host, or for running non-interactive agents in CI/CD. Docker Sandboxes with microVM are the strongest isolation that exists outside a dedicated VM.</p>
<p>ai-jail makes more sense when you already have the environment configured and want instant startup with no Docker daemon. Or when you use tools that are annoying to run inside a container (CUDA, Wayland, mise). Or simply when you want the same protection for any agent, not just the ones with devcontainer integration.</p>
<p>And you can combine them. I know people who run ai-jail inside a Dev Container to get environment reproducibility + filesystem restriction. Security layers stack.</p>
<h2>Immutable Operating Systems: The Last Layer<span class="hx:absolute hx:-mt-20" id="immutable-operating-systems-the-last-layer"></span>
    <a href="#immutable-operating-systems-the-last-layer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you want to take security seriously, the third layer is the operating system itself.</p>
<p>Immutable systems like <a href="https://fedoraproject.org/atomic-desktops/silverblue/"target="_blank" rel="noopener">Fedora Silverblue</a>, <a href="https://nixos.org/"target="_blank" rel="noopener">NixOS</a>, and <a href="https://aeondesktop.github.io/"target="_blank" rel="noopener">openSUSE Aeon</a> have a read-only root filesystem. The base system can&rsquo;t be modified by any process, even with root. Updates are atomic: they either apply completely or not at all. And if something goes wrong, you roll back to the previous image in one reboot.</p>
<p>In practice, that means even if an AI agent escaped the sandbox (exploiting a kernel vulnerability, for example), it couldn&rsquo;t modify the system persistently. On the next reboot, the system returns to the declared state. On NixOS, the entire system is defined by a configuration file (<code>configuration.nix</code>). On Silverblue, the base is an OSTree image that gets atomic updates via <code>rpm-ostree</code>.</p>
<p>For developers, the catch is: your dev tools run in containers (Toolbox/Distrobox on Silverblue, <code>nix-shell</code> on NixOS). The base system stays untouched. Desktop apps come via Flatpak, which already runs in a sandbox. The result is that the host&rsquo;s attack surface is minimal.</p>
<p>Fedora Silverblue is probably the most accessible entry point. It&rsquo;s already Fedora underneath, with GNOME, works with hardware Fedora supports, and Toolbox gives you a containerized Fedora Server where you install whatever you want without touching the host. NixOS is more powerful (full reproducibility, declarative rollback), but the learning curve is real.</p>
<p>The full combination looks like this: the immutable OS handles the system (read-only filesystem, atomic updates, one-reboot rollback). ai-jail handles the session (isolated namespace, ephemeral home, sensitive data invisible). And Git handles the code (remote untouched as long as the agent doesn&rsquo;t have push).</p>
<p>None of those layers is perfect on its own. But the attack that punches through all three at the same time — escaping the namespace, persisting on a read-only filesystem, and corrupting a Git remote — is a scenario I&rsquo;d be comfortable calling unlikely.</p>
<p>The best part is that none of them requires changing how you work. ai-jail is one command before your agent. Git you already use. And an immutable OS is an installation, not a workflow change.</p>
<h2>Technical Details (For Those Who Care)<span class="hx:absolute hx:-mt-20" id="technical-details-for-those-who-care"></span>
    <a href="#technical-details-for-those-who-care" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Written in Rust with 124 tests and 4 dependencies: <code>lexopt</code> (CLI parsing without clap), <code>serde</code> + <code>toml</code> (config), <code>nix</code> (Unix syscalls). No async runtime, no color framework (raw ANSI), ~880KB binary with LTO and strip.</p>
<p>Signal handling is done correctly: SIGINT, SIGTERM, and SIGHUP are forwarded to the child process. The handler only calls <code>libc::kill</code>, which is async-signal-safe. Process reaping uses <code>waitpid</code> in a loop with retry on EINTR.</p>
<p>Temporary files (like the custom <code>/etc/hosts</code> the sandbox mounts) use RAII: a <code>SandboxGuard</code> that implements <code>Drop</code> in Rust. If the parent process dies for any reason, cleanup happens.</p>
<p>Configuration compatibility is guaranteed by development policy: never remove fields, never rename, new fields always with <code>#[serde(default)]</code>, unknown fields are silently ignored. Regression tests for old <code>.ai-jail</code> formats guarantee that updating the binary never breaks existing configs. There are 32 tests just for config.</p>
<h2>Roadmap<span class="hx:absolute hx:-mt-20" id="roadmap"></span>
    <a href="#roadmap" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>What&rsquo;s left:</p>
<ul>
<li>More tool backends in bootstrap: Aider, Cursor, Windsurf. As more agents standardize permission configuration files.</li>
<li>Profile sharing for monorepos, so you don&rsquo;t have to configure each service separately.</li>
</ul>
<h2>Installation and First Use (Quick Recap)<span class="hx:absolute hx:-mt-20" id="installation-and-first-use-quick-recap"></span>
    <a href="#installation-and-first-use-quick-recap" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. Install</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl"><span class="c1"># or: cargo install ai-jail</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. On Linux, install bubblewrap</span>
</span></span><span class="line"><span class="cl">sudo pacman -S bubblewrap  <span class="c1"># Arch</span>
</span></span><span class="line"><span class="cl"><span class="c1"># sudo apt install bubblewrap  # Debian/Ubuntu</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Enter the project and run</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/Projects/my-app
</span></span><span class="line"><span class="cl">ai-jail claude
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. (Optional) Generate permission configs</span>
</span></span><span class="line"><span class="cl">ai-jail --bootstrap
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5. (Optional) See what the sandbox does</span>
</span></span><span class="line"><span class="cl">ai-jail --dry-run --verbose claude</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>.ai-jail</code> file it creates can be committed to your repo. From then on, any dev who clones the project runs <code>ai-jail claude</code> and gets the same sandbox.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>January&rsquo;s shell script solved the problem. ai-jail solves the problem properly. Per-project config, macOS support, lockdown mode, permission bootstrap, dry-run for auditing, and an 880KB binary that installs in 10 seconds.</p>
<p>If you use AI agents to code, run them in a sandbox. The LLM&rsquo;s good intentions are no guarantee of anything, and supply-chain attacks don&rsquo;t pick their victims. Process isolation is the barrier that works.</p>
<p>The project is GPL-3.0 and is on GitHub: <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">github.com/akitaonrails/ai-jail</a></p>
<p>Issues and PRs are welcome.</p>
]]></content:encoded><category>ai-jail</category><category>coding-agents</category><category>security</category><category>rust</category></item><item><title>AI Agents: Locking Down Your System</title><link>https://akitaonrails.github.io/en/2026/01/10/ai-agents-locking-down-your-system/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/01/10/ai-agents-locking-down-your-system/</guid><pubDate>Sat, 10 Jan 2026 11:50:36 -0300</pubDate><description>&lt;p&gt;These days we have dozens of AI coding agents — Cursor, Claude Code, Windsurf, Copilot, OpenCode, Crush and more. But there&amp;rsquo;s a problem that&amp;rsquo;s already turned into a meme: the risk of them trashing your system or shipping your private data off to someone on the internet (malware-style behavior).&lt;/p&gt;
&lt;p&gt;The thing is, to be useful these tools need access to your filesystem so they can poke around your project files and analyze your code. And more: they need to run things like compilers, linters, and the everyday stuff like grep, sed, awk, ls, and other system commands.&lt;/p&gt;</description><content:encoded><![CDATA[<p>These days we have dozens of AI coding agents — Cursor, Claude Code, Windsurf, Copilot, OpenCode, Crush and more. But there&rsquo;s a problem that&rsquo;s already turned into a meme: the risk of them trashing your system or shipping your private data off to someone on the internet (malware-style behavior).</p>
<p>The thing is, to be useful these tools need access to your filesystem so they can poke around your project files and analyze your code. And more: they need to run things like compilers, linters, and the everyday stuff like grep, sed, awk, ls, and other system commands.</p>
<p>Eventually, an LLM might fire off some random <code>rm -rf /</code> and start wiping things from your system, for example. The code itself, if it accidentally deletes something it shouldn&rsquo;t, in theory you should be safe because your code should at the bare minimum be in a Git repo (GitHub, GitLab, etc.), so you&rsquo;d just revert the change. But if the agent runs an <code>rm</code> outside your project directory, you can be in for all kinds of trouble.</p>
<p>It doesn&rsquo;t happen often and all these tools have checks for it, including asking whether you really want to run a command. At the same time, they also have an option to let any command run during the session without asking — because at some point it gets tedious to &ldquo;ok&rdquo; every individual command. Everyone eventually gets lazy and says <em>&ldquo;screw it, run whatever you want&rdquo;</em>.</p>
<p>And you might be thinking <em>&ldquo;oh, my favorite tool is well-behaved, it always asks before running anything, so I&rsquo;m in control&rdquo;</em>. Don&rsquo;t forget: <strong>SUPPLY-CHAIN ATTACKS</strong> exist. Every other day some open source library gets compromised with malware and the programs that depend on those libs end up infected without anyone noticing. This has happened many times, in many NPM libraries for example. There&rsquo;s no way to guarantee that even open source software is trustworthy. Always assume everything is insecure until proven otherwise, and treat these programs accordingly: with maximum restrictions.</p>
<p>To make sure these agents really aren&rsquo;t going to do anything outside the project directory, the best option is to build a <strong>JAIL</strong>. Here&rsquo;s the command for that:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bwrap --ro-bind /usr /usr <span class="se">\
</span></span></span><span class="line"><span class="cl">      --ro-bind /bin /bin <span class="se">\
</span></span></span><span class="line"><span class="cl">      --ro-bind /lib /lib <span class="se">\
</span></span></span><span class="line"><span class="cl">      --ro-bind /lib64 /lib64 <span class="se">\
</span></span></span><span class="line"><span class="cl">      --dev /dev <span class="se">\
</span></span></span><span class="line"><span class="cl">      --proc /proc <span class="se">\
</span></span></span><span class="line"><span class="cl">      --bind <span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span> <span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      --chdir <span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      --unshare-all <span class="se">\
</span></span></span><span class="line"><span class="cl">      --share-net <span class="se">\
</span></span></span><span class="line"><span class="cl">      bash</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>bwrap</code> is <strong>Bubblewrap</strong>, one of the components that make things like <strong>Flatpak</strong> possible. It creates a sandbox. The Bash that fires up when you run the command above is essentially in &ldquo;jail&rdquo; — locked in. Inside it we hand out read-only access to a few system directories in case it needs libraries and tools. That&rsquo;s what <code>--ro-bind</code> is for.</p>
<p>But only the current directory <code>pwd</code> has write permission. Any command that runs inside the jail has no permission to write anywhere else on your system. Look at the example in the screenshot below where I try to delete a file in my <code>$HOME</code>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260110120225_screenshot-2026-01-10_12-02-14.png" alt="sandbox"  loading="lazy" /></p>
<p>It can&rsquo;t even list the files in my home directory. Everything is invisible inside the sandbox. This is one of the upsides of apps running under Flatpak: they&rsquo;re much more isolated than apps installed directly.</p>
<p>Inside this sandboxed bash we can now run <code>opencode</code>, <code>crush</code>, <code>cursor</code> or any other agent and be confident that they aren&rsquo;t going to do anything stupid to your system.</p>
<p>To make life easier, I&rsquo;d recommend creating a script like <code>~/.local/bin/ai-jail</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ai-jail — bubblewrap sandbox for AI coding agents</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Mounts the project dir read-write, auto-discovers home dotfiles with a</span>
</span></span><span class="line"><span class="cl"><span class="c1"># deny-list for sensitive dirs, and isolates namespaces.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Usage: ai-jail [--map PATH]... COMMAND [ARGS...]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#        ai-jail claude</span>
</span></span><span class="line"><span class="cl"><span class="c1">#        ai-jail bash</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">PROJECT_DIR</span><span class="o">=</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">TEMP_HOSTS</span><span class="o">=</span><span class="k">$(</span>mktemp /tmp/bwrap-hosts.XXXXXX<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;rm -f &#34;$TEMP_HOSTS&#34;&#39;</span> EXIT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Mise discovery ─────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="nv">REAL_MISE_BIN</span><span class="o">=</span><span class="k">$(</span><span class="nb">type</span> -p mise 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Localhost fix (Go resolver needs /etc/hosts) ───────────────</span>
</span></span><span class="line"><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;127.0.0.1 localhost ai-sandbox\n::1       localhost ai-sandbox\n&#39;</span> &gt; <span class="s2">&#34;</span><span class="nv">$TEMP_HOSTS</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Parse --map / --rw-map arguments ─────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="nv">EXTRA_MOUNTS</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> --map <span class="o">||</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> --rw-map <span class="o">]]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nv">FLAG</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">MAP_PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -e <span class="s2">&#34;</span><span class="nv">$MAP_PATH</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$FLAG</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;--rw-map&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            <span class="nv">EXTRA_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$MAP_PATH</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$MAP_PATH</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span>
</span></span><span class="line"><span class="cl">            <span class="nv">EXTRA_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--ro-bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$MAP_PATH</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$MAP_PATH</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;Warning: Path </span><span class="nv">$MAP_PATH</span><span class="s2"> not found, skipping.&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="nb">shift</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Mise init command ──────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$REAL_MISE_BIN</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">MISE_INIT</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$REAL_MISE_BIN</span><span class="s2"> trust &amp;&amp; eval \&#34;\$(</span><span class="nv">$REAL_MISE_BIN</span><span class="s2"> activate bash)\&#34; &amp;&amp; eval \&#34;\$(</span><span class="nv">$REAL_MISE_BIN</span><span class="s2"> env)\&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nv">MISE_INIT</span><span class="o">=</span><span class="s2">&#34;true&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Dotfile deny-list (never mounted — sensitive data) ─────────</span>
</span></span><span class="line"><span class="cl"><span class="nv">DOTDIR_DENY</span><span class="o">=(</span>.gnupg .aws .mozilla .basilisk-dev .sparrow<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Subdirs of ~/.config to hide (tmpfs over rw config mount)</span>
</span></span><span class="line"><span class="cl"><span class="nv">CONFIG_DENY</span><span class="o">=(</span>BraveSoftware Bitwarden<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Subdirs of ~/.cache to hide (sensitive browser/app caches)</span>
</span></span><span class="line"><span class="cl"><span class="nv">CACHE_DENY</span><span class="o">=(</span>BraveSoftware basilisk-dev chromium spotify nvidia mesa_shader_cache<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Dotdirs requiring read-write access</span>
</span></span><span class="line"><span class="cl"><span class="nv">DOTDIR_RW</span><span class="o">=(</span>.claude .crush .codex .aider .config .cargo .cache .docker<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Helper functions ───────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl">is_denied<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">name</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> d in <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOTDIR_DENY</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">do</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;</span><span class="nv">$d</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return</span> 0<span class="p">;</span> <span class="k">done</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">is_rw<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">name</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> d in <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOTDIR_RW</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">do</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;</span><span class="nv">$d</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return</span> 0<span class="p">;</span> <span class="k">done</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Auto-discover dot-directories in $HOME ─────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Only directories — regular dotfiles (e.g. .claude.json) are NOT mounted.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># The tmpfs $HOME is writable so tools can create dotfiles as needed.</span>
</span></span><span class="line"><span class="cl"><span class="nv">DOTFILE_MOUNTS</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> entry in <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">&#34;</span>/.*<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$entry</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">||</span> <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    <span class="nv">name</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$entry</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;.&#34;</span> <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;..&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    is_denied <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> is_rw <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="nv">DOTFILE_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$entry</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/</span><span class="nv">$name</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="nv">DOTFILE_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--ro-bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$entry</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/</span><span class="nv">$name</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Explicit dotfile mounts (regular files) ────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.gitconfig&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">DOTFILE_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--ro-bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.gitconfig&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.gitconfig&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.claude.json&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">DOTFILE_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.claude.json&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.claude.json&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Hide sensitive subdirs inside ~/.config (after rw mount) ───</span>
</span></span><span class="line"><span class="cl"><span class="nv">CONFIG_HIDE_MOUNTS</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> denied in <span class="s2">&#34;</span><span class="si">${</span><span class="nv">CONFIG_DENY</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/</span><span class="nv">$denied</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">CONFIG_HIDE_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--tmpfs&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/</span><span class="nv">$denied</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Hide sensitive subdirs inside ~/.cache (after rw mount) ──</span>
</span></span><span class="line"><span class="cl"><span class="nv">CACHE_HIDE_MOUNTS</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> denied in <span class="s2">&#34;</span><span class="si">${</span><span class="nv">CACHE_DENY</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.cache/</span><span class="nv">$denied</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">CACHE_HIDE_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--tmpfs&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.cache/</span><span class="nv">$denied</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Override ~/.local subdirs as rw (parent .local is ro) ──────</span>
</span></span><span class="line"><span class="cl"><span class="nv">LOCAL_OVERRIDES</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/state&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">LOCAL_OVERRIDES</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/state&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/state&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> rw_share in zoxide crush opencode atuin mise yarn flutter kotlin NuGet pipx ruby-advisory-db uv<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/</span><span class="nv">$rw_share</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">LOCAL_OVERRIDES</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/</span><span class="nv">$rw_share</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/share/</span><span class="nv">$rw_share</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── GPU device mounts (NVIDIA + DRM) ─────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="nv">GPU_MOUNTS</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> dev in /dev/nvidia* /dev/dri<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">[</span> -e <span class="s2">&#34;</span><span class="nv">$dev</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">GPU_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--dev-bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$dev</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$dev</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Docker socket ────────────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="nv">DOCKER_MOUNT</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -S /var/run/docker.sock <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">DOCKER_MOUNT</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;/var/run/docker.sock&#34;</span> <span class="s2">&#34;/var/run/docker.sock&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Shared memory (Chromium needs large /dev/shm) ───────────</span>
</span></span><span class="line"><span class="cl"><span class="nv">SHM_MOUNT</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -d /dev/shm <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">SHM_MOUNT</span><span class="o">+=(</span><span class="s2">&#34;--dev-bind&#34;</span> <span class="s2">&#34;/dev/shm&#34;</span> <span class="s2">&#34;/dev/shm&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Display passthrough (X11 + Wayland) ─────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="nv">DISPLAY_MOUNTS</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl"><span class="nv">DISPLAY_ENV</span><span class="o">=()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># X11 / XWayland socket</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -d /tmp/.X11-unix <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">DISPLAY_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;/tmp/.X11-unix&#34;</span> <span class="s2">&#34;/tmp/.X11-unix&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DISPLAY</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">DISPLAY_ENV</span><span class="o">+=(</span><span class="s2">&#34;--setenv&#34;</span> <span class="s2">&#34;DISPLAY&#34;</span> <span class="s2">&#34;</span><span class="nv">$DISPLAY</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">XAUTHORITY</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">DISPLAY_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--ro-bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$XAUTHORITY</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$XAUTHORITY</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">DISPLAY_ENV</span><span class="o">+=(</span><span class="s2">&#34;--setenv&#34;</span> <span class="s2">&#34;XAUTHORITY&#34;</span> <span class="s2">&#34;</span><span class="nv">$XAUTHORITY</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Wayland socket (lives in XDG_RUNTIME_DIR, typically /run/user/UID)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">XDG_RUNTIME_DIR</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$XDG_RUNTIME_DIR</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">DISPLAY_MOUNTS</span><span class="o">+=(</span><span class="s2">&#34;--bind&#34;</span> <span class="s2">&#34;</span><span class="nv">$XDG_RUNTIME_DIR</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$XDG_RUNTIME_DIR</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">DISPLAY_ENV</span><span class="o">+=(</span><span class="s2">&#34;--setenv&#34;</span> <span class="s2">&#34;XDG_RUNTIME_DIR&#34;</span> <span class="s2">&#34;</span><span class="nv">$XDG_RUNTIME_DIR</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WAYLAND_DISPLAY</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">DISPLAY_ENV</span><span class="o">+=(</span><span class="s2">&#34;--setenv&#34;</span> <span class="s2">&#34;WAYLAND_DISPLAY&#34;</span> <span class="s2">&#34;</span><span class="nv">$WAYLAND_DISPLAY</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Assemble and launch ───────────────────────────────────────</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Jail Active: </span><span class="nv">$PROJECT_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">bwrap <span class="se">\
</span></span></span><span class="line"><span class="cl">  --ro-bind /usr /usr <span class="se">\
</span></span></span><span class="line"><span class="cl">  --symlink usr/bin /bin <span class="se">\
</span></span></span><span class="line"><span class="cl">  --symlink usr/lib /lib <span class="se">\
</span></span></span><span class="line"><span class="cl">  --symlink usr/lib /lib64 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --ro-bind /etc /etc <span class="se">\
</span></span></span><span class="line"><span class="cl">  --ro-bind <span class="s2">&#34;</span><span class="nv">$TEMP_HOSTS</span><span class="s2">&#34;</span> /etc/hosts <span class="se">\
</span></span></span><span class="line"><span class="cl">  --ro-bind /opt /opt <span class="se">\
</span></span></span><span class="line"><span class="cl">  --ro-bind /sys /sys <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dev /dev <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">GPU_MOUNTS</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SHM_MOUNT</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --proc /proc <span class="se">\
</span></span></span><span class="line"><span class="cl">  --tmpfs /tmp <span class="se">\
</span></span></span><span class="line"><span class="cl">  --tmpfs /run <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOCKER_MOUNT</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DISPLAY_MOUNTS</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --tmpfs <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOTFILE_MOUNTS</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">CONFIG_HIDE_MOUNTS</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">CACHE_HIDE_MOUNTS</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">LOCAL_OVERRIDES</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">EXTRA_MOUNTS</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --bind <span class="s2">&#34;</span><span class="nv">$PROJECT_DIR</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$PROJECT_DIR</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --chdir <span class="s2">&#34;</span><span class="nv">$PROJECT_DIR</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --die-with-parent <span class="se">\
</span></span></span><span class="line"><span class="cl">  --unshare-pid <span class="se">\
</span></span></span><span class="line"><span class="cl">  --unshare-uts <span class="se">\
</span></span></span><span class="line"><span class="cl">  --unshare-ipc <span class="se">\
</span></span></span><span class="line"><span class="cl">  --hostname <span class="s2">&#34;ai-sandbox&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DISPLAY_ENV</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --setenv PS1 <span class="s2">&#34;(jail) \w \$ &#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --setenv _ZO_DOCTOR <span class="m">0</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  bash -c <span class="s2">&#34;</span><span class="nv">$MISE_INIT</span><span class="s2"> &amp;&amp; </span><span class="si">${</span><span class="p">*</span><span class="k">:-</span><span class="nv">bash</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This version is heavily customized. Read it through carefully because it has a deny-list so that only the directories you actually need inside the Jail are available. Some are read-write because Claude needs to be able to write to <code>~/.claude</code> or Codex needs <code>~/.codex</code>; several directories should never be inside the Jail like a Brave password store or BitWarden. Everything else you might need should be read-only. This script handles all of that.</p>
<p>Just <code>chmod +x ~/.local/bin/ai-jail</code>. Now you can run it directly or pass the command you want to run inside the sandbox:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>~/.local/bin/ai-jail crush</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In this version it&rsquo;ll create a temporary <code>$HOME</code> directory in case it needs to write something temporary there. And I also set it up to map your <code>~/.config</code> so the agents have access to your settings. If you need more stuff like environment variables and the like, modify it to map those too — but this is the bare minimum to put together a safe sandbox.</p>
<p>Be very careful with programs that run random commands on your system. Nothing is 100% safe, especially an LLM that can hallucinate at any moment and decide to delete things, or shady tools you downloaded without being sure of where they came from that might be malware in disguise. Always run truly suspicious things only in a VM, or at the very least, inside a jail like this one.</p>
<p>This isn&rsquo;t just for AI agents, obviously. Any time you&rsquo;re about to run any command you&rsquo;re not sure about or that looks fishy, run it inside this jail.</p>
<p>As a bonus, note that I also map <a href="https://akitaonrails.com/en/2025/09/07/omarchy-2-0-mise-for-organizing-dev-environments/"target="_blank" rel="noopener">Mise</a> into the container. If you&rsquo;re on Omarchy, you should be using Mise too.</p>
<p>Specifically for Claude Code, which is what I use the most, I don&rsquo;t like to leave everything authorized in automatic mode with <code>--allow-dangerously-skip-permissions</code>, but I also don&rsquo;t like having to keep authorizing dumb stuff like every single domain it wants to search the web for (just searching is no problem at all) or any command that just lists things like <code>ls</code> or <code>grep</code>. So you can improve things by configuring directly in <code>~/.claude/settings.json</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;permissions&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;allow&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git add *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git commit *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git status *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git diff *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git log *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git branch *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git checkout *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git switch *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git stash *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git show *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git fetch *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git pull *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git merge *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git tag *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git remote -v*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git rev-parse *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git ls-files *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(ls *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(find *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(grep *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(wc *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(cat *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(head *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(tail *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(file *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(which *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(echo *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pwd)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(cd *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(ps *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(env *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(printenv *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(mkdir *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(cp *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(mv *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(touch *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(npm *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(npx *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(yarn *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pnpm *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(bun *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(node *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(ruby *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(bundle *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(rails *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(rake *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(go *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(cargo *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(rustc *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(python *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pip *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker compose *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker ps *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker logs *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker images *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(make *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(gh *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(curl *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(jq *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(sed *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(awk *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(sort *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(uniq *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(diff *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(* --version)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(* --help)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;WebSearch&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;WebFetch&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;deny&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(rm -rf *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(rm -r *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(sudo *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(chmod 777 *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git reset --hard *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git clean *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git push --force *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git push -f *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker rm *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker rmi *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker system prune *)&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;ask&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git push *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git rebase *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git branch -D *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(git branch -d *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(rm *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(kamal *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker run *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker exec *)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(docker stop *)&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;defaultMode&#34;</span><span class="p">:</span> <span class="s2">&#34;acceptEdits&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;enabledPlugins&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;gopls-lsp@claude-plugins-official&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;typescript-lsp@claude-plugins-official&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;pyright-lsp@claude-plugins-official&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;rust-analyzer-lsp@claude-plugins-official&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;github@claude-plugins-official&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;pr-review-toolkit@claude-plugins-official&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;frontend-design@claude-plugins-official&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;alwaysThinkingEnabled&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Look up the documentation (or just ask Claude itself) about more customizations you can do in this file to better fit your project type or workflow.</p>
<p>This is basically how a Docker or Podman container works too. I explained that in my <a href="https://akitaonrails.com/2023/03/02/akitando-139-entendendo-como-containers-funcionam/"target="_blank" rel="noopener">video about Docker</a>. If you haven&rsquo;t watched it yet, I recommend it.</p>
]]></content:encoded><category>ai-jail</category><category>coding-agents</category><category>security</category><category>linux</category></item></channel></rss>