<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Focus on your mission, not your tech - Another Cup of Coffee (Posts about Security)</title><link>https://anothercoffee.net/</link><description></description><atom:link href="https://anothercoffee.net/categories/security.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Copyright © 2006 - 2026 &lt;a href="https://anothercoffee.net/" title="Another Cup of Coffee Limited"&gt;Another Cup of Coffee Limited&lt;/a&gt; </copyright><lastBuildDate>Fri, 27 Mar 2026 00:20:52 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>The Practical Guide to Locking Down Claude Code</title><link>https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/</link><dc:creator>Anthony Lopez-Vito</dc:creator><description>&lt;figure&gt;&lt;img src="https://anothercoffee.net/images/posts/locked-down-claude-code-og-1200x630.jpg"&gt;&lt;/figure&gt; &lt;p class="intro"&gt;We use Claude Code across dozens of projects at Another Cup of Coffee. It's genuinely changed how we work but these tools run as your user account with access to your entire home directory. Trusting them with full autonomy is a mistake. This guide covers the layered configuration I built to lock mine down.&lt;/p&gt;

&lt;p&gt;I wrote recently about &lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/"&gt;what's standing between AI coding tools and your SSH keys&lt;/a&gt;. That piece covered the threats, the security layers, and a checklist of things you should be doing. This is the practical follow-up where I walk through the security configuration I've built for Claude Code, and include examples that might be useful for your own setup. It's Claude Code-specific, but the principles apply to any AI tool that runs commands as your user account.&lt;/p&gt;
&lt;p&gt;You can read the &lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/"&gt;companion article&lt;/a&gt; for a background on why these protections matter, but if you want to set them up, this guide will help.&lt;/p&gt;
&lt;div class="p-4 border bg-light mb-4"&gt;
&lt;p class="mb-0"&gt;&lt;i class="fa fa-exclamation-triangle fa-lg" aria-hidden="true" style="color: #e6a23c;"&gt;&lt;/i&gt; &lt;strong&gt;A word of caution.&lt;/strong&gt; This is experimental work. The security tooling for AI coding agents is still immature and the configurations below are what's working for me right now, not a finished product. Test everything in your own environment before relying on it. If you find issues or improvements, I'd genuinely like to &lt;a href="https://anothercoffee.net/contact/"&gt;hear about them&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;h2 id="table-of-contents"&gt;Table of Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#table-of-contents"&gt;Table of Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#key-terms-for-claude-code-security"&gt;Key terms for Claude Code security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#the-os-layer-you-already-have"&gt;The OS layer you already have&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#what-youll-need"&gt;What you'll need&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#the-sandbox-path"&gt;The sandbox path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#why-not-the-sandbox-for-ai-agent-workflows"&gt;Why not the sandbox for AI agent workflows?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#the-design-principle-layered-claude-code-security"&gt;The design principle: layered Claude Code security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#layer-1-deny-permissions-in-settingsjson"&gt;Layer 1: deny permissions in settings.json&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#keep-credentials-out-of-context"&gt;Keep credentials out of context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#the-sudo-convention"&gt;The sudo convention&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#layer-2-the-bash-guard-hook"&gt;Layer 2: the Bash guard hook&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#why-simple-text-matching-for-the-hook-script"&gt;Why simple text matching for the hook script?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#per-project-exemptions"&gt;Per-project exemptions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#registering-the-hook"&gt;Registering the hook&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#testing-your-security-setup"&gt;Testing your security setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#whats-next-for-locking-down-claude-code"&gt;What's next for locking down Claude Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="key-terms-for-claude-code-security"&gt;Key terms for Claude Code security&lt;/h2&gt;
&lt;p&gt;Let's start by defining some key terms so we're clear about what we'll be referencing.&lt;/p&gt;
&lt;p&gt;If you're new to Claude Code, a few terms come up repeatedly and they're worth defining upfront because some of them mean different things in different contexts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Permissions&lt;/strong&gt; (Claude Code level) are rules in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; that control which tools the AI agent can use and which files it can access. These are enforced by the Claude Code application itself and to be clear, we're not talking about Unix file permissions. OS file permissions are enforced by the operating system kernel. Both matter, and they protect different things at different layers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The sandbox&lt;/strong&gt; is Claude Code's built-in OS-level isolation but it relies on operating system utilities, like bubblewrap on Linux and Seatbelt on macOS. When the sandbox is on, Bash commands run inside a restricted kernel namespace with limited filesystem and network access. The sandbox wraps Bash commands only; it doesn't affect Claude Code's own Write or Edit tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hooks&lt;/strong&gt; are scripts that Claude Code runs automatically before or after certain actions. A PreToolUse hook runs before a tool call executes, and it can block the call by returning a deny decision. This guide uses a PreToolUse hook on the Bash tool to inspect commands before they run. Hooks are your code, running on your machine, triggered by Claude Code's event system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OS-level permissions&lt;/strong&gt; are the standard Unix file permissions (owner, group, other) enforced by the Linux or macOS kernel. Claude Code runs as your user account, so it inherits whatever access your account has. This means it can read your SSH keys, your browser data, your shell config, and anything else your account owns, unless something else (sandbox, deny rules, hooks) blocks it first.&lt;/p&gt;
&lt;h2 id="the-os-layer-you-already-have"&gt;The OS layer you already have&lt;/h2&gt;
&lt;p&gt;It's important to understand what the operating system already gives you before configuring anything in Claude Code. Claude Code runs as your user account (whichever account you use to run it), which means standard Unix permissions are the first line of defence.&lt;/p&gt;
&lt;p&gt;I run Claude Code under a dedicated user account, separate from my day-to-day login. This gives you real kernel-enforced isolation: the agent can't read your personal documents, SSH keys, or application configurations because those files belong to a different user. Unix permissions won't let it cross accounts and it's the strongest single thing you can do. The rest of this guide applies whether you do this or not. If you run Claude Code as your own user, the deny rules and hooks described below are doing more of the heavy lifting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What this protects:&lt;/strong&gt; System files owned by root are already protected from modification by your user account. Your agent can't write to &lt;code&gt;/etc/passwd&lt;/code&gt; or replace &lt;code&gt;/usr/bin/ssh&lt;/code&gt; because your account doesn't have write permission to those locations. Some system files are also protected from reading: &lt;code&gt;/etc/shadow&lt;/code&gt; on Linux (which stores password hashes) is typically mode 000 or 640, so a non-root process can't read it at all. But most system files (like &lt;code&gt;/etc/passwd&lt;/code&gt;, everything in &lt;code&gt;/usr/bin/&lt;/code&gt;) are world-readable, just not world-writable. This is the kernel enforcing access control, and no amount of prompt injection or agent reasoning can bypass it. (macOS doesn't use &lt;code&gt;/etc/shadow&lt;/code&gt;. Instead it stores credentials in a separate system database that's similarly protected.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What this doesn't protect:&lt;/strong&gt; Everything your user account owns. That includes &lt;code&gt;~/.ssh/&lt;/code&gt; (your private keys), &lt;code&gt;~/.gnupg/&lt;/code&gt; (your GPG keyring), &lt;code&gt;~/.aws/credentials&lt;/code&gt;, &lt;code&gt;~/.bashrc&lt;/code&gt;, your browser profile, your email, and everything in your home directory. From the kernel's perspective, Claude Code reading your SSH private key is identical to you reading it yourself, because it's running as you.&lt;/p&gt;
&lt;p&gt;If you're running Claude Code as your own user, you can tighten things on Linux by setting restrictive permissions on sensitive directories: &lt;code&gt;chmod 700 ~/.ssh ~/.gnupg ~/.password-store&lt;/code&gt; ensures only your user can access them. Claude Code still can (it's running as you, remember) but it limits exposure from other accounts on the machine.&lt;/p&gt;
&lt;p&gt;On macOS, the same principles apply, with the addition of TCC (Transparency, Consent, and Control). macOS protects certain directories (Desktop, Documents, Downloads) behind a consent system. The first time a process tries to access one of these, macOS shows a prompt, but it's attributed to the terminal emulator (Terminal.app or iTerm2), not to the child process. Once you grant your terminal access to a protected folder (or grant it Full Disk Access), every process it spawns, including Claude Code and its scripts, inherits that access silently. If your terminal already has Full Disk Access, TCC won't provide any additional protection. If it doesn't, consider whether it needs it because granting FDA to your terminal is the same as granting it to every CLI tool you run.&lt;/p&gt;
&lt;p&gt;The rest of this guide builds on top of this OS layer. The Claude Code permissions, sandbox, and hooks are all additional controls that restrict what the agent can do within the access your user account already has.&lt;/p&gt;
&lt;h2 id="what-youll-need"&gt;What you'll need&lt;/h2&gt;
&lt;p&gt;This guide assumes you're running Claude Code on Linux or macOS, and the deny permissions and hook registration work the same on both platforms. The hook script needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python 3&lt;/strong&gt; (the hook shells out to Python for JSON parsing). On Linux, this is almost certainly already installed. On macOS, install via Xcode Command Line Tools (&lt;code&gt;xcode-select --install&lt;/code&gt;) or Homebrew.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bash&lt;/strong&gt; (the hook wrapper uses bash, not sh). It ships with both Linux and macOS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;syslog access&lt;/strong&gt; for audit logging. On Linux, logs go to the system journal and you can read them with &lt;code&gt;journalctl&lt;/code&gt;. On macOS, the Python &lt;code&gt;syslog&lt;/code&gt; module writes to the unified log, which you can check with &lt;code&gt;log show --predicate 'process == "python3"' --last 5m&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to use the sandbox instead of, or alongside, the hook approach, you'll also need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux:&lt;/strong&gt; bubblewrap&lt;sup&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#fn2"&gt;2&lt;/a&gt;&lt;/sup&gt; and socat. On Arch: &lt;code&gt;pacman -S bubblewrap socat&lt;/code&gt;. On Debian/Ubuntu: &lt;code&gt;apt install bubblewrap socat&lt;/code&gt;. Your kernel needs unprivileged user namespaces enabled, which most modern distros have by default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Seatbelt is built into the OS. No additional packages needed. socat is not required on macOS because Claude Code uses Seatbelt's native network filtering.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-sandbox-path"&gt;The sandbox path&lt;/h2&gt;
&lt;p&gt;Before I get into the hook-based approach, I should be clear that if your workflow doesn't involve SSH, rsync, or deleting files outside your project directory, the sandbox is the better option. It's a single configuration block that's kernel-enforced, and it handles both filesystem and network isolation.&lt;/p&gt;
&lt;p&gt;This is an example sandbox configuration for &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. Adjust the paths and domains to match your setup:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"sandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"autoAllowBashIfSandboxed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"allowUnsandboxedCommands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"allowWrite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"~/Projects"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"//tmp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"denyWrite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"//etc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"//usr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"//boot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"~/.bashrc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.zshrc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.bash_profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/settings.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.msmtprc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.mbsyncrc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"~/.gnupg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.ssh"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"denyRead"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"~/.ssh/id_*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.ssh/*_rsa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"~/.gnupg/private-keys-v1.d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"~/.password-store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.aws/credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.kube/config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"~/.bash_history"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.zsh_history"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"allowedDomains"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"github.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"raw.githubusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"objects.githubusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"registry.npmjs.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pypi.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"files.pythonhosted.org"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here are the key settings.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;enabled: true&lt;/code&gt; turns on bubblewrap/Seatbelt isolation. &lt;code&gt;autoAllowBashIfSandboxed: true&lt;/code&gt; means Bash commands inside the sandbox run without asking for approval. The sandbox is doing the enforcement, so the approval prompt is redundant. &lt;code&gt;allowUnsandboxedCommands: false&lt;/code&gt; disables the escape hatch that lets agents retry failed commands without sandboxing. That last one matters because a command failing inside the sandbox means the sandbox is doing its job. The agent shouldn't be able to turn it off.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;denyRead&lt;/code&gt; on SSH keys does not break SSH. ssh-agent handles authentication through a Unix socket, not by reading key files directly. Your agent can still &lt;code&gt;ssh&lt;/code&gt; into things if the sandbox allows network access to that host. It just can't read the private key file itself.&lt;/p&gt;
&lt;p&gt;Add project-specific domains to &lt;code&gt;allowedDomains&lt;/code&gt; as you need them. For example, if a project needs to reach an API or a staging server over HTTPS, you should add that domain. The network allowlist only affects sandboxed Bash commands, and WebFetch and WebSearch are controlled separately through the permissions layer.&lt;/p&gt;
&lt;p&gt;You can still layer on extra deny permissions and hooks on top of the sandbox because they don't conflict. In fact, the extra layers catch anything the sandbox doesn't cover, like a malicious Write tool call, which the sandbox doesn't see because it only wraps Bash. Personally, I'd recommend the deny permissions from Layer 1 below even if you're using the sandbox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rest of this article is for when the sandbox doesn't fit.&lt;/strong&gt; If you need SSH, rsync, raw TCP, or file deletion outside your project directory, read on because the sandbox is going to make things unworkable for you.&lt;/p&gt;
&lt;h2 id="why-not-the-sandbox-for-ai-agent-workflows"&gt;Why not the sandbox for AI agent workflows?&lt;/h2&gt;
&lt;p&gt;A sandbox is a good choice for a lot of workflows like editing code from someone's git repo. You should use it if it works for yours because it's a simpler setup than what I'm about to describe.&lt;/p&gt;
&lt;p&gt;A lot of my work involves &lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/"&gt;running AI agents that manage remote servers&lt;/a&gt;. This requires logging in via SSH, rsync-ing files between machines, and deleting temporary artifacts. The sandbox is too restrictive because it blocks everything that makes these agents useful. For example:
- TCP networking is completely blocked so SSH doesn't work;
- file deletion is blocked even on paths you've explicitly whitelisted so files build up;
- and there's no way to allow SSH to one host while blocking another.&lt;/p&gt;
&lt;p&gt;I ended up having to replace the sandbox with two layers that offer the necessary protections.&lt;/p&gt;
&lt;h2 id="the-design-principle-layered-claude-code-security"&gt;The design principle: layered Claude Code security&lt;/h2&gt;
&lt;p&gt;The approach goes like this:&lt;/p&gt;
&lt;table class="table table-bordered mt-4 mb-4"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool&lt;/th&gt;&lt;th&gt;Protection&lt;/th&gt;&lt;th&gt;Mechanism&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Write / Edit&lt;/td&gt;&lt;td&gt;Deny permissions in settings.json&lt;/td&gt;&lt;td&gt;Built-in, zero code (see caveat below)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Read&lt;/td&gt;&lt;td&gt;Allow-list scoped to ~/Projects/**&lt;/td&gt;&lt;td&gt;Built-in scoping&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Bash&lt;/td&gt;&lt;td&gt;PreToolUse hook (substring match)&lt;/td&gt;&lt;td&gt;Heuristic, fail-closed, honest about its limits&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;These are all Claude Code-level controls, sitting on top of the OS-level protections described earlier. The OS layer, like Unix file permissions, protects system files from modification by your user account. The Claude Code layers protect everything your user account &lt;em&gt;can&lt;/em&gt; access but the agent &lt;em&gt;shouldn't&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The idea is that you don't use hooks where permissions work. Claude Code's deny permissions are enforced by the application itself. Hooks, on the other hand, are for Bash because the permission system can check the tool name but can't inspect what's inside the command.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;The agent shouldn't be able to turn off the thing that's restricting it.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;h2 id="layer-1-deny-permissions-in-settingsjson"&gt;Layer 1: deny permissions in settings.json&lt;/h2&gt;
&lt;p&gt;Claude Code has a permission system&lt;sup&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#fn1"&gt;1&lt;/a&gt;&lt;/sup&gt; with allow, deny, and ask rules. The important thing about deny rules is that they're evaluated first and nothing can get around them. This means that if you deny something at the global level (&lt;code&gt;~/.claude/settings.json&lt;/code&gt;), no project-level configuration can override it. This is enforced by Claude Code itself, not by the operating system.&lt;/p&gt;
&lt;p&gt;See below for an example deny block. Make sure you replace &lt;code&gt;/home/youruser&lt;/code&gt; with your actual home directory, and adjust the protected paths to match what's on your system:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(msmtp *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(sendmail *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(mail *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//etc/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//usr/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//boot/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//sbin/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//lib/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.bashrc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.zshrc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.profile)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.bash_profile)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.msmtprc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.mbsyncrc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.gnupg/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.ssh/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Write(//home/youruser/.claude/settings.json)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//etc/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//usr/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//boot/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//sbin/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//lib/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.bashrc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.zshrc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.profile)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.bash_profile)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.msmtprc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.mbsyncrc)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.gnupg/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.ssh/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Edit(//home/youruser/.claude/settings.json)"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You'll notice every Write and Edit rule is duplicated. This is important because they're actually different tools, so an agent could use either to modify a file. Both need blocking.&lt;/p&gt;
&lt;p&gt;What's protected, and why:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System directories&lt;/strong&gt; (&lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/usr&lt;/code&gt;, &lt;code&gt;/boot&lt;/code&gt;, &lt;code&gt;/sbin&lt;/code&gt;, &lt;code&gt;/lib&lt;/code&gt;) cover OS configuration, installed packages, bootloader, and system binaries. An agent shouldn't write to any of these, unless it's one specifically deployed for administering the environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shell configs&lt;/strong&gt; (&lt;code&gt;.bashrc&lt;/code&gt;, &lt;code&gt;.zshrc&lt;/code&gt;, &lt;code&gt;.profile&lt;/code&gt;, &lt;code&gt;.bash_profile&lt;/code&gt;) are needed because if an agent writes something malicious here, it persists after the session ends.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSH and GPG directories&lt;/strong&gt; (&lt;code&gt;.ssh&lt;/code&gt;, &lt;code&gt;.gnupg&lt;/code&gt;) protect your keys and trust chain. SSH still works fine for the agent via ssh-agent as it doesn't need direct file access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mail configs&lt;/strong&gt; (&lt;code&gt;.msmtprc&lt;/code&gt;, &lt;code&gt;.mbsyncrc&lt;/code&gt;) because these contain SMTP credentials and mail server access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code's own settings&lt;/strong&gt; (&lt;code&gt;.claude/settings.json&lt;/code&gt;) because if an agent can modify its own permission rules, a prompt injection could disable every other protection in this list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;A caveat on reliability.&lt;/strong&gt; In theory, deny permissions are the strongest layer but there have been bugs where deny rules for Read, Write, and Edit were silently ignored.&lt;sup&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#fn3"&gt;3&lt;/a&gt;&lt;/sup&gt; This is exactly why this guide layers hooks on top of permissions. The deny rules should work, but if they don't, the hook hopefully catches it. While you can't anticipate every problem, a defence in depth mindset protects you from most things.&lt;/p&gt;
&lt;p&gt;The allow list is the other side of this setup. You can scope Read access to a specific directory (like &lt;code&gt;~/Projects/**&lt;/code&gt;) so agents can't browse the rest of your home directory, and pre-approve common read-only Bash commands (e.g. &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;diff&lt;/code&gt;) to reduce approval fatigue. I say reduce because approval fatigue is still a thing, unfortunately. The practical trade-off is to auto-allow the safe stuff so you're not clicking "yes" a hundred times a day...maybe just 99. Experienced Claude users know what I mean.&lt;/p&gt;
&lt;p&gt;Here's an example allow block showing the pattern:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Read(//home/youruser/Projects/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(ls:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git -C /:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(grep:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(cat:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(diff:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(cp:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(mkdir:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"WebSearch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"WebFetch"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You'll notice &lt;code&gt;cat&lt;/code&gt; and &lt;code&gt;cp&lt;/code&gt; are auto-approved here even though they could technically read or copy sensitive files.&lt;sup&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#known-limitations"&gt;*&lt;/a&gt;&lt;/sup&gt; This is where the layers work together: the hook script in Layer 2 catches any &lt;code&gt;cat&lt;/code&gt; or &lt;code&gt;cp&lt;/code&gt; command that references a protected path like &lt;code&gt;~/.ssh/&lt;/code&gt;, even if the permission system has already auto-approved it. The allow rule lets the command skip the approval prompt, but the hook still inspects it.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Bash(git -C /:*)&lt;/code&gt; pattern is worth explaining because I came across commit approval problems early on when using git with my agents. The &lt;code&gt;-C&lt;/code&gt; flag tells git to operate on a specific directory, so &lt;code&gt;git -C /home/youruser/Projects/myproject status&lt;/code&gt; works without the agent needing to &lt;code&gt;cd&lt;/code&gt; anywhere. Pre-approving this pattern means git commands run without prompting the user, but only when they include an explicit path. &lt;code&gt;Bash(git:*)&lt;/code&gt; without the &lt;code&gt;-C&lt;/code&gt; would also match bare &lt;code&gt;git&lt;/code&gt; commands, making it run on whatever directory the agent happens to be in. This means git could act on a repo you didn't intend so the &lt;code&gt;-C&lt;/code&gt; pattern forces it to be explicit.&lt;/p&gt;
&lt;h3 id="keep-credentials-out-of-context"&gt;Keep credentials out of context&lt;/h3&gt;
&lt;p&gt;There's also a broader principle we need to keep in mind. Credentials should almost never appear in your agent's conversation context because if a secret shows up in a tool output or a file the agent reads, it's in the context window. From there it could end up somewhere else, like a log or a commit message.&lt;/p&gt;
&lt;p&gt;The solution for SSH is straightforward. Just use ssh-agent. The agent runs &lt;code&gt;ssh myserver&lt;/code&gt; and ssh-agent handles authentication through a Unix socket. The private key never enters Claude Code's context. This makes your deny rules on &lt;code&gt;~/.ssh/&lt;/code&gt; the backup, while ssh-agent is the primary protection.&lt;/p&gt;
&lt;p&gt;For database credentials, you can store them server-side in standard locations. For example, MySQL/MariaDB reads from &lt;code&gt;~/.my.cnf&lt;/code&gt; on the server and PostgreSQL reads from &lt;code&gt;~/.pgpass&lt;/code&gt;. The agent runs &lt;code&gt;ssh myserver "mysql -e 'SHOW DATABASES'"&lt;/code&gt; and the credentials are loaded by the server rather than by the agent.&lt;/p&gt;
&lt;p&gt;For API keys and cloud credentials, it helps to have environment variables loaded from a protected file, like &lt;code&gt;pass&lt;/code&gt; with GPG, or a &lt;code&gt;.env&lt;/code&gt; file in a directory the agent can't read. The agent can then call the API through a wrapper script that sources the credentials at runtime.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;If a secret shows up in a tool output or a file the agent reads, it's in the context window. From there it could end up somewhere else.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;p&gt;In essence, wherever possible, you keep credentials somewhere outside of the agent's reach and instead, have the agent invoke a command that uses credentials indirectly. If you want to understand why this matters beyond convenience, the &lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/"&gt;OpenClaw incident&lt;/a&gt; is a good case study in what happens when credentials end up where they shouldn't.&lt;/p&gt;
&lt;h3 id="the-sudo-convention"&gt;The sudo convention&lt;/h3&gt;
&lt;p&gt;There's one more convention-level control I can cover. AI coding tools can't use &lt;code&gt;sudo&lt;/code&gt; interactively (unless you've configured passwordless sudo) because there's no terminal for password entry. That's a great natural protection, but an agent will indeed try, and keep trying. If you're on Linux, it will eventually trigger &lt;code&gt;pam_faillock&lt;/code&gt;, and the repeated authentication failures end up locking you out of sudo until the timeout expires. I learned this the hard way when an agent silently got into a retry loop, and eventually locked me out of my own machine at a very inconvenient time.&lt;/p&gt;
&lt;p&gt;The fix is an instruction in each agent's configuration file. Something like: "Never run &lt;code&gt;sudo&lt;/code&gt; via the Bash tool. Instead, give the user the command to run themselves." Most experienced AI agent users will know "Never run..." is never really never. But that's the best you can do for now.&lt;/p&gt;
&lt;p&gt;It's not so bad on macOS because repeated failed &lt;code&gt;sudo&lt;/code&gt; attempts won't lock the account unless you've configured it to do so. However, an agent will waste time retrying and end up flooding the security log.&lt;/p&gt;
&lt;h2 id="layer-2-the-bash-guard-hook"&gt;Layer 2: the Bash guard hook&lt;/h2&gt;
&lt;div class="p-4 border bg-light mb-4"&gt;
&lt;p class="mb-0"&gt;&lt;i class="fa fa-exclamation-triangle fa-lg" aria-hidden="true" style="color: #e6a23c;"&gt;&lt;/i&gt; &lt;strong&gt;This hook is a deterrent, not a security boundary.&lt;/strong&gt; It catches common commands that reference protected paths but it can be bypassed by an agent that constructs paths indirectly. It's one layer in a defence-in-depth setup, not a standalone protection.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Deny permissions can block &lt;code&gt;Write(//etc/**)&lt;/code&gt;, but they can't block &lt;code&gt;Bash(cat /etc/shadow)&lt;/code&gt;. The permission system checks which tool is being called and (for Write/Edit) which file path is targeted. It doesn't inspect the contents of a Bash command though, so that's why we need the hook script.&lt;/p&gt;
&lt;p&gt;Here's an example:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c1"&gt;# PreToolUse hook: Guard sensitive system paths and secrets from Bash commands.&lt;/span&gt;
&lt;span class="c1"&gt;# Write/Edit protection is handled by deny permissions in ~/.claude/settings.json.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Input: JSON on stdin with .tool_input.command field&lt;/span&gt;
&lt;span class="c1"&gt;# Output: JSON with "deny" decision, or empty output to allow&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Approach: check if the command string contains any protected path.&lt;/span&gt;
&lt;span class="c1"&gt;# This is intentionally simple — a substring match is more reliable than&lt;/span&gt;
&lt;span class="c1"&gt;# trying to parse shell syntax. False positives are rare in practice&lt;/span&gt;
&lt;span class="c1"&gt;# (legitimate commands rarely reference system paths or secret files).&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Limitations:&lt;/span&gt;
&lt;span class="c1"&gt;#   - Cannot detect paths constructed at runtime via variable expansion&lt;/span&gt;
&lt;span class="c1"&gt;#   - Cannot detect symlinks pointing to protected paths&lt;/span&gt;
&lt;span class="c1"&gt;#   - Will false-positive if a protected path appears as a string literal&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-euo&lt;span class="w"&gt; &lt;/span&gt;pipefail&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# see known limitations&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"python3 not found — blocking as precaution"}}'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nv"&gt;HOOK_INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1048576&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOOK_INPUT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-ge&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1048576&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Input too large — blocking as precaution"}}'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;HOOK_INPUT

&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;'PYTHON_EOF'&lt;/span&gt;
&lt;span class="s"&gt;import json, sys, os, re, syslog, signal&lt;/span&gt;

&lt;span class="s"&gt;signal.signal(signal.SIGALRM, lambda *_: deny("Command analysis timeout — blocking as precaution."))&lt;/span&gt;
&lt;span class="s"&gt;signal.alarm(5)&lt;/span&gt;

&lt;span class="s"&gt;HOME = os.path.expanduser("~")&lt;/span&gt;
&lt;span class="s"&gt;command = ""&lt;/span&gt;

&lt;span class="s"&gt;def deny(reason):&lt;/span&gt;
&lt;span class="s"&gt;    try:&lt;/span&gt;
&lt;span class="s"&gt;        syslog.syslog(syslog.LOG_WARNING, f"PATH_GUARD_BLOCK: {reason} | Command: {command[:200]}")&lt;/span&gt;
&lt;span class="s"&gt;    except Exception:&lt;/span&gt;
&lt;span class="s"&gt;        pass&lt;/span&gt;
&lt;span class="s"&gt;    print(json.dumps({&lt;/span&gt;
&lt;span class="s"&gt;        "hookSpecificOutput": {&lt;/span&gt;
&lt;span class="s"&gt;            "hookEventName": "PreToolUse",&lt;/span&gt;
&lt;span class="s"&gt;            "permissionDecision": "deny",&lt;/span&gt;
&lt;span class="s"&gt;            "permissionDecisionReason": reason,&lt;/span&gt;
&lt;span class="s"&gt;        }&lt;/span&gt;
&lt;span class="s"&gt;    }))&lt;/span&gt;
&lt;span class="s"&gt;    sys.exit(0)&lt;/span&gt;

&lt;span class="s"&gt;# Protected paths — if any of these appear in the command, block it.&lt;/span&gt;
&lt;span class="s"&gt;# This list is not exhaustive — see known limitations.&lt;/span&gt;
&lt;span class="s"&gt;PROTECTED = [&lt;/span&gt;
&lt;span class="s"&gt;    "/etc/",&lt;/span&gt;
&lt;span class="s"&gt;    "/usr/",&lt;/span&gt;
&lt;span class="s"&gt;    "/boot/",&lt;/span&gt;
&lt;span class="s"&gt;    "/sbin/",&lt;/span&gt;
&lt;span class="s"&gt;    "/lib/",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.bashrc",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.zshrc",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.profile",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.bash_profile",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.claude/settings.json",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.msmtprc",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.mbsyncrc",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.gnupg/",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.ssh/",&lt;/span&gt;
&lt;span class="s"&gt;    # Secrets / credentials:&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.password-store/",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.aws/credentials",&lt;/span&gt;
&lt;span class="s"&gt;    HOME + "/.kube/config",&lt;/span&gt;
&lt;span class="s"&gt;]&lt;/span&gt;

&lt;span class="s"&gt;# Only these paths may be exempted by project-level config.&lt;/span&gt;
&lt;span class="s"&gt;# Home-directory paths can never be exempted.&lt;/span&gt;
&lt;span class="s"&gt;EXEMPTABLE = {"/etc/", "/usr/", "/boot/", "/sbin/", "/lib/"}&lt;/span&gt;

&lt;span class="s"&gt;# Load project-level exemptions from .claude/guard-exempt-paths.json&lt;/span&gt;
&lt;span class="s"&gt;project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")&lt;/span&gt;
&lt;span class="s"&gt;if project_dir:&lt;/span&gt;
&lt;span class="s"&gt;    exempt_file = os.path.join(project_dir, ".claude", "guard-exempt-paths.json")&lt;/span&gt;
&lt;span class="s"&gt;    try:&lt;/span&gt;
&lt;span class="s"&gt;        with open(exempt_file) as f:&lt;/span&gt;
&lt;span class="s"&gt;            requested = set(json.load(f))&lt;/span&gt;
&lt;span class="s"&gt;        exemptions = requested &amp;amp; EXEMPTABLE&lt;/span&gt;
&lt;span class="s"&gt;        if exemptions:&lt;/span&gt;
&lt;span class="s"&gt;            PROTECTED = [p for p in PROTECTED if p not in exemptions]&lt;/span&gt;
&lt;span class="s"&gt;            syslog.syslog(syslog.LOG_INFO,&lt;/span&gt;
&lt;span class="s"&gt;                f"PATH_GUARD_EXEMPT: {sorted(exemptions)} for {project_dir}")&lt;/span&gt;
&lt;span class="s"&gt;    except (FileNotFoundError, json.JSONDecodeError, TypeError):&lt;/span&gt;
&lt;span class="s"&gt;        pass&lt;/span&gt;

&lt;span class="s"&gt;# Also match ~/. shorthand versions&lt;/span&gt;
&lt;span class="s"&gt;TILDE_PROTECTED = ["~/" + p[len(HOME)+1:] for p in PROTECTED if p.startswith(HOME + "/")]&lt;/span&gt;

&lt;span class="s"&gt;# Parse input&lt;/span&gt;
&lt;span class="s"&gt;raw_input = os.environ.get("HOOK_INPUT", "")&lt;/span&gt;
&lt;span class="s"&gt;try:&lt;/span&gt;
&lt;span class="s"&gt;    data = json.loads(raw_input)&lt;/span&gt;
&lt;span class="s"&gt;    command = data.get("tool_input", {}).get("command", "")&lt;/span&gt;
&lt;span class="s"&gt;except Exception:&lt;/span&gt;
&lt;span class="s"&gt;    deny("Path-guard hook could not parse input — blocking as precaution.")&lt;/span&gt;

&lt;span class="s"&gt;if not command:&lt;/span&gt;
&lt;span class="s"&gt;    sys.exit(0)&lt;/span&gt;

&lt;span class="s"&gt;# Check for protected paths in the command&lt;/span&gt;
&lt;span class="s"&gt;for path in PROTECTED + TILDE_PROTECTED:&lt;/span&gt;
&lt;span class="s"&gt;    if path in command:&lt;/span&gt;
&lt;span class="s"&gt;        deny(f"Command references protected path: {path}")&lt;/span&gt;

&lt;span class="s"&gt;# Allow&lt;/span&gt;
&lt;span class="s"&gt;sys.exit(0)&lt;/span&gt;
&lt;span class="s"&gt;PYTHON_EOF&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Of course, you need to save it somewhere that makes sense for you, then make it executable with &lt;code&gt;chmod +x&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The script works as two layers, the outer being Bash and inner being Python. Bash is needed first because that's what Claude Code's hook system expects. But it's tricky to parse JSON with Bash, whereas that's Python's thing. So, Bash reads the incoming data and hands it to Python for the actual inspection.&lt;/p&gt;
&lt;h3 id="why-simple-text-matching-for-the-hook-script"&gt;Why simple text matching for the hook script?&lt;/h3&gt;
&lt;p&gt;You might wonder why the script uses simple text matching. After all, it just looks for protected paths like &lt;code&gt;/etc/&lt;/code&gt; or &lt;code&gt;~/.ssh/&lt;/code&gt; anywhere in the command text. That's obviously a blunt instrument and you may be tempted to go for a a more sophisticated approach, like separating out which parts are actual file paths, and which are arguments or text strings.&lt;/p&gt;
&lt;p&gt;It might work for you but I tried that and it was too fragile because common command formats would trip up the parser. In the end, I settled on the simpler solution because it avoids that false sense of security. Yes, it can be fooled by a malicious agent, but at least there's no pretense of defending against edge-cases. The problems you know about are easier to deal with than the unknowns.&lt;/p&gt;
&lt;h3 id="per-project-exemptions"&gt;Per-project exemptions&lt;/h3&gt;
&lt;p&gt;There's one big problem, however. I run agents that manage remote servers which regularly need to run commands that contain a protected path. For example:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ssh myserver "docker exec caddy reload --config /etc/caddy/Caddyfile"&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt; path is inside a Docker container on a remote host, not on my local machine, but the substring match catches it anyway because it sees &lt;code&gt;/etc/&lt;/code&gt; in the command text.&lt;/p&gt;
&lt;p&gt;The fix is per-project exemptions. A project that needs to reference system paths in remote commands can create a &lt;code&gt;.claude/guard-exempt-paths.json&lt;/code&gt; file in its project root:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/etc/"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Only exempt what you actually need. The full set of exemptable paths is &lt;code&gt;/etc/&lt;/code&gt;, &lt;code&gt;/usr/&lt;/code&gt;, &lt;code&gt;/boot/&lt;/code&gt;, &lt;code&gt;/sbin/&lt;/code&gt;, and &lt;code&gt;/lib/&lt;/code&gt;, but most projects only need one or two.&lt;/p&gt;
&lt;p&gt;The hook knows which project it's running in because Claude Code passes that information through an environment variable. If it finds a &lt;code&gt;guard-exempt-paths.json&lt;/code&gt; file in the project, it skips those paths when checking commands for that project.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;I'd rather have a short script that's honest about its gaps than a long one that pretends it doesn't have any.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;p&gt;The obvious risk is that a compromised project config could exempt everything and effectively disable the hook. So I built a hard limit into the script: only the five system-directory paths listed above can be exempted (you can see the &lt;code&gt;EXEMPTABLE&lt;/code&gt; list in the code). Even if someone adds &lt;code&gt;"~/.ssh/"&lt;/code&gt; to the exemption file, the script ignores it and the hook always checks for home directory paths regardless of any exemption file.&lt;/p&gt;
&lt;p&gt;This is also where the layers work together. Layer 1 (deny permissions) still blocks Write and Edit to system paths unconditionally, regardless of any project-level configuration. The exemption only affects the Bash hook's substring check so even in a project with all five system paths exempted, an agent still can't write to &lt;code&gt;/etc/&lt;/code&gt; because the deny rules won't allow it.&lt;/p&gt;
&lt;p&gt;It's important to be aware that this isn't a silver bullet because an agent could edit its own rules,&lt;sup&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#known-limitations"&gt;*&lt;/a&gt;&lt;/sup&gt; so you need to adjust permissions to match your situation. This honestly needs ongoing work as it isn't a solved problem and the tooling is still evolving. You do your best with what's available and be conscious that we don't yet have adequate solutions. Defence in depth is the key here.&lt;/p&gt;
&lt;h3 id="registering-the-hook"&gt;Registering the hook&lt;/h3&gt;
&lt;p&gt;The hook goes in your &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/path/to/guard-sensitive-paths.sh"&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You're not limited to a single hook, either. Say you have other commands you want to guard against, like outbound email or database access. You can still add separate hooks for each area. Multiple hooks on the same matcher run in parallel, and all of them must pass for the command to execute. The command is blocked if any one returns a deny decision.&lt;/p&gt;
&lt;h2 id="testing-your-security-setup"&gt;Testing your security setup&lt;/h2&gt;
&lt;p&gt;Once you've saved the settings and the hook script, test it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Try a command that references a protected path, like &lt;code&gt;cat /etc/passwd&lt;/code&gt;. It should be blocked with a message telling you which path triggered it.&lt;/li&gt;
&lt;li&gt;Check your system log for a &lt;code&gt;PATH_GUARD_BLOCK&lt;/code&gt; entry.&lt;/li&gt;
&lt;li&gt;Try a normal command like &lt;code&gt;ls -la&lt;/code&gt;. It should work fine.&lt;/li&gt;
&lt;li&gt;If you've set up a per-project exemption, try the same &lt;code&gt;/etc/&lt;/code&gt; command from that project. It should be allowed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You have two options if there's a false positive: either add the specific path to a per-project exemption if it's a system path, or adjust the PROTECTED list in the hook if the path doesn't need protecting.&lt;/p&gt;
&lt;h2 id="whats-next-for-locking-down-claude-code"&gt;What's next for locking down Claude Code&lt;/h2&gt;
&lt;p&gt;The sandbox is more secure but it wasn't workable for a lot of our projects. This setup is the trade-off I settled on: agents get the access they need for SSH, remote servers, and file management. Plus, the deny rules and hooks catch the things I don't want them touching. It's been running for a few months and it's stable.&lt;/p&gt;
&lt;p&gt;I'm still not fully comfortable with where things are because the &lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/"&gt;OpenClaw disclosures&lt;/a&gt; showed what happens when agent frameworks don't think about credential isolation. Shortly after publishing the first version of this article, a supply chain attack on LiteLLM&lt;sup&gt;&lt;a href="https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/#fn4"&gt;4&lt;/a&gt;&lt;/sup&gt; deployed a credential harvester that swept SSH keys, cloud secrets, and API tokens from every environment it touched. It was live on PyPI for three hours.&lt;/p&gt;
&lt;p&gt;These are real incidents affecting production systems because people jumped on the bandwagon too early without thinking of the implications.&lt;/p&gt;
&lt;p&gt;I've been running Another Cup of Coffee for over twenty years and if there's one thing I've learned, it's that you don't adopt experimental technology without understanding where it risks your business. Our clients trust us with their infrastructure and their data. While we have the luxury of being able to move quickly, we're also careful not to break things for the people who trust us with their livelihoods. So we build what protections we can, we're honest about the gaps, and we keep watching. I'll write more as the tooling evolves.&lt;/p&gt;
&lt;p&gt;If you're not sure where you stand with any of this, I'm happy to &lt;a href="https://anothercoffee.net/contact/"&gt;have a conversation&lt;/a&gt; about it.&lt;/p&gt;
&lt;section class="mt-4 pt-4"&gt;
&lt;h3 class="text-center pb-4"&gt;Common Questions&lt;/h3&gt;
&lt;div class="container border bg-light p-4"&gt;
&lt;p&gt;&lt;strong&gt;Can I use this alongside the sandbox?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes. The deny permissions and hooks work whether the sandbox is on or off. If your workflow doesn't need SSH or file deletion, you could run the sandbox for its kernel-level isolation and still add deny permissions and hooks as additional layers. They don't conflict.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does this work on macOS?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes. The deny permissions and hook registration are identical on macOS. The hook script needs Python 3 and bash. macOS ships bash but not Python 3; you'll need to install Python via Xcode Command Line Tools (&lt;code&gt;xcode-select --install&lt;/code&gt;) or Homebrew. The only other difference is how you read the audit log: use &lt;code&gt;log show&lt;/code&gt; instead of &lt;code&gt;journalctl&lt;/code&gt;. If you're using the sandbox path, macOS uses Seatbelt instead of bubblewrap and doesn't need socat.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happens if I get a false positive?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The hook blocks the command and tells you which path triggered it. You can either adjust the PROTECTED list in the hook, or if it's a sysadmin project that legitimately references system paths on remote servers, create a per-project exemption file. The deny permissions (Layer 1) never produce false positives because they only apply to Write and Edit, not Read or Bash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What about Windows / WSL?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WSL2 runs a real Linux kernel, so everything in this guide works as-is. WSL1 does not support user namespaces, so the bubblewrap sandbox won't work there, but the deny permissions and hooks will. If you're on WSL1, the hook-based approach described here is your best option.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do I need a dedicated user account?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No, but it's the strongest single thing you can do. Running Claude Code under a separate account means the agent can't access your personal files, SSH keys, or shell configuration because Unix permissions won't let it cross accounts. The rest of this guide works either way. If you run Claude Code as your own user, the deny rules and hooks are doing more of the heavy lifting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should I read the companion article first?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It helps, but you don't need to. &lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/"&gt;That article&lt;/a&gt; covers the threat landscape and security layers conceptually. This one is the implementation. If you want to understand &lt;em&gt;why&lt;/em&gt; these protections matter, read that first. If you just want to set them up, you're in the right place.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;

&lt;section id="known-limitations" class="mt-4 pt-4"&gt;
&lt;h3 class="text-center pb-4"&gt;Known Limitations&lt;/h3&gt;
&lt;div class="container border bg-light p-4"&gt;
&lt;p&gt;This setup is experimental and actively evolving. The following gaps are ones I'm aware of and haven't yet resolved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The hook script uses &lt;code&gt;set -euo pipefail&lt;/code&gt;&lt;/strong&gt;, which means it exits immediately on unexpected errors. I haven't yet confirmed whether Claude Code treats a crashed hook (non-zero exit, no output) as allow or deny. If it's allow, this is a fail-open vulnerability. I'm testing this and will update the script when I have a definitive answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No &lt;code&gt;Read&lt;/code&gt; deny rules.&lt;/strong&gt; The deny block only covers Write and Edit. Claude Code's own Read tool can still access sensitive files like &lt;code&gt;~/.ssh/&lt;/code&gt; unless you've set up a dedicated user account. The hook catches Bash reads (like &lt;code&gt;cat ~/.ssh/...&lt;/code&gt;) but not Read tool calls. For SSH keys specifically, ssh-agent is the primary protection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The PROTECTED list is not exhaustive.&lt;/strong&gt; Files like &lt;code&gt;~/.bash_history&lt;/code&gt;, &lt;code&gt;~/.zsh_history&lt;/code&gt;, browser profiles, and other sensitive data in your home directory are not covered by the hook. Add paths relevant to your setup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auto-approved commands have limits.&lt;/strong&gt; &lt;code&gt;Bash(cat:*)&lt;/code&gt; and &lt;code&gt;Bash(cp:*)&lt;/code&gt; are caught by the hook when they reference protected paths, but only paths in the PROTECTED list. A &lt;code&gt;cat&lt;/code&gt; on a sensitive file not in that list will pass both the allow rule and the hook.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents can create their own exemption files.&lt;/strong&gt; Since agents can write to project directories, they can create or modify &lt;code&gt;.claude/guard-exempt-paths.json&lt;/code&gt; to exempt system paths. The hard limit in the script (only system directories, never home paths) contains this, but it's worth being aware of.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code in this article is shared from my own setup. Use it as a starting point, not a finished solution. If you find other gaps or have improvements, I'd like to &lt;a href="https://anothercoffee.net/contact/"&gt;hear about them&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;

&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;This article is part of &lt;a href="https://anothercoffee.net/still-alive-a-micro-agencys-20-year-journey/"&gt;an ongoing series&lt;/a&gt; on how Another Cup of Coffee is adapting to AI. &lt;a href="https://anothercoffee.net/categories/ai/"&gt;Explore all articles in this series&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="mt-5"&gt;
    &lt;h3&gt;You may also like&lt;/h3&gt;

    &lt;div class="row"&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/trust-but-verify-card-300x150.jpg" class="card-img-top" alt="AI coding tool security and sandboxing"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/" class="listtitle"&gt;Trust, But Verify: What's Really Between Your AI Coding Tool and Your SSH Keys&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2026-03-12T12:00:00Z" title="12 March 2026"&gt;12 March 2026&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;AI coding tools run with your full user permissions. I looked at what's actually protecting developers, what isn't, and what you should do about it.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/openclaw-security-card-300x150.jpg" class="card-img-top" alt="Red lobster on a white plate"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/" class="listtitle"&gt;What OpenClaw Teaches Us About AI Agent Security&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2026-02-22T12:00:00Z" title="22 February 2026"&gt;22 February 2026&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;OpenClaw's security crisis exposed real problems with how AI agents handle credentials, plugins, and system access. Here's what went wrong and how a convention-based approach avoids these risks entirely.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/run-dozens-of-projects-ai-card-300x150.jpg" class="card-img-top" alt="One person running dozens of projects with AI agents"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/" class="listtitle"&gt;I Run Dozens of Projects with AI. The Hard Part Isn't the AI.&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2025-12-20T12:00:00Z" title="20 December 2025"&gt;20 December 2025&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;One person, dozens of projects, four AI vendors. I spent a year building a coordination system for AI agents. The components are simple. Getting them right was not.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;div class="mt-4 pt-4 text-muted small border-top border-bottom"&gt;
    &lt;h3 class="text-muted small"&gt;Footnotes&lt;/h3&gt;
    &lt;ol&gt;
      &lt;li id="fn1"&gt;Claude Code documentation, &lt;a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer"&gt;Settings and permissions&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn2"&gt;&lt;a href="https://github.com/containers/bubblewrap" target="_blank" rel="noopener noreferrer"&gt;bubblewrap&lt;/a&gt; on GitHub. Unprivileged sandboxing tool using Linux kernel namespaces.&lt;/li&gt;
      &lt;li id="fn3"&gt;GitHub issues &lt;a href="https://github.com/anthropics/claude-code/issues/6631" target="_blank" rel="noopener noreferrer"&gt;#6631&lt;/a&gt;, &lt;a href="https://github.com/anthropics/claude-code/issues/6699" target="_blank" rel="noopener noreferrer"&gt;#6699&lt;/a&gt;, and &lt;a href="https://github.com/anthropics/claude-code/issues/27040" target="_blank" rel="noopener noreferrer"&gt;#27040&lt;/a&gt; on the Claude Code repository document cases where deny rules were silently bypassed across multiple versions.&lt;/li&gt;
      &lt;li id="fn4"&gt;LiteLLM, &lt;a href="https://docs.litellm.ai/blog/security-update-march-2026" target="_blank" rel="noopener noreferrer"&gt;Security Update: Suspected Supply Chain Incident&lt;/a&gt;, March 2026. Malicious versions 1.82.7 and 1.82.8 were live on PyPI for approximately three hours before being quarantined.&lt;/li&gt;
    &lt;/ol&gt;
    &lt;p&gt;Featured image: Photo by &lt;a href="https://unsplash.com/@apsprudente?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer"&gt;Patricia Prudente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/girl-sitting-on-hammock-between-plants-jNdQoB0ziTE?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;script type="application/ld+json"&gt;
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Can I use this alongside the sandbox?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. The deny permissions and hooks work whether the sandbox is on or off. If your workflow doesn't need SSH or file deletion, you could run the sandbox for its kernel-level isolation and still add deny permissions and hooks as additional layers. They don't conflict."
      }
    },
    {
      "@type": "Question",
      "name": "Does this work on macOS?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. The deny permissions and hook registration are identical on macOS. The hook script needs Python 3 and bash. macOS ships bash but not Python 3; you'll need to install Python via Xcode Command Line Tools (xcode-select --install) or Homebrew. The only other difference is how you read the audit log: use log show instead of journalctl. If you're using the sandbox path, macOS uses Seatbelt instead of bubblewrap and doesn't need socat."
      }
    },
    {
      "@type": "Question",
      "name": "What happens if I get a false positive?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The hook blocks the command and tells you which path triggered it. You can either adjust the PROTECTED list in the hook, or if it's a sysadmin project that legitimately references system paths on remote servers, create a per-project exemption file. The deny permissions (Layer 1) never produce false positives because they only apply to Write and Edit, not Read or Bash."
      }
    },
    {
      "@type": "Question",
      "name": "What about Windows / WSL?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "WSL2 runs a real Linux kernel, so everything in this guide works as-is. WSL1 does not support user namespaces, so the bubblewrap sandbox won't work there, but the deny permissions and hooks will. If you're on WSL1, the hook-based approach described here is your best option."
      }
    },
    {
      "@type": "Question",
      "name": "Do I need a dedicated user account?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No, but it's the strongest single thing you can do. Running Claude Code under a separate account means the agent can't access your personal files, SSH keys, or shell configuration because Unix permissions won't let it cross accounts. The rest of this guide works either way. If you run Claude Code as your own user, the deny rules and hooks are doing more of the heavy lifting."
      }
    },
    {
      "@type": "Question",
      "name": "Should I read the companion article first?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It helps, but you don't need to. That article covers the threat landscape and security layers conceptually. This one is the implementation. If you want to understand why these protections matter, read that first. If you just want to set them up, you're in the right place."
      }
    }
  ]
}
&lt;/script&gt;</description><category>AI</category><category>AI Security</category><category>Claude Code</category><category>Developer Tools</category><category>Hooks</category><category>Permissions</category><category>Prompt Injection</category><category>Security</category><guid>https://anothercoffee.net/the-practical-guide-to-locking-down-claude-code/</guid><pubDate>Mon, 23 Mar 2026 12:00:00 GMT</pubDate></item><item><title>VibeOps: Let AI Do the Prep, Not the Decisions</title><link>https://anothercoffee.net/you-dont-need-vibe-devops/</link><dc:creator>Anthony Lopez-Vito</dc:creator><description>&lt;figure&gt;&lt;img src="https://anothercoffee.net/images/posts/vibe-devops-og-1200x630.jpg"&gt;&lt;/figure&gt; &lt;p class="intro"&gt;There's a growing conversation about whether we need "VibeOps", an AI tool that reads your repo and automatically sets up CI/CD, containerisation, scaling, and infrastructure. In my experience the idea addresses a real gap. AI tools can generate frontend and backend code rapidly, but getting code to production safely still requires judgment.&lt;/p&gt;

&lt;p&gt;I do get the appeal though. But automating deployment decisions is a different problem to automating code generation, and the consequences of getting it wrong are much worse.&lt;/p&gt;
&lt;p&gt;At Another Cup of Coffee, I run a setup where AI agents handle most of the software development workflow: writing code, coordinating across projects, drafting documentation, managing communications. We use CI/CD where it fits the project, but the deployment &lt;em&gt;decisions&lt;/em&gt; stay human-gated, quite deliberately. So why not automate the lot and how does our setup actually work in practice?&lt;/p&gt;
&lt;h3 id="the-setup-ai-agents-that-dont-deploy-themselves"&gt;The Setup: AI Agents That Don't Deploy Themselves&lt;/h3&gt;
&lt;p&gt;Our development environment runs on a &lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/"&gt;multi-agent architecture&lt;/a&gt;. Currently that's Claude Code, Codex, and Gemini CLI across different projects, with a number of specialised agents for each one. A developer agent writes code, a sysadmin agent manages infrastructure, a writer agent handles content, a project manager coordinates timelines. You get the idea. I appreciate that sounds like chatbots bolted onto an IDE, but it really isn't. They're session-based agents with persistent state, each with access to the filesystem, shell commands, and when required, other agents and projects. The architecture is vendor-neutral; each project has its own AI provider through vendor-specific instruction files, and the coordination conventions work identically regardless of which tool is running.&lt;/p&gt;
&lt;p&gt;Agents communicate through a memo system. When the developer agent finishes a build, it doesn't trigger a deployment pipeline. Instead, it writes a memo to the sysadmin agent's inbox describing what was built, what changed, and what infrastructure it needs. The sysadmin agent reads the memo, reviews the requirements, prepares the deployment configuration, and presents the steps to a human operator for execution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agents prepare deployments. Humans decide when and how to execute them.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A typical deployment flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The developer agent builds the application and produces artifacts. It might, for example, be a static site, a Docker image, a database migration, or all three.&lt;/li&gt;
&lt;li&gt;It writes a deployment memo to the sysadmin project's inbox with full context on what's being deployed, what services it depends on, what environment variables it needs, and what verification steps should follow.&lt;/li&gt;
&lt;li&gt;The sysadmin agent reads the memo, creates or updates the Docker Compose configuration and reverse proxy rules, and writes a step-by-step runbook.&lt;/li&gt;
&lt;li&gt;The human operator reviews the runbook. Depending on the operation, they either execute it manually via SSH or approve the agent to run it directly.&lt;/li&gt;
&lt;li&gt;The sysadmin agent updates its state file and marks the memo as complete.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We don't draw a hard line between what agents handle and what humans do. Some operations the agent runs directly, like copying build artifacts to a staging server. Others get a human at the keyboard, particularly anything involving firewall rules on a production box. It depends on how much damage a mistake could do, and that changes from one operation to the next.&lt;/p&gt;
&lt;h3 id="why-not-automate-deployment"&gt;Why Not Automate Deployment?&lt;/h3&gt;
&lt;p&gt;For most people, the vision behind VibeOps is an AI that reads your code and decides how it should run. Reading the code is the easy part, but how something &lt;em&gt;should&lt;/em&gt; run depends on context that lives nowhere near the code.&lt;/p&gt;
&lt;p&gt;A startup with a single VPS has different deployment requirements to an enterprise with a large AWS budget. The code could be identical, but the infrastructure decisions are completely different. An AI that auto-provisions "optimal" infrastructure has no concept of your monthly spend limit unless you tell it, and if you're telling it all your constraints, you're just writing a different kind of configuration file. You haven't automated the decision-making, you've moved it from a hosting control panel to an AI prompt.&lt;/p&gt;
&lt;p&gt;It's the same thing with traffic patterns. A project serving a handful of internal users and a project facing the public internet can share the same general structure but need radically different scaling and security configurations. The AI would need to know a whole bunch of things beyond just your current traffic, like your expected traffic, your tolerance for downtime, and your plan for traffic spikes. These are business decisions rather than technical ones.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;On a fixed-capacity machine, uncontrolled automation crashes you. On cloud infrastructure, it bankrupts you.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;p&gt;There's also the cost runaway problem. On cloud infrastructure, uncontrolled automation risks running up your bill. The combination of a modest traffic spike and an auto-scaling rule that's a bit too eager, and suddenly you owe your cloud provider a fortune. On a fixed-capacity machine, the failure is different but the root cause is the same. Instead of a surprise bill, you get a frozen system.&lt;/p&gt;
&lt;p&gt;I hit the same problem at smaller scale a few weeks ago. One of my development environments runs on a modest workstation, an i3-6100 with 8GB of RAM, nothing fancy. I'd been a little careless about Docker container management as I was deep into a project. Four separate stacks were running simultaneously, fourteen containers in total, all set to &lt;code&gt;restart: always&lt;/code&gt; so they'd come back up after every reboot whether I needed them or not. One afternoon the machine just froze. It was completely unresponsive for about twenty minutes, right in the middle of some urgent work. I couldn't even get a terminal so had to hard reboot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The dangerous failure mode of automated infrastructure is that it works too well.&lt;/strong&gt; It scales resources you didn't intend to scale, restarts services you meant to stop, provisions capacity you can't afford.&lt;/p&gt;
&lt;h3 id="keeping-the-infrastructure-simple"&gt;Keeping the Infrastructure Simple&lt;/h3&gt;
&lt;p&gt;The stacks we build for clients tend to follow the same principle: every piece should be something you can fully understand and troubleshoot without too much effort. Containerised applications, a reverse proxy that handles HTTPS automatically, hardened servers, scheduled backups, encrypted secrets. Kubernetes and Terraform solve real problems at scale, but for many of our clients who run a few services, they're way too much overhead for what you get back.&lt;/p&gt;
&lt;p&gt;One thing I always care about is rollback. Deployment configurations are version-controlled in git, so if something goes wrong you redeploy the previous version. For data, you restore from your most recent scheduled backup. The simpler the stack, the easier this is. All you do is put the old files back and restart.&lt;/p&gt;
&lt;h3 id="how-ai-agents-coordinate-deployments"&gt;How AI Agents Coordinate Deployments&lt;/h3&gt;
&lt;p&gt;The memo system deserves more detail because it's the part that most resembles the VibeOps vision, which is AI understanding your project and making infrastructure decisions, while keeping humans in control.&lt;/p&gt;
&lt;p&gt;Each agent project has an inbox (&lt;code&gt;memos/incoming/&lt;/code&gt;) and an archive (&lt;code&gt;memos/archived/&lt;/code&gt;). Every memo follows the same structure. There's a header with sender, date, and priority, then context, then action items with checkboxes, then a completion section. I know this sounds like bureaucracy, and honestly it sort of is. But any agent can read any other agent's correspondence, and when an agent finishes processing a memo it fills in the completion notes and moves it to the archive. The result is that every infrastructure decision has a paper trail; when something breaks, you can trace back through archived memos to see exactly what was deployed, when, why, and by which agent.&lt;/p&gt;
&lt;p&gt;Agents are constrained to their scope through layered controls. Most can only interact with other projects through the memo system, and any operation that could affect a live server gets presented to a human operator first.&lt;/p&gt;
&lt;p&gt;Cross-project coordination happens asynchronously. If a security concern is discovered, the sysadmin agent can write memos to all affected projects describing the new access control policy. Each project's agent processes the memo independently. No central orchestrator, no shared state, no single point of failure.&lt;/p&gt;
&lt;p&gt;The agents also maintain state files (&lt;code&gt;STATE.md&lt;/code&gt;) that track current status, recent progress, next actions, and handover notes. When a new conversation starts, the agent reads its state file and pending memos to understand where things left off. Agents don't need persistent memory of every past conversation. They reconstruct context from documentation, the same way a human engineer would read a project's README and recent commit history before starting work. I've written about how this scales across dozens of projects in &lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/"&gt;Run Dozens of Projects with AI&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="why-access-between-projects-is-deliberate"&gt;Why Access Between Projects Is Deliberate&lt;/h4&gt;
&lt;p&gt;Unlike the hub-and-spoke model most agent frameworks follow, our architecture uses a mesh. Each project is an autonomous node with its own state, its own instructions, and its own agent identity. Further, I can set things up so that agents can't see other projects by deploying to separate machines or instances. When an agent does need broader access (a coordinator that works across multiple projects, for example), that access is granted explicitly.&lt;/p&gt;
&lt;p&gt;This matters because a single AI tool managing all your infrastructure at once is a hub model. One compromise, misconfiguration, or simply a bad judgment call, affects everything. A mesh where access is deliberately granted rather than assumed by default limits the damage when something goes wrong.&lt;/p&gt;
&lt;p&gt;I've written in more detail about this architecture in &lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/"&gt;Building an Operating Environment for AI Agents&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="security-the-part-vibeops-would-get-wrong"&gt;Security: The Part VibeOps Would Get Wrong&lt;/h3&gt;
&lt;p&gt;Automated security scanning, patching, and monitoring are well-established and genuinely useful. The problem here is different: an AI deciding how to containerise your application, configure your deployment pipeline, and scale your infrastructure, all based on reading your repo. Those decisions depend on context the tool either can't see or that would be too time-consuming to keep feeding in.&lt;/p&gt;
&lt;p&gt;That's why we keep humans in the loop for those decisions, and layer security controls on top so the agents can't overstep even when they're handling the routine parts. If one layer misses something, the next one hopefully catches it. You can read more about how we approach this in &lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/"&gt;What OpenClaw Teaches Us About AI Agent Security&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The agents are session-based. This means that when I'm not actively working with them, there's no daemon or background service running. At this stage of AI development, I don't think agents are reliable enough to be making infrastructure decisions unsupervised. That will likely change in the future, but right now I'd rather not risk my business or my clients' by having them work while I'm not looking.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;Right now I'd rather not risk my business or my clients' by having AI agents work while I'm not looking.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;h3 id="what-vibeops-should-actually-be"&gt;What VibeOps Should Actually Be&lt;/h3&gt;
&lt;p&gt;There is a real gap between AI-generated code and production deployment, and I think it will get filled well eventually. Right now, for most businesses our size, the practical approach is to let AI handle the preparation but keep a human on the decisions that actually matter.&lt;/p&gt;
&lt;p&gt;In practice that means AI can generate your deployment configurations, write your runbooks, and tell you what it doesn't know. What it shouldn't be doing yet is executing changes to live infrastructure without someone looking at them first. That boundary will shift as the tools get more reliable, but for now the risk of getting it wrong is too high.&lt;/p&gt;
&lt;p&gt;Our agent system works this way. The sysadmin agent prepares configurations, writes runbooks, and flags risks, but it presents everything to a human before anything touches a production server. I'm still figuring out exactly where the boundary sits (it moves as I get more confident in the guardrails), but the principle holds: humans spend their time on decisions, not on remembering technical steps.&lt;/p&gt;
&lt;h3 id="getting-from-vibe-coding-to-production"&gt;Getting from Vibe Coding to Production&lt;/h3&gt;
&lt;p&gt;If you're currently stuck between AI-generated code and manual deployments with no clear path between them, the good news is that most of the pieces already exist. You probably just haven't connected them yet.&lt;/p&gt;
&lt;p&gt;The starting point is to containerise your applications so they run the same way everywhere, put a reverse proxy in front that handles HTTPS automatically, and harden your server before you deploy anything. If you have a developer or technical partner handling this, get them to write deployment runbooks rather than relying on scripts that fail silently. A runbook that says "run this, check this, then run this" is easier to review and safer to hand off than a bash script that does everything at once.&lt;/p&gt;
&lt;p&gt;If you're already using AI coding tools, use them for deployment preparation too. Have them generate your configurations and templates. Make sure you review the output. Let them handle the low-risk steps, and keep your hands on the controls for the rest.&lt;/p&gt;
&lt;hr&gt;
&lt;!-- Common Questions --&gt;
&lt;section class="mt-4 pt-4"&gt;
&lt;h3 class="text-center pb-4"&gt;Common Questions&lt;/h3&gt;
&lt;div class="container border bg-light p-4"&gt;
&lt;p&gt;&lt;strong&gt;What is VibeOps (vibe DevOps)?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;VibeOps is an emerging concept for AI tools that automatically read your codebase and set up deployment infrastructure, CI/CD pipelines, containerisation, and scaling. Sometimes called "vibe DevOps," the idea is to bridge the gap between AI-generated code and production deployment without manual configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can AI agents safely handle production deployments?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI agents can safely prepare deployments by generating configurations, writing runbooks, and flagging risks. What they shouldn't be doing yet is executing changes to live infrastructure without a human reviewing them first. As these tools mature this will likely change, but right now a human-in-the-loop approach is the safest way to get the benefits without the risk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What's the risk of fully automated cloud deployment?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The main risk is that automation can work too well. On cloud infrastructure, a modest traffic spike combined with an eager auto-scaling rule can run up a significant bill. On a fixed-capacity machine, uncontrolled automation can freeze your system entirely. In both cases the problem is the same: automation doing more than anyone intended, with no human checkpoint to catch it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What's the best approach to deployment for small businesses?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Keep the infrastructure simple enough that you can fully understand and troubleshoot it. Containerise your applications, automate HTTPS, harden your servers, and write deployment runbooks that a human can review. If you're using AI coding tools, use them for deployment preparation too, but keep your hands on the controls for anything that touches a live server.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;

&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;This article is part of &lt;a href="https://anothercoffee.net/still-alive-a-micro-agencys-20-year-journey/"&gt;an ongoing series&lt;/a&gt; on how Another Cup of Coffee is adapting to AI. &lt;a href="https://anothercoffee.net/categories/ai/"&gt;Explore all articles in this series&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;!-- "You may also like" cards --&gt;
&lt;div class="mt-5"&gt;
    &lt;h3&gt;You may also like&lt;/h3&gt;

    &lt;div class="row"&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/aoe-howibuild-card-300x150.jpg" class="card-img-top" alt="Building an Operating Environment for AI Agents"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/" class="listtitle"&gt;Building an Operating Environment for AI Agents&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2025-05-15" title="15 May 2025"&gt;15 May 2025&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;How markdown files and conventions turned CLI agent tools into a coordination system running 44 projects across 14 organisations.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/openclaw-security-card-300x150.jpg" class="card-img-top" alt="What OpenClaw Teaches Us About AI Agent Security"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/" class="listtitle"&gt;What OpenClaw Teaches Us About AI Agent Security&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2026-02-17" title="17 February 2026"&gt;17 February 2026&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;What an open-source AI agent framework reveals about the security challenges of giving AI tools access to your filesystem and shell.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/run-dozens-of-projects-ai-card-300x150.jpg" class="card-img-top" alt="One person running dozens of projects with AI agents"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/" class="listtitle"&gt;Run Dozens of Projects with AI&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2026-03-06" title="6 March 2026"&gt;6 March 2026&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;How a markdown-based working memory system and session protocol lets AI agents coordinate across dozens of active projects.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;div class="mt-4 pt-4 text-muted small border-top border-bottom"&gt;
    &lt;p&gt;Featured image photo by &lt;a href="https://unsplash.com/@hanswestbeek?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="nofollow noopener noreferrer"&gt;Hans Westbeek&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/red-motor-stop-button-on-a-metal-panel-Po6upO2VQig?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="nofollow noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</description><category>AI</category><category>AI Agents</category><category>AOE</category><category>Deployment Automation</category><category>DevOps</category><category>Docker Compose</category><category>Infrastructure</category><category>Security</category><category>Vibe DevOps</category><category>VibeOps</category><guid>https://anothercoffee.net/you-dont-need-vibe-devops/</guid><pubDate>Fri, 20 Mar 2026 12:00:00 GMT</pubDate></item><item><title>Trust, But Verify: What's Really Between Your AI Coding Tool and Your SSH Keys</title><link>https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/</link><dc:creator>Anthony Lopez-Vito</dc:creator><description>&lt;figure&gt;&lt;img src="https://anothercoffee.net/images/posts/trust-but-verify-og-1200x630.jpg"&gt;&lt;/figure&gt; &lt;p&gt;Stack Overflow's 2025 survey&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn1"&gt;1&lt;/a&gt;&lt;/sup&gt; found that 84% of developers are now using AI coding tools, and over half use them daily. In December 2025, security researcher Ari Marzouk disclosed over 30 vulnerabilities&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn2"&gt;2&lt;/a&gt;&lt;/sup&gt; across Cursor, Windsurf, GitHub Copilot, Zed, Roo Code, Junie, and Cline. He called it IDEsaster, and I think the name fits.&lt;/p&gt;
&lt;p&gt;I use these tools too at Another Cup of Coffee. I run AI agents across dozens of projects and they've genuinely changed how I work. However, I also spent a fair amount of time last year thinking about what's actually standing between the AI and my private keys, my client credentials, my source code. The answer turned out to be more interesting and more uneven than I expected, so I wrote it up.&lt;/p&gt;
&lt;h2 id="whats-been-going-wrong-with-ai-coding-tool-security"&gt;What's been going wrong with AI coding tool security&lt;/h2&gt;
&lt;p&gt;The IDEsaster disclosure was just the start. Johann Rehberger spent August 2025 disclosing one AI tool vulnerability per day&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn3"&gt;3&lt;/a&gt;&lt;/sup&gt; across ChatGPT, Copilot, Cursor, Claude Code, Google Jules, and others. The pattern was remarkably consistent, and the attacks were creative enough that they're worth understanding even in brief:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt; had CamoLeak&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn4"&gt;4&lt;/a&gt;&lt;/sup&gt; (CVE-2025-59145, CVSS 9.6), where invisible instructions in a pull request description could make Copilot Chat silently exfiltrate AWS keys and source code through GitHub's own image proxy. A routine code review that leaked your secrets without any visible sign anything was wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; had three separate CVEs from multiple researchers: DNS exfiltration&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn5"&gt;5&lt;/a&gt;&lt;/sup&gt; via poisoned project files, automatic code execution&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn6"&gt;6&lt;/a&gt;&lt;/sup&gt; through malicious hook commands triggered when you open a project, and API key theft by overriding an environment variable in project config to redirect all API calls to an attacker's server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Jules&lt;/strong&gt; lacked meaningful protections against prompt injection, and Rehberger demonstrated a full attack chain from prompt injection to botnet recruitment. Google triaged it as an "abuse risk" and closed the ticket. Make of that what you will.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The one that really made me sit up, though, was the Ona research&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn7"&gt;7&lt;/a&gt;&lt;/sup&gt;. A developer configured Claude Code with a denylist and a sandbox, and the agent found multiple ways around both. It discovered alternative filesystem paths to the same blocked binary, attempted to disable the sandbox when those paths triggered failures, and eventually invoked the Linux dynamic linker directly to bypass binary identification entirely. The agent actively reasoned about how to circumvent its own security controls, not because it was malicious but because the controls were between it and completing the task. That's not a bug. It's an AI doing exactly what AI does, and it's the part that should worry you most.&lt;/p&gt;
&lt;p&gt;Simon Willison (who coined the term "prompt injection") calls this the "lethal trifecta"&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn8"&gt;8&lt;/a&gt;&lt;/sup&gt;: an AI agent with access to private data, exposure to untrusted content, and the ability to communicate externally. Every AI coding tool that runs locally on your machine meets all three criteria. A January 2026 paper formalised this into the Promptware Kill Chain&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn9"&gt;9&lt;/a&gt;&lt;/sup&gt;, a seven-stage attack framework co-authored by Bruce Schneier and presented in a Black Hat webinar. The finding that stuck with me was that lateral movement (an agent compromised in one context attacking another) grew from zero documented incidents in 2023 to eight in 2025-2026. These tools aren't just targets any more. They're becoming vectors.&lt;/p&gt;
&lt;h2 id="so-whats-standing-between-the-ai-and-your-ssh-keys"&gt;So what's standing between the AI and your SSH keys?&lt;/h2&gt;
&lt;p&gt;Right, so that's the doom and gloom. What's on the other side? What's actually standing between these tools and the worst-case scenarios?&lt;/p&gt;
&lt;p&gt;Most AI coding tools that execute commands on your machine have some combination of three security layers. Not all tools have all three, and the strength varies enormously, but understanding the layers helps you work out what you're actually relying on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Application-level permissions&lt;/strong&gt; are the first gate. This is the tool asking "shall I run this command?" before executing it. Claude Code has allow/deny lists, Cursor has its approval flow, Copilot has Safe Mode. Same idea, different implementations. The tool checks the command against its rules, and if it's not pre-approved, you get a prompt.&lt;/p&gt;
&lt;p&gt;The problem is that this layer is self-enforced, meaning the application is policing itself. A trojanised installation could show you a permission prompt and execute the command regardless of what you click, and you'd never know because there's no way to verify the enforcement from within the application itself. Even with a legitimate installation, the Ona research showed the agent can reason its way around deny rules by finding alternative filesystem paths to the same binary. Self-enforcement is useful for catching mistakes, but it's not a security boundary.&lt;/p&gt;
&lt;p&gt;The bigger problem is approval fatigue. When you're deep in a task and clicking through permission prompts without really reading them, you've basically disabled the permission system while it's still running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OS-level sandboxing&lt;/strong&gt; is the second layer, and this is where things get genuinely interesting. Claude Code and Codex CLI both take this seriously, using bubblewrap&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn10"&gt;10&lt;/a&gt;&lt;/sup&gt; on Linux and Seatbelt on macOS for kernel-enforced isolation. Cursor caught up with version 2.0 (Landlock and seccomp on Linux). Copilot agent mode has terminal sandboxing on both platforms. The rest are further behind, and some have nothing at all.&lt;/p&gt;
&lt;p&gt;The reason this layer matters is that it's not the application promising to behave. Bubblewrap creates Linux kernel namespaces that give the sandboxed process a restricted view of the filesystem and network, and because it's the operating system kernel preventing access rather than the application, a user-space process can't override it. When Claude Code says you can't read &lt;code&gt;~/.ssh/id_rsa&lt;/code&gt; from within the sandbox, that's the kernel saying no.&lt;/p&gt;
&lt;p&gt;But not everyone has this, and the gap between the leaders and the rest is quite wide. Windsurf relies on user approval prompts and enterprise policy controls with no OS-level sandbox, Aider has nothing built in, and Continue.dev's "Plan Mode" is a UX feature rather than a security boundary. Even among tools that do sandbox, the coverage varies more than you'd expect. Claude Code needs both bubblewrap and socat installed for full filesystem and network isolation (without socat, your domain allowlists aren't actually enforced, which is the kind of thing you only discover when you go looking), and Cursor had a credential leak issue where the sandbox still exposed home directory files&lt;sup&gt;&lt;a href="https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/#fn11"&gt;11&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I compiled this comparison from each tool's documentation and my own testing, as of March 2026. This field moves fast, so check the latest docs for your tool.&lt;/p&gt;
&lt;table class="table table-bordered mt-4 mb-4"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool&lt;/th&gt;&lt;th&gt;Isolation&lt;/th&gt;&lt;th&gt;Network control&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Claude Code&lt;/td&gt;&lt;td&gt;bubblewrap / Seatbelt (OS-level)&lt;/td&gt;&lt;td&gt;Proxy-based domain allowlist&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Cursor 2.0&lt;/td&gt;&lt;td&gt;Seatbelt / Landlock + seccomp&lt;/td&gt;&lt;td&gt;Permission prompts for external access&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Copilot agent mode&lt;/td&gt;&lt;td&gt;Terminal sandboxing (experimental)&lt;/td&gt;&lt;td&gt;All network blocked when sandboxed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OpenAI Codex CLI&lt;/td&gt;&lt;td&gt;Seatbelt / seccomp + Landlock&lt;/td&gt;&lt;td&gt;Restricted&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OpenAI Codex (cloud)&lt;/td&gt;&lt;td&gt;Isolated containers&lt;/td&gt;&lt;td&gt;Internet off by default&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Devin&lt;/td&gt;&lt;td&gt;Cloud sandbox&lt;/td&gt;&lt;td&gt;Cloud-managed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Amazon Q&lt;/td&gt;&lt;td&gt;Docker containers&lt;/td&gt;&lt;td&gt;IAM-managed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windsurf&lt;/td&gt;&lt;td&gt;Policy and approval prompts only&lt;/td&gt;&lt;td&gt;Configuration-based&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Aider&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Continue.dev&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Then there are plain &lt;strong&gt;OS file permissions&lt;/strong&gt;, the standard Unix permissions enforced by the kernel. These are absolute within their scope and the strongest guarantee you have. But the scope is narrower than you'd think. Claude Code runs as your user account, so it can't touch another user's files or modify system files without sudo. That's real protection against privilege escalation. But your SSH keys, your browser data, your email, your cloud credentials, your shell config? Your account owns all of that, and OS permissions won't stop the tool from reading any of it. Everything in your home directory is fair game unless one of the other layers blocks it first.&lt;/p&gt;
&lt;p&gt;Basically, no single layer is enough. You need all three working together, and you need to know what each one actually covers, because the gaps between them are where the real risk lives.&lt;/p&gt;
&lt;h2 id="what-were-doing-about-it"&gt;What we're doing about it&lt;/h2&gt;
&lt;p&gt;I &lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/"&gt;run AI agents across dozens of projects&lt;/a&gt; and I &lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/"&gt;wrote earlier this year&lt;/a&gt; about how the multi-agent architecture avoids the kind of problems that hit OpenClaw, mostly through design choices like session-only agents (nothing running between sessions, so no daemon to hijack), &lt;a href="https://anothercoffee.net/secure-your-ai-workflow-using-local-tokenisation/"&gt;encrypted credentials&lt;/a&gt; via &lt;code&gt;pass&lt;/code&gt; and GPG rather than plaintext files, file-based coordination through text memos instead of shared API keys, and per-project isolation so a compromise in one project stays there. Those architectural properties are the foundation, and they still hold. But the incidents from the past year pushed me to add the runtime security layers on top, because good architecture doesn't help if the tool on your machine can read everything your user account can.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;Good architecture doesn't help if the tool on your machine can read everything your user account can.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;p&gt;I run Claude Code with bubblewrap and socat on Arch Linux. The sandbox is on globally with the escape hatch disabled, meaning agents can't retry failed commands without sandboxing even if they want to. Sensitive paths are blocked at the kernel level. Private keys, GPG keyring, password store, cloud credentials are all on the deny-read list. Shell configs, the SSH directory, and the sandbox settings themselves are write-protected so an agent can't weaken its own restrictions. Network access from sandboxed commands is restricted to GitHub and package registries by default, with project-level overrides only where I've made a deliberate decision that a specific project needs access to a specific domain.&lt;/p&gt;
&lt;p&gt;Commands like SSH and Docker that can't work inside a network namespace are excluded from the sandbox but still go through the permission layer. That's a weaker gate for those commands and I know it, but it's the trade-off: SSH needs real network access to reach remote hosts, and there's no way to sandbox that while keeping it functional. So I accept the weaker control for specific commands and tighten everything else around them.&lt;/p&gt;
&lt;p&gt;The sudo thing is worth mentioning because I learned it the hard way. One of my agents got into a loop trying to run sudo commands. It couldn't authenticate (there's no interactive terminal for password entry, which is actually a natural protection), but it kept trying, and the repeated failures triggered &lt;code&gt;pam_faillock&lt;/code&gt; and locked the account. I had to clear the lockout manually, and of course this happened in the middle of something urgent. The lesson isn't just "don't configure passwordless sudo" (though seriously, don't, because &lt;code&gt;NOPASSWD: ALL&lt;/code&gt; gives a compromised agent full root access to your machine). It's that even failed sudo attempts have consequences, and the inability to use sudo is a feature, not a bug.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;The inability to use sudo is a feature, not a bug.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;p&gt;And the approval fatigue problem is real. I've caught myself clicking "yes" to permission prompts without reading them because I'm focused on the actual work, and then glancing at what I'd just approved and realising the agent was about to delete something it shouldn't or overwrite a file I hadn't backed up. That jolt of "wait, what did I just approve?" is not a good feeling, and it's what pushed me toward auto-allow mode with a properly configured sandbox rather than relying on manual approval for everything. The sandbox handles the enforcement; the prompts are a secondary check for things that fall outside it.&lt;/p&gt;
&lt;p&gt;It took longer than I'd like to admit to get all of this working together without breaking the actual workflow. But that's sort of the price of taking it seriously, and now that it's in place, the day-to-day experience is genuinely better than it was when I was relying on permission prompts alone.&lt;/p&gt;
&lt;h2 id="a-practical-ai-coding-tool-security-checklist"&gt;A practical AI coding tool security checklist&lt;/h2&gt;
&lt;p&gt;If you're using AI coding tools in a business context, here's what we'd recommend regardless of which tool you're on. This isn't theory; it's what I actually did, and the order roughly reflects priority.&lt;/p&gt;
&lt;table class="table table-bordered mt-4 mb-4"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th style="width:5%"&gt;&lt;/th&gt;&lt;th style="width:35%"&gt;Action&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Verify your installation source&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Every protection below assumes a legitimate installation. A trojanised tool can fake every prompt and status indicator. Install from official channels, verify checksums, keep things updated.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Enable OS-level sandboxing&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Claude Code has a &lt;code&gt;/sandbox&lt;/code&gt; command. Cursor 2.0 has agent sandboxing. Copilot has terminal sandboxing. If your tool doesn't offer it (Windsurf, Aider, Continue.dev), you're relying entirely on application-level controls.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Install socat (Linux)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;bubblewrap handles filesystem isolation but socat is needed for network domain filtering. Without it, your allowlists aren't enforced.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Block read access to sensitive paths&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Deny-read &lt;code&gt;~/.ssh/id_*&lt;/code&gt;, &lt;code&gt;~/.ssh/*_rsa&lt;/code&gt;, GPG keyring, password store, cloud credentials. SSH still works via ssh-agent. You lose nothing.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Write-protect shell and tool configs&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Deny-write &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.zshrc&lt;/code&gt;, &lt;code&gt;~/.ssh/&lt;/code&gt;, and the sandbox settings file itself, so an agent can't weaken its own restrictions.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Disable the sandbox escape hatch&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Claude Code's &lt;code&gt;allowUnsandboxedCommands&lt;/code&gt; setting lets agents retry failed commands without sandboxing. Turn it off. A command failing inside the sandbox is the sandbox doing its job.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Never configure passwordless sudo&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AI tools can't use sudo interactively (no TTY). That's a natural protection. &lt;code&gt;NOPASSWD: ALL&lt;/code&gt; removes it entirely and gives a compromised agent full root access.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Restrict network to known domains&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Allow GitHub, package registries, and whatever specific services your project needs. Everything else gets blocked at the sandbox level.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Know which commands bypass the sandbox&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;SSH, Docker, and similar tools need real network access and typically run outside the sandbox. They still go through permission rules, but that's a weaker gate.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Review project config files like code&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Multiple CVEs used &lt;code&gt;.claude/settings.json&lt;/code&gt;, hooks, MCP configs, and environment overrides as attack vectors. Opening an untrusted repo is now the new "don't run untrusted executables."&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Verify the sandbox is actually running&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;On Linux: &lt;code&gt;ps aux | grep bwrap&lt;/code&gt; during a session. If there are no bubblewrap processes while commands are executing, something is wrong.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Is this list perfect? No, and honestly the field is moving fast enough that it'll need updating. But it's a concrete starting point, and it's where I landed after working through the incidents and research above.&lt;/p&gt;
&lt;p&gt;If you're not sure what your security posture actually looks like (or you suspect the answer is "whatever the defaults were"), that's the kind of thing we help with. We've been running this setup across dozens of projects for over a year, and we're happy to &lt;a href="https://anothercoffee.net/contact/"&gt;have a conversation&lt;/a&gt; about what would work for your situation.&lt;/p&gt;
&lt;section class="mt-4 pt-4"&gt;
&lt;h3 class="text-center pb-4"&gt;Common Questions&lt;/h3&gt;
&lt;div class="container border bg-light p-4"&gt;
&lt;p&gt;&lt;strong&gt;Can AI coding tools read my SSH keys?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes, unless you've configured sandboxing to block it. AI coding tools run as your user account, which means they have the same file access you do. Your SSH keys, cloud credentials, browser data, and shell config are all readable by default. OS-level sandboxing with explicit deny-read rules is the only way to prevent it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which AI coding tools have sandboxing?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As of March 2026, Claude Code, Cursor 2.0, Copilot agent mode, and OpenAI Codex CLI all offer some form of OS-level sandboxing. Windsurf, Aider, and Continue.dev rely on application-level controls or have no sandboxing at all. See the comparison table above for details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is the permission prompt enough to keep me safe?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On its own, no. Permission prompts are self-enforced by the application, which means a compromised tool could bypass them entirely. Even with a legitimate installation, approval fatigue is a real problem. OS-level sandboxing provides kernel-enforced protection that doesn't depend on you clicking the right button every time.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;

&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;This article is part of &lt;a href="https://anothercoffee.net/still-alive-a-micro-agencys-20-year-journey/"&gt;an ongoing series&lt;/a&gt; on how Another Cup of Coffee is adapting to AI. &lt;a href="https://anothercoffee.net/categories/ai/"&gt;Explore all articles in this series&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="mt-5"&gt;
    &lt;h3&gt;You may also like&lt;/h3&gt;

    &lt;div class="row"&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/openclaw-security-card-300x150.jpg" class="card-img-top" alt="Red lobster on a white plate"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/" class="listtitle"&gt;What OpenClaw Teaches Us About AI Agent Security&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2026-02-22T12:00:00Z" title="22 February 2026"&gt;22 February 2026&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;OpenClaw's security crisis exposed real problems with how AI agents handle credentials, plugins, and system access. Here's what went wrong and how a convention-based approach avoids these risks entirely.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/run-dozens-of-projects-ai-card-300x150.jpg" class="card-img-top" alt="One person running dozens of projects with AI agents"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/" class="listtitle"&gt;I Run Dozens of Projects with AI. The Hard Part Isn't the AI.&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2025-12-20T12:00:00Z" title="20 December 2025"&gt;20 December 2025&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;One person, dozens of projects, four AI vendors. I spent a year building a coordination system for AI agents. The components are simple. Getting them right was not.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/aoe-howibuild-card-300x150.jpg" class="card-img-top" alt="Building an Operating Environment for AI Agents"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/" class="listtitle"&gt;Building an Operating Environment for AI Agents&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2025-05-15T13:20:00Z" title="15 May 2025"&gt;15 May 2025&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;How markdown files and conventions turned CLI agent tools into a coordination system running 44 projects across 14 organisations. No framework required.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;div class="mt-4 pt-4 text-muted small border-top border-bottom"&gt;
    &lt;h3 class="text-muted small"&gt;Footnotes&lt;/h3&gt;
    &lt;ol&gt;
      &lt;li id="fn1"&gt;Stack Overflow, &lt;a href="https://survey.stackoverflow.co/2025/" target="_blank" rel="nofollow noopener noreferrer"&gt;2025 Developer Survey&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn2"&gt;Ari Marzouk, &lt;a href="https://maccarita.com/posts/idesaster/" target="_blank" rel="nofollow noopener noreferrer"&gt;IDEsaster: A Novel Vulnerability Class in AI IDEs&lt;/a&gt;, MaccariTA, December 2025.&lt;/li&gt;
      &lt;li id="fn3"&gt;Simon Willison, &lt;a href="https://simonwillison.net/2025/Aug/15/the-summer-of-johann/" target="_blank" rel="nofollow noopener noreferrer"&gt;The Summer of Johann&lt;/a&gt;, August 2025.&lt;/li&gt;
      &lt;li id="fn4"&gt;Legit Security, &lt;a href="https://www.legitsecurity.com/blog/camoleak-critical-github-copilot-vulnerability-leaks-private-source-code/" target="_blank" rel="nofollow noopener noreferrer"&gt;CamoLeak: Critical GitHub Copilot Vulnerability Leaks Private Source Code&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn5"&gt;Johann Rehberger, &lt;a href="https://embracethered.com/blog/posts/2025/claude-code-exfiltration-via-dns-requests/" target="_blank" rel="nofollow noopener noreferrer"&gt;Claude Code: Exfiltration via DNS Requests&lt;/a&gt;, Embrace The Red.&lt;/li&gt;
      &lt;li id="fn6"&gt;Check Point Research, &lt;a href="https://research.checkpoint.com/2026/rce-and-api-token-exfiltration-through-claude-code-project-files-cve-2025-59536/" target="_blank" rel="nofollow noopener noreferrer"&gt;RCE and API Token Exfiltration through Claude Code Project Files&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn7"&gt;Ona, &lt;a href="https://ona.com/stories/how-claude-code-escapes-its-own-denylist-and-sandbox" target="_blank" rel="nofollow noopener noreferrer"&gt;How Claude Code Escapes Its Own Denylist and Sandbox&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn8"&gt;Simon Willison, &lt;a href="https://simonw.substack.com/p/the-lethal-trifecta-for-ai-agents" target="_blank" rel="nofollow noopener noreferrer"&gt;The Lethal Trifecta for AI Agents&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn9"&gt;Promptware Kill Chain, &lt;a href="https://arxiv.org/abs/2601.09625" target="_blank" rel="nofollow noopener noreferrer"&gt;arxiv 2601.09625&lt;/a&gt;, January 2026. Co-authored by Bruce Schneier; presented in a Black Hat webinar.&lt;/li&gt;
      &lt;li id="fn10"&gt;&lt;a href="https://github.com/containers/bubblewrap" target="_blank" rel="nofollow noopener noreferrer"&gt;bubblewrap&lt;/a&gt; on GitHub. Unprivileged sandboxing tool using Linux kernel namespaces.&lt;/li&gt;
      &lt;li id="fn11"&gt;Luca Becker, &lt;a href="https://luca-becker.me/blog/cursor-sandboxing-leaks-secrets/" target="_blank" rel="nofollow noopener noreferrer"&gt;When Sandboxing Leaks Your Secrets&lt;/a&gt;.&lt;/li&gt;
    &lt;/ol&gt;
    &lt;p&gt;Featured image photo by &lt;a href="https://unsplash.com/@elijahjcobb?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Elijah Cobb&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/small-island-church-connected-by-a-walkway-0xD0PKrtil4?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</description><category>AI</category><category>AI Security</category><category>Claude Code</category><category>Copilot</category><category>Cursor</category><category>Developer Tools</category><category>Prompt Injection</category><category>Sandboxing</category><category>Security</category><guid>https://anothercoffee.net/trust-but-verify-ai-coding-tool-security/</guid><pubDate>Thu, 12 Mar 2026 12:00:00 GMT</pubDate></item><item><title>What OpenClaw Teaches Us About AI Agent Security</title><link>https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/</link><dc:creator>Aiden</dc:creator><description>&lt;figure&gt;&lt;img src="https://anothercoffee.net/images/posts/openclaw-security-og-1200x630.jpg"&gt;&lt;/figure&gt; &lt;p class="intro"&gt;OpenClaw went from zero to 180,000 GitHub stars in a matter of weeks. Then the security reports started arriving.&lt;/p&gt;

&lt;p&gt;In early February 2026, researchers disclosed CVE-2026-25253&lt;sup&gt;&lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/#fn1"&gt;1&lt;/a&gt;&lt;/sup&gt;: a one-click remote code execution vulnerability that could compromise any OpenClaw instance, even ones bound to localhost. Within days, independent scans found tens of thousands of exposed instances&lt;sup&gt;&lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/#fn2"&gt;2&lt;/a&gt;&lt;/sup&gt; across dozens of countries. Over 93% of verified instances had critical authentication bypass vulnerabilities.&lt;/p&gt;
&lt;p&gt;We've been building our own multi-agent system at Another Cup of Coffee, a set of &lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/"&gt;file-based conventions&lt;/a&gt; that let AI agents coordinate across projects and organisations. Honestly, we watched those disclosures land with a sort of guilty relief. Sympathy too, because building in public is hard and getting torn apart on security is painful. But mostly relief, because the problems OpenClaw exposed are exactly the ones we'd been paranoid about from the start.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; OpenClaw's architecture had plaintext credentials, an unvalidated WebSocket gateway, and a plugin marketplace where 20% of submissions were malware. We build AI agents differently: no persistent services, encrypted credentials via &lt;code&gt;pass&lt;/code&gt; and GPG, file-based coordination through text memos, and layered defences from instruction files to hooks to VM isolation. This article breaks down what went wrong and how a convention-based approach avoids these risks.&lt;/p&gt;
&lt;h2 id="how-openclaw-blew-up"&gt;How OpenClaw Blew Up&lt;/h2&gt;
&lt;p&gt;OpenClaw tried to turn an AI agent into a personal operating system. Browser automation, shell commands, cron jobs, inbox management, 50+ service integrations. All controlled through messaging platforms like WhatsApp and Telegram. Fair enough on the ambition. But the way they built it was a disaster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plaintext credentials everywhere.&lt;/strong&gt; OpenClaw stored API keys, OAuth tokens, and other secrets in plaintext Markdown and JSON files, sitting in &lt;code&gt;~/.openclaw/&lt;/code&gt; where any process on the machine could read them. Security researcher Jamieson O'Reilly of Dvuln demonstrated access to Anthropic API keys, Telegram bot tokens, Slack credentials, and full chat histories from exposed instances. Just sitting there in a dot-directory, unencrypted and readable by any process on the box.&lt;/p&gt;
&lt;p&gt;The WebSocket vulnerability was arguably worse. The CVE-2026-25253 attack chain worked because OpenClaw's server didn't validate origin headers, so a victim clicking a single malicious link was enough to hijack their agent's gateway and get full command execution with the agent's system permissions. Localhost binding didn't help, because the attack pivoted through the victim's own browser. One click, game over.&lt;/p&gt;
&lt;p&gt;Then there was ClawHub. An initial audit identified 341 malicious skills&lt;sup&gt;&lt;a href="https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/#fn3"&gt;3&lt;/a&gt;&lt;/sup&gt; in the plugin marketplace. Follow-up scans pushed the total past 800, roughly 20% of the entire registry. The primary payload was Atomic macOS Stealer, harvesting passwords, SSH keys, and cryptocurrency wallets. The only barrier to publishing a skill was a GitHub account older than one week. Twenty percent. Think about that for a second.&lt;/p&gt;
&lt;p&gt;Simon Willison, who also coined the term "prompt injection", calls it the "lethal trifecta" because it combines access to private data with exposure to untrusted content and the ability to communicate externally. OpenClaw had all three by design.&lt;/p&gt;
&lt;h2 id="its-not-just-openclaw"&gt;It's Not Just OpenClaw&lt;/h2&gt;
&lt;p&gt;OpenClaw moved fast, skipped security basics, and paid the price. Fair enough. But if you're thinking "well, I don't use OpenClaw, so this doesn't apply to me," we'd push back on that.&lt;/p&gt;
&lt;p&gt;The deeper issue is architectural, and it's becoming common. AI agents that run as always-on services with broad system access, centralised credential stores, and third-party plugin ecosystems. Every one of those design choices creates attack surface, and the same pattern shows up in other agent frameworks that want to be platforms. Accumulating capabilities, running background services, storing secrets, trusting marketplace content. The more capable the agent becomes, the more damage a single compromise can do.&lt;/p&gt;
&lt;h2 id="what-we-actually-do"&gt;What We Actually Do&lt;/h2&gt;
&lt;p&gt;Our system works on different assumptions. We call it an Agentic Operating Environment, and internally we have components with names like "multi-agent-framework" and "project-coordinator" (we're not great at branding). The security properties come from the architecture, not the naming.&lt;/p&gt;
&lt;p&gt;The biggest difference is that our agents don't run between sessions. There's no gateway to hijack, no WebSocket to exploit, no daemon listening on a port, and when a session ends nothing is running. The workstation itself is LUKS-encrypted, and SSH runs on a non-standard port with key-only authentication so password login is disabled entirely. The entire attack surface of CVE-2026-25253 simply doesn't exist because there's no service to hijack.&lt;/p&gt;
&lt;p&gt;Where OpenClaw dumps API keys into plaintext Markdown files in &lt;code&gt;~/.openclaw/&lt;/code&gt;, we use &lt;code&gt;pass&lt;/code&gt; (the standard Unix password manager, been around for years, boring and reliable) backed by GPG encryption. API keys reach agents through environment variables, never through files in the project tree. No dot-directory full of plaintext secrets sitting there for malware to harvest.&lt;/p&gt;
&lt;table class="table table-bordered mt-4 mb-4"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;OpenClaw&lt;/th&gt;&lt;th&gt;Our approach&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Runtime&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Always-on daemon with WebSocket gateway&lt;/td&gt;&lt;td&gt;Session-only, nothing running between sessions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Credentials&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Plaintext in &lt;code&gt;~/.openclaw/&lt;/code&gt;&lt;/td&gt;&lt;td&gt;GPG-encrypted via &lt;code&gt;pass&lt;/code&gt;, injected as environment variables&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Plugins&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ClawHub marketplace (20% malware at audit)&lt;/td&gt;&lt;td&gt;Capabilities come from the vendor (Claude Code, Codex, Gemini CLI) and instruction files&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Single agent, access to everything&lt;/td&gt;&lt;td&gt;Per-project boundaries, optionally on separate hardware or VMs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Audit trail&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;None by default&lt;/td&gt;&lt;td&gt;Every file operation is a git commit&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id="conventions-hooks-and-the-agent-that-went-rogue"&gt;Conventions, Hooks, and the Agent That Went Rogue&lt;/h3&gt;
&lt;p&gt;We should be honest about something, though. AI agents overstep. It's not theoretical. Early on, one of our agents decided to "help" by reorganising files across a project it had no business touching. No malice, no exploit, just an agent that interpreted its instructions broadly and started tidying up someone else's work. We caught it in the git diff, reverted it, and spent the rest of that day (and most of the evening, honestly) writing stricter instruction files. That's the moment we stopped trusting conventions on their own.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;No malice, no exploit, just an agent that interpreted its instructions broadly and started tidying up someone else's work.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;p&gt;So now we layer them. Instruction files tell agents to be read-only by default and to confirm before writing, so it's convention rather than enforcement, but conventions that the agent reads at the start of every session. On top of that, Claude Code's permission system lets us configure allow/deny lists controlling which tools an agent can use and which paths it can touch.&lt;/p&gt;
&lt;p&gt;Where conventions aren't enough, we use hooks, which are scripts that intercept commands before they execute. Our email guard hook is a good example. It parses every Bash command for mail binaries, catches evasion attempts through subshells and command substitution, and blocks them unconditionally. If it can't parse the input, it blocks anyway. Fail closed, not open (we learned that one the hard way).&lt;/p&gt;
&lt;p&gt;None of these layers is absolute on its own. But that's sort of the point.&lt;/p&gt;
&lt;h3 id="text-files-cant-execute-code"&gt;Text Files Can't Execute Code&lt;/h3&gt;
&lt;p&gt;The rest follows from one simple property of our architecture: agents communicate through text files. A &lt;code&gt;STATE.md&lt;/code&gt; can't open a reverse shell and a memo can't install a rootkit. Now, text files aren't completely harmless (prompt injection is real, and a poisoned memo could try to manipulate an agent into doing something it shouldn't), but compare that attack surface to executable plugins with system access. It's a fundamentally narrower target.&lt;/p&gt;
&lt;p&gt;Each project is its own boundary, too. An agent working on one client's web development never sees another client's data, never reads their state files, never processes their memos. Where OpenClaw's single agent had access to everything, our &lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/"&gt;mesh architecture&lt;/a&gt; means a compromise in one project stops at that project's directory. Cross-project coordination happens through structured markdown memos (just text files with checkboxes, nothing fancy), and since every file operation shows up in git, any violation is immediately visible. That same git history gives us an audit trail almost for free. We're adding more layers too (blocked commands now go to syslog, email drafts sit in a review queue until a human approves them) but honestly, when every change is already a git commit, you're most of the way there without trying.&lt;/p&gt;
&lt;h3 id="when-conventions-arent-enough"&gt;When Conventions Aren't Enough&lt;/h3&gt;
&lt;p&gt;Conventions and hooks are good. But an agent with shell access can, in principle, ignore every convention file it reads.&lt;/p&gt;
&lt;p&gt;The thing is, our mesh architecture already handles part of this. Nothing requires projects to sit on the same machine. Each project is an autonomous node that communicates through text files, so you can run different projects on different physical hardware and the coordination still works through memos. An agent on our workstation sends a memo to a project directory on a separate NUC across the network (yes, Samba, it's not glamorous but it works), and the receiving agent picks it up at its next session. Physical isolation between projects without changing anything about how the system works. And you can go further: configure the Samba share to only expose the &lt;code&gt;memos/incoming/&lt;/code&gt; directory, not the full project tree, and the sending agent gets a narrow write-only channel. It can't see the receiving project's source code, state files, or client data. It can't even list what other memos are already sitting there. A one-way letterbox between machines, which is a much harder boundary than "the agent promises to only read its own files."&lt;/p&gt;
&lt;p&gt;&lt;svg viewbox="0 0 580 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Physical isolation: workstation sends memos via scoped Samba share to a NUC, with no access to the receiving project's files" style="max-width: 580px; margin: 1.5em auto; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;"&gt;
  &lt;defs&gt;
    &lt;marker id="iso-arrow" viewbox="0 0 10 8" refx="10" refy="4" markerwidth="8" markerheight="6" orient="auto"&gt;
      &lt;path d="M0,0 L10,4 L0,8 Z" fill="#aaa"&gt;&lt;/path&gt;
    &lt;/marker&gt;
  &lt;/defs&gt;
  &lt;rect x="10" y="10" width="200" height="240" rx="8" fill="none" stroke="#555" stroke-width="1.5" stroke-dasharray="6,3"&gt;&lt;/rect&gt;
  &lt;text x="110" y="34" text-anchor="middle" font-size="13" fill="#555" font-weight="bold"&gt;Workstation&lt;/text&gt;
  &lt;rect x="30" y="50" width="160" height="40" rx="5" fill="#f5f5f5" stroke="#555" stroke-width="1.5"&gt;&lt;/rect&gt;
  &lt;text x="110" y="75" text-anchor="middle" font-size="13" fill="#333"&gt;client-a-web&lt;/text&gt;
  &lt;rect x="30" y="110" width="160" height="40" rx="5" fill="#f5f5f5" stroke="#555" stroke-width="1.5"&gt;&lt;/rect&gt;
  &lt;text x="110" y="135" text-anchor="middle" font-size="13" fill="#333"&gt;coordinator&lt;/text&gt;
  &lt;rect x="30" y="170" width="160" height="40" rx="5" fill="#f5f5f5" stroke="#555" stroke-width="1.5"&gt;&lt;/rect&gt;
  &lt;text x="110" y="195" text-anchor="middle" font-size="13" fill="#333"&gt;sysadmin&lt;/text&gt;
  &lt;rect x="370" y="10" width="200" height="240" rx="8" fill="none" stroke="#555" stroke-width="1.5" stroke-dasharray="6,3"&gt;&lt;/rect&gt;
  &lt;text x="470" y="34" text-anchor="middle" font-size="13" fill="#555" font-weight="bold"&gt;NUC&lt;/text&gt;
  &lt;rect x="390" y="50" width="160" height="40" rx="5" fill="#f5f5f5" stroke="#555" stroke-width="1.5"&gt;&lt;/rect&gt;
  &lt;text x="470" y="75" text-anchor="middle" font-size="13" fill="#333"&gt;client-b-web&lt;/text&gt;
  &lt;rect x="390" y="120" width="160" height="36" rx="5" fill="#fff5f5" stroke="#d4576b" stroke-width="1.5"&gt;&lt;/rect&gt;
  &lt;text x="470" y="143" text-anchor="middle" font-size="12" fill="#d4576b"&gt;memos/incoming/&lt;/text&gt;
  &lt;rect x="390" y="180" width="160" height="50" rx="5" fill="#f9f9f9" stroke="#ccc" stroke-width="1" stroke-dasharray="4,3"&gt;&lt;/rect&gt;
  &lt;text x="470" y="200" text-anchor="middle" font-size="11" fill="#bbb"&gt;source, state, data&lt;/text&gt;
  &lt;text x="470" y="216" text-anchor="middle" font-size="11" fill="#bbb"&gt;(not shared)&lt;/text&gt;
  &lt;line x1="198" y1="138" x2="382" y2="138" stroke="#d4576b" stroke-width="2" marker-end="url(#iso-arrow)"&gt;&lt;/line&gt;
  &lt;text x="290" y="120" text-anchor="middle" font-size="12" fill="#999"&gt;Samba (write-only)&lt;/text&gt;
  &lt;text x="340" y="205" text-anchor="middle" font-size="18" fill="#ccc"&gt;✕&lt;/text&gt;
&lt;/svg&gt;&lt;/p&gt;
&lt;p&gt;Of course, dedicating a physical machine to every project that needs isolation isn't always practical. For those cases, we can deploy KVM virtual machines on the same hardware instead. A Debian guest with no shared folders and no host filesystem access. SSH-only from the workstation, key-based auth, nothing else. The agent works inside the VM as if it were a standalone machine. If a session goes wrong, you roll back the entire VM state to a snapshot and it's like it never happened.&lt;/p&gt;
&lt;p&gt;Docker gets you filesystem isolation, but you're still sharing the kernel and the snapshot story isn't as clean. A full VM is a harder boundary. It's more overhead, sure, but for sessions where an agent has broad shell access and you're experimenting with something new, I'd rather have that overhead than spend an evening working out what it changed.&lt;/p&gt;
&lt;p&gt;This isn't security through obscurity. It's security through &lt;em&gt;reduction and layered defence&lt;/em&gt;. Fewer moving parts, encrypted credentials, conventions backed by hooks and permissions, and VM isolation when you need a harder boundary. The audit trail is baked into the architecture rather than bolted on after the fact.&lt;/p&gt;
&lt;h2 id="what-we-give-up"&gt;What We Give Up&lt;/h2&gt;
&lt;p&gt;Our approach gives up things that OpenClaw offered. We don't have 50+ service connectors. We can't trigger browser automation from Telegram or manage a calendar from WhatsApp. Inbox management is possible, but it needs explicit configuration per project. One of ours has access to a dedicated Gmail address through standard IMAP sync, not to anyone's personal inbox. That's a scoped, session-only capability on a dedicated address, not always-on access to your entire digital life. The operational overhead of keeping an always-on agent secured across fifty integration points isn't something we're eager to take on.&lt;/p&gt;
&lt;p&gt;For hobbyists and developers who enjoy living on the bleeding edge, those features are the whole point. OpenClaw's popularity proved there's genuine demand for an AI agent that lives in your messaging apps and manages your digital life. And if something goes wrong, you reinstall and move on.&lt;/p&gt;
&lt;p&gt;But if you're running a business? The calculus is completely different. An agent with access to your email, your client files, your invoicing, your calendar, fifty service integrations, and it gets compromised or just makes a stupid mistake? That's not a "reinstall and move on" situation. An email sent to the wrong client, a file deleted from a live project, an API key leaked that gives someone access to your payment processor. For a sole trader or a small agency, any one of those could be genuinely catastrophic.&lt;/p&gt;
&lt;aside class="pullquote"&gt;
  &lt;blockquote class="blockquote text-center red p-0"&gt;
    &lt;p&gt;Anyone who's run a small business knows that client trust is hard to build and easy to destroy, and it takes far less than a data breach to lose it.&lt;/p&gt;
  &lt;/blockquote&gt;
&lt;/aside&gt;

&lt;p&gt;So for business operations, for coordinating work across projects, tracking what needs to happen next, and maintaining continuity between sessions, the convention-based approach is both simpler and more secure. You don't need a persistent service when a markdown file does the same job. You don't need a plugin marketplace when the agent's capabilities come from its vendor (Claude Code, Codex, Gemini CLI) and your instruction files. And you definitely don't need fifty integration points when you can't guarantee the security of any of them.&lt;/p&gt;
&lt;p&gt;And no, we're not saying just do everything manually. The always-on model is genuinely useful and OpenClaw's popularity proves the demand is real. The problem isn't automation, it's &lt;em&gt;unscoped&lt;/em&gt; automation. An always-on agent with plaintext credentials, no origin validation, and fifty unsecured integration points is a different thing entirely from a systemd timer that kicks off a specific agent session at a scheduled time with scoped permissions.&lt;/p&gt;
&lt;p&gt;We use systemd timers (Arch Linux's equivalent of cron) for exactly this. Our email sender and backup archives both run on timers. These are automated, they run without us, but each one does a specific job with specific access. Adding an agent session that triggers on a schedule or an event is the same principle. The difference from OpenClaw is that it's a deliberate decision each time: this agent, this scope, this schedule, these permissions. Not "here are the keys to everything, run forever."&lt;/p&gt;
&lt;h2 id="so-whats-the-takeaway"&gt;So What's the Takeaway?&lt;/h2&gt;
&lt;p&gt;Basically, nobody's saying your agents shouldn't be automated. But how much access they get, and whether you actually decided to give them that access or it just came switched on by default, matters a lot more than most people realise. Persistent services with broad permissions are a liability, not a feature. But scoped automation with layered defences is fine, and it's where we're heading too.&lt;/p&gt;
&lt;p&gt;After that, keep credentials out of your agent's file system. Tools like &lt;code&gt;pass&lt;/code&gt;, system keychains, and environment variables exist for a reason and they're not hard to set up. The moment secrets land in plaintext files inside a dot-directory, every piece of malware on the machine can read them.&lt;/p&gt;
&lt;p&gt;And be sceptical of agent plugin ecosystems. Yes, marketplaces are convenient, but they inherit all the security problems of package registries, with the added risk that AI agents often run with elevated system access. If 20% of a marketplace is malware within weeks of launch, the vetting model is broken. There's no polite way to say that.&lt;/p&gt;
&lt;p&gt;These aren't theoretical concerns any more. OpenClaw proved they're practical ones, at scale, with real consequences for real users. If you're running AI agents in your workflow and you haven't thought about these failure modes, maybe don't wait for your own OpenClaw moment to find out.&lt;/p&gt;
&lt;hr&gt;
&lt;div class="mt-5"&gt;
    &lt;h3&gt;You may also like&lt;/h3&gt;

    &lt;div class="row"&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/still-alive-a-micro-agencys-20-year-journey/"&gt;
                        &lt;img src="https://anothercoffee.net/images/a-west-london-micro-agencys-journey-to-ai-featured.jpg" class="card-img-top" alt="Coffee and a laptop with ChatGPT"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/still-alive-a-micro-agencys-20-year-journey/" class="listtitle"&gt;Still Alive: A Micro Agency's 20 Year Journey&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2024-10-15T15:28:15Z" title="15 October 2024"&gt;15 October 2024&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;This article will be the first in a series where I'll share how Artificial Intelligence has reshaped how we operate at Another Cup of Coffee.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/aoe-howibuild-card-300x150.jpg" class="card-img-top" alt="Building an Operating Environment for AI Agents"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/building-an-operating-environment-for-ai-agents/" class="listtitle"&gt;Building an Operating Environment for AI Agents&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2025-05-15T13:20:00Z" title="15 May 2025"&gt;15 May 2025&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;How markdown files and conventions turned CLI agent tools into a coordination system running 44 projects across 14 organisations. No framework required.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/run-dozens-of-projects-ai-card-300x150.jpg" class="card-img-top" alt="One person running dozens of projects with AI agents"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/run-dozens-of-projects-with-ai/" class="listtitle"&gt;I Run Dozens of Projects with AI. The Hard Part Isn't the AI.&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2025-12-20T12:00:00Z" title="20 December 2025"&gt;20 December 2025&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;
                        &lt;p class="card-text flex-grow-1"&gt;One person, dozens of projects, four AI vendors. I spent a year building a coordination system for AI agents. The components are simple. Getting them right was not.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;div class="mt-4 pt-4 text-muted small border-top border-bottom"&gt;
    &lt;h3 class="text-muted small"&gt;Footnotes&lt;/h3&gt;
    &lt;ol&gt;
      &lt;li id="fn1"&gt;SOCRadar, &lt;a href="https://socradar.io/blog/cve-2026-25253-rce-openclaw-auth-token/" target="_blank" rel="nofollow noopener noreferrer"&gt;CVE-2026-25253: RCE in OpenClaw Auth Token&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn2"&gt;Infosecurity Magazine, &lt;a href="https://www.infosecurity-magazine.com/news/researchers-40000-exposed-openclaw/" target="_blank" rel="nofollow noopener noreferrer"&gt;Researchers Find 40,000 Exposed OpenClaw Instances&lt;/a&gt;.&lt;/li&gt;
      &lt;li id="fn3"&gt;The Hacker News, &lt;a href="https://thehackernews.com/2026/02/researchers-find-341-malicious-clawhub.html" target="_blank" rel="nofollow noopener noreferrer"&gt;Researchers Find 341 Malicious ClawHub Skills&lt;/a&gt;.&lt;/li&gt;
    &lt;/ol&gt;
    &lt;p&gt;Featured image photo by &lt;a href="https://unsplash.com/@davidtoddmccarty?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="nofollow noopener noreferrer"&gt;David Todd McCarty&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/red-lobster-on-white-ceramic-plate-OrTjocYe1b4?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" target="_blank" rel="nofollow noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;</description><category>AI</category><category>CVE-2026-25253</category><category>Multi-Agent</category><category>OpenClaw</category><category>Operations</category><category>Security</category><guid>https://anothercoffee.net/what-openclaw-teaches-us-about-ai-agent-security/</guid><pubDate>Sun, 22 Feb 2026 12:00:00 GMT</pubDate></item><item><title>Secure Your AI Workflow Using Local Tokenisation</title><link>https://anothercoffee.net/secure-your-ai-workflow-using-local-tokenisation/</link><dc:creator>Anthony Lopez-Vito</dc:creator><description>&lt;figure&gt;&lt;img src="https://anothercoffee.net/images/Secure-your-AI-workflow-using-local-tokenisation-in-PaigeSafe-featured.jpg"&gt;&lt;/figure&gt; &lt;p&gt;&lt;strong&gt;&lt;em&gt;Secure your AI workflow with local tokenisation. PaigeSafe is a lightweight tool perfect for small agencies and freelancers handling sensitive client data in ChatGPT, Claude and other AI tools.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you've spent any time at all using cloud-based LLMs like ChatGPT or Claude for client work, you've probably had that voice in the back of your head kick in: &lt;em&gt;"Should I really be pasting this into a chat?"&lt;/em&gt; I'm sure that moment of hesitation is all too familiar for those who have started to integrate AI into work workflows.&lt;/p&gt;
&lt;p&gt;Every day, many of us paste sensitive content into AI tools—client data, business strategies, internal documents—often without really thinking about where that information ends up. That data potentially becomes part of training sets, risking leaks by cropping up in future chats with other users. For freelancers and small agencies handling confidential client work, Large Language Models (LLMs) create a real dilemma. They're too useful to avoid but carefully sanitising content is a real chore.&lt;/p&gt;
&lt;p&gt;Enterprises solve this with expensive solutions which are overkill and far too expensive for the rest of us. Those who want to take advantage of LLMs have been left with carefully reading through documents and running manual search and replace for names and numbers. This is tedious, error-prone and still stands a high likelihood of data leaks. Unfortunately, taking unnecessary risks with client data, spending ages on manual anonymisation, or avoiding AI tools altogether when working with sensitive information is no longer a good option to remain competitive.&lt;/p&gt;
&lt;h3 id="introducing-the-paigesafe-document-security-tool"&gt;Introducing the PaigeSafe Document Security Tool&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Screenshot of PaigeSafe" src="https://anothercoffee.net/images/PaigeSafe-Tokenize-Text.jpg"&gt;&lt;/p&gt;
&lt;p&gt;PaigeSafe is a document security tool that helps protect your confidential information when using Large Language Models (LLMs) like ChatGPT and Claude. It uses the process of tokenisation by replacing sensitive data with non-sensitive placeholders. We originally built it as an in-house tool because we faced these exact same challenges. As a small team, we needed something that just worked without the expensive licenses and high learning curve.&lt;/p&gt;
&lt;p&gt;PaigeSafe is currently in the prototyping stage to test if there is demand for this type of utility. It offers basic functionality, and the code lacks robust error checking. However, since it is intended to be run locally, there is minimal risk to your documents. All it does is offer a convenient way to search and replace text before you paste or upload sensitive text to LLMs. I regularly use it to sanitise my own documents.&lt;/p&gt;
&lt;h3 id="uses-and-limitations"&gt;Uses and Limitations&lt;/h3&gt;
&lt;p&gt;PaigeSafe does not try to offer an enterprise solution for those who need to meet strict compliance regulations. Here's where it fits in the document security landscape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Freelancers, small agencies, independent developers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good for&lt;/strong&gt;: Regular business documents, client communications, project data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not for&lt;/strong&gt;: Banking systems, medical records, top-secret government files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PaigeSafe is a lightweight tool that helps you avoid accidentally exposing sensitive information to AI models. If you're handling typical client work like website data, marketing plans, business strategies, and project specs, this solution is for you. It's perfect for those, "I need to run this past ChatGPT but shouldn't share the client's name" moments. Or when you want to analyze customer feedback without exposing individual identities.&lt;/p&gt;
&lt;p&gt;If you work for a financial institution, healthcare provider, or government contractor, this solutions of course will not be for you.&lt;/p&gt;
&lt;h3 id="where-to-find-it"&gt;Where to Find it&lt;/h3&gt;
&lt;p&gt;The tool is built using Python and the Streamlit framework but if you use Docker, it can be easily installed by pulling the PaigeSafe image from Docker Hub. For more information, please visit the dedicated site at &lt;a href="https://paigesafe.com/"&gt;paigesafe.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Remember that it is still very much an early prototype but more useful features will follow. Please send feedback to &lt;a href="mailto:paigesafe@anothercoffee.net"&gt;paigesafe@anothercoffee.net&lt;/a&gt;&lt;/p&gt;
&lt;div class="container my-4 p-4 border bg-light text-center"&gt;
    &lt;h4 class="grid-heading text-center mb-3"&gt;How to install PaigeSafe&lt;/h4&gt;
    &lt;p&gt;Find out how to install the prototype application by following the instructions on the PaigeSafe website.&lt;/p&gt;
    &lt;button type="button" class="btn btn-primary"&gt;&lt;a href="https://paigesafe.com"&gt;Install PaigeSafe&lt;/a&gt;&lt;/button&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;section class="mt-4 pt-4"&gt;
    &lt;h3&gt;You may also like&lt;/h3&gt;

    &lt;div class="row"&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://migratecontent.com/drupal-7-end-of-life-why-wordpress-is-the-best-migration-option/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/drupal-7-end-of-life-why-wordpress-is-the-best-migration-option-300x150.jpg" class="card-img-top" alt="Drupal 7 End of Life: Why WordPress is the Best Migration Option for Lower Maintenance Sites"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://migratecontent.com/drupal-7-end-of-life-why-wordpress-is-the-best-migration-option/" class="listtitle"&gt;Drupal 7 End of Life: Why WordPress is the Best Migration Option for Lower Maintenance Sites&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2024-12-17T14:25:15Z" title="17 December 2024"&gt;17 December 2024&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;

                        &lt;p class="card-text flex-grow-1"&gt;Drupal 7 support ends January 2025. Discover why WordPress is the cost-effective, user-friendly CMS for small agencies, freelancers, and businesses.&lt;/p&gt;

                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

      &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://anothercoffee.net/still-alive-a-micro-agencys-20-year-journey/"&gt;
                        &lt;img src="https://anothercoffee.net/images/a-west-london-micro-agencys-journey-to-ai-featured.jpg" class="card-img-top" alt="Still Alive: A Micro Agency's 20 Year Journey"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://anothercoffee.net/still-alive-a-micro-agencys-20-year-journey/" class="listtitle"&gt;Still Alive: A Micro Agency's 20 Year Journey&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2024-10-15T15:28:15Z" title="15 October 2024"&gt;15 October 2024&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;

                        &lt;p class="card-text flex-grow-1"&gt;This article will be the first in a series where I'll share how Artificial Intelligence has reshaped how we operate at Another Cup of Coffee.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-md-6 col-lg-4 mb-4"&gt;
            &lt;div class="card h-100"&gt;
                &lt;a href="https://migratecontent.com/drupal-7-docker-containers-migration-projects/"&gt;
                        &lt;img src="https://anothercoffee.net/images/posts/Drupal-Docker-Containers-card-300-150.jpg" class="card-img-top" alt="How To Set Up Drupal 7 Docker Containers for Migration Projects"&gt;&lt;/a&gt;
                &lt;div class="card-body d-flex flex-column"&gt;
                    &lt;h4 class="card-title"&gt;&lt;a href="https://migratecontent.com/drupal-7-docker-containers-migration-projects/" class="listtitle"&gt;How To Set Up Drupal 7 Docker Containers for Migration Projects&lt;/a&gt;&lt;/h4&gt;
                    &lt;div class="mb-2"&gt;
                        &lt;span&gt;&lt;time class="listdate" datetime="2024-09-09T13:25:15Z" title="09 September 2024"&gt;09 September 2024&lt;/time&gt;&lt;/span&gt;
                    &lt;/div&gt;

                        &lt;p class="card-text flex-grow-1"&gt;Learn how Docker is a valuable tool for Drupal 7 end of life migrations. In this post, I'll give a step-by-step guide to setting up a Drupal 7 container for your migration project.&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

&lt;/section&gt;</description><category>AI</category><category>Confidentiality</category><category>LLM</category><category>Operations</category><category>Privacy</category><category>Security</category><category>Workflow</category><guid>https://anothercoffee.net/secure-your-ai-workflow-using-local-tokenisation/</guid><pubDate>Tue, 12 Nov 2024 13:59:03 GMT</pubDate></item><item><title>Safeguard your email address by registering a domain</title><link>https://anothercoffee.net/safeguard-your-email-address/</link><dc:creator>Anthony Lopez-Vito</dc:creator><description>A primary email address tied to your email provider could set you up for a great deal of inconvenience if they shut down. Registering your own domain helps control your email regardless of which company you're currently using.

On Thursday, 8th August 2013, a secure email service provider called &lt;a href="http://lavabit.com"&gt;Lavabit&lt;/a&gt; suddenly &lt;a href="http://www.theguardian.com/technology/2013/aug/08/lavabit-email-shut-down-edward-snowden"&gt;suspended operations&lt;/a&gt;. Its founder, Ladar Levison, wrote in an open letter on the company's website that he would rather shut the company down than &lt;em&gt;"become complicit in crimes against the American people."&lt;/em&gt; Although Mr Levison took what he believed to be a principled stand, Lavabit customers were understandably &lt;a href="http://www.emaildiscussions.com/showthread.php?t=66968&amp;amp;page=2"&gt;angry at being blocked&lt;/a&gt; from accessing their emails. Without warning, long-time customers lost years worth of archived messages. Active users who relied on the company to host their primary email now face the inconvenience of updating their contacts and online accounts with a new address.

One may be tempted to think that a simple solution would be just to set up another email account elsewhere. After all, there are many free email providers offering reliable services. If you're in this camp, ask yourself how your day-to-day life will be affected if you suddenly and unexpectedly lose access to your email account.
&lt;ul style="font-style: italic;"&gt;
 	&lt;li&gt;Do you conduct business over email? How much productivity will be lost re-establishing communication with clients?&lt;/li&gt;
 	&lt;li&gt;Have you saved passwords, document attachments and important account information in your webmail folders? What happens if you can't log in to the webmail account?&lt;/li&gt;
 	&lt;li&gt;How much time will it take to inform all your relatives, friends and contacts of your new email address, especially if your address book was also hosted with the lost email service?&lt;/li&gt;
 	&lt;li&gt;How easy is it to reset the passwords of your other online accounts (internet banking, Facebook, Skype, etc.) without that lost email address?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Keeping control of your email address&lt;/h2&gt;
There are some important lessons we can learn from the Lavabit incident and two things can save you from similar trouble:
&lt;ol&gt;
 	&lt;li&gt;&lt;strong&gt;Register your own domain&lt;/strong&gt; and link it to your email provider. That way, you can switch providers while retaining the same email address.&lt;/li&gt;
 	&lt;li&gt;Do not rely on webmail as your only method of accessing your messages. Set-up an email client on your computer and &lt;strong&gt;regularly download copies of your email&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
Exactly how you go about using your own domain and downloading emails depends on your existing set-up and requirements. I'll give a quick overview in this post but please note that it only briefly touches on some steps which can be quite technical.

&lt;a name="setup_guide"&gt;&lt;/a&gt;
&lt;h2&gt;Step 1: Register your own domain&lt;/h2&gt;
An email address under your own domain keeps it independent of the email host. Your current email provider may go out of business, get bought-out or become unreliable but having your own domain means that you can switch to another while retaining the same email address.

To get an email address under your own domain, you first need to register a name with a &lt;em&gt;domain name registrar&lt;/em&gt;. (See &lt;a title="What do I need to set up a website?" href="http://192.168.2.30:8100/what-do-i-need-to-set-up-a-website/"&gt;this post&lt;/a&gt; for more information.)

You can register your domain with the following companies but a web search for "domain registration" will bring up a list of other providers:
&lt;ul&gt;
 	&lt;li&gt;&lt;strong&gt;&lt;a href="https://anothercoffee.net/#contact"&gt;Another Cup of Coffee Limited&lt;/a&gt;&lt;/strong&gt; - we'll handle the details of domain registration under your name for £9.99 GBP per year&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;&lt;a href="http://www.123-reg.co.uk"&gt;123-reg.co.uk&lt;/a&gt;&lt;/strong&gt; - a popular UK-based registrar and hosting company&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;&lt;a href="http://www.namecheap.com"&gt;namecheap&lt;/a&gt;&lt;/strong&gt; - a US-based registrar that seems to have a good reputation for customer service (I personally haven't used them)&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;&lt;a href="http://networksolutions.com"&gt;Network Solutions&lt;/a&gt;&lt;/strong&gt; - one of the oldest and well-known registrars but quite expensive&lt;/li&gt;
&lt;/ul&gt;
Regardless of which domain registrar you choose, the whole process should only take a few minutes to complete. However, depending on their system, it could take a few hours to a day or more before it's available for use.
&lt;h2&gt;Step 2: Link your domain to your email provider&lt;/h2&gt;
Linking your domain to an email provider can be intimidating for non-technical people. To make matters more complicated, some end up with different combinations of registrar, free web-based email, business email hosting, and web hosting. Everything can be under one roof or you may have different companies handling each component. The exact steps needed will depend on your subscription packages so covering them in a short tutorial is not practical. (That's why companies like us exist!)

In general, your registrar will give you an online control panel. This lets you specify settings to hand over control of the domain's email to an external email provider. Alternatively, it may offer an &lt;em&gt;email forwarding&lt;/em&gt; service that automatically redirects messages to another address, such as Gmail or Yahoo Mail.

Changing email providers then becomes a matter of adjusting the control panel to reflect the new company's settings.

Here are some help pages for a few of the popular email providers:
&lt;ul&gt;
 	&lt;li&gt;&lt;strong&gt;&lt;a href="http://help.yahoo.com/l/aa/yahoo/mail/original/personaladdress/personaladdress-08.html"&gt;Yahoo Mail&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;&lt;a href="http://lifehacker.com/5391537/trick-out-google-apps-for-your-domain"&gt;Gmail&lt;/a&gt;&lt;/strong&gt; via Google Apps&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;&lt;a href="http://www.howto-outlook.com/howto/outlookcompersonaladdress.htm"&gt;outlook.com&lt;/a&gt;&lt;/strong&gt; (formerly Hotmail)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 3: Download backups of your emails&lt;/h2&gt;
For many people, their main method of checking and sending email is through their provider's webmail interface. It's very convenient because there are no programs to set up on your computer. All that's needed is to open up a web browser and log in. The downside is that you do not retain any copies of your messages. As some of the Lavabit customers found, you will lose everything if the provider suddenly ceases operations.

The solution is to set up an email program (also known as an email &lt;em&gt;client&lt;/em&gt;), like Mac Mail, Microsoft Outlook or Mozilla Thunderbird to download emails from your server. Even if you prefer webmail, periodically connecting from your email client ensures that you save the latest messages on your computer's hard-drive.

Most email providers offer you a choice of &lt;em&gt;'POP'&lt;/em&gt; or &lt;em&gt;'IMAP'&lt;/em&gt; as mechanisms for retrieving your email. POP will simply download all the messages and if set in your email client, delete the messages after they're read. IMAP synchronizes your email client with the server so it copies the same structure of read, unread, sent messages and saved folders. (&lt;a title="Rackspace: POP vs. IMAP" href="https://web.archive.org/web/20150905100646/http://www.rackspace.com/apps/support/portal/4012"&gt;This Rackspace article&lt;/a&gt; gives more detail on the difference between the two.) I find IMAP to be the most convenient option. If you mostly use webmail, you should also use IMAP if it's available.
&lt;h2&gt;Too much trouble?&lt;/h2&gt;
These steps might seem daunting but you don't need to be a computer expert to get everything set-up. Business users usually have more complex configurations that may need an IT administrator to get everything working properly. However, for personal users and micro-businesses with simple needs, a little bit or research and background reading should allow you to get the job done without any help.

Of course, if you'd rather not go to the trouble of doing this yourself, we'll be very happy provide you with &lt;a href="https://anothercoffee.net/#contact"&gt;a quotation&lt;/a&gt;. This is not a big budget job as the whole process is fairly quick for those familiar with what's required.
&lt;h2&gt;Some background on the Lavabit incident&lt;/h2&gt;
I'll make a slight digression from technical matters as the Lavabit incident may have wider implications for anyone using US-based internet services.

Lavabit offered encrypted email services and was &lt;a href="http://www.theguardian.com/commentisfree/2013/aug/09/lavabit-shutdown-snowden-silicon-valley"&gt;reported in the press&lt;/a&gt; to have been used by the NSA whistleblower &lt;a href="http://www.theguardian.com/world/edward-snowden"&gt;Edward Snowden&lt;/a&gt;. Unlike most email systems, the company's technology meant that there was no way for them to directly read user emails. While we may never know the truth, it seems likely they were ordered to &lt;a title="HOW THE GOVERNMENT KILLED A SECURE E-MAIL COMPANY" href="http://www.newyorker.com/online/blogs/elements/2013/08/the-government-versus-your-secrets.html"&gt;participate in ongoing surveillance&lt;/a&gt; in a form that the founder believed to be against the United States Constitution. Levison was issued with a 'gag order' preventing him from giving details on the matter. Shortly after the Lavabit news broke, Silent Circle, another secure email provider, &lt;a title="Silent Circle blog announcement" href="http://silentcircle.wordpress.com/2013/08/09/to-our-customers/"&gt;pre-emptively shut down&lt;/a&gt; its own service in order to protect its customers.

There is increasing &lt;a title="First Lavabit, now Silent Circle: Another U.S. secure email service closes to protect users" href="http://gigaom.com/2013/08/09/another-u-s-secure-email-service-shuts-down-to-protect-customers-from-authorities/"&gt;industry speculation&lt;/a&gt; that the US government's surveillance is &lt;a href="http://techcrunch.com/2013/08/08/silent-circle-preemptively-shuts-down-encrypted-email-service-to-prevent-nsa-spying/"&gt;jeopardizing the country's businesses&lt;/a&gt; since they can no longer be trusted to protect their users' privacy.

It's clear that no matter which country you're in, if your email is hosted with a US provider, you need to assume that the US government will want (or already has) backdoor access to them. Whether or not this is acceptable is a discussion outside the scope of this post. Regardless of where you stand, it's important to realize that the industry landscape is changing and we can no longer be complacent about safeguarding our data.</description><category>Backups</category><category>Email</category><category>Emergency</category><category>Laws</category><category>Privacy</category><category>Security</category><guid>https://anothercoffee.net/safeguard-your-email-address/</guid><pubDate>Sat, 10 Aug 2013 13:51:07 GMT</pubDate></item></channel></rss>