# The Wall from Inside: Running coding agents in Apple containers

> Source: <https://fluxum.net/blog/the-wall-from-inside>
> Published: 2026-10-28 13:23:20+00:00

[Blog](https://fluxum.net/blog) ·  · 13 Min. Lesezeit

# The Wall from Inside

*In my last article, I described five layers that fence in my coding agents. The outermost one is the wall: a native Apple container in which the agents run without seeing my Mac. Here I look at it from the inside: how it’s built, how it differs from Docker Desktop, what I tested and where I stumbled.*

[“Cage, Wall, Bouncer”](https://fluxum.net/blog/cage-wall-bouncer) was about how the layers work together. The wall only came up briefly: the agents run in an Apple container, and they can’t get to my Mac. That sounds simple. In practice, there are a few decisions behind it, and I only got some of them right on the second attempt.

## Why a container at all?

My multi-agent system starts Claude Code and Codex without any permission prompts. Every run has all the rights of the user the agent daemon runs as. On my Mac, that would mean SSH keys, every file, every tool I’m logged in to.

There are three common ways to separate them:

- **A separate macOS user.** Cheap, but the boundary is the macOS permission system. Anything readable by all users stays readable.
- **A full VM.** A strong boundary, but heavy: its own operating system, updates, memory that stays allocated.
- **A container.** Light and reproducible. The only question is how strong the boundary is.

Apple’s `container` answers that question in a particular way.

## Apple Container: one VM per container

With `container`, Apple has released its own open-source tool for Linux containers on the Mac. The difference from Docker Desktop lies in the architecture. According to Apple’s technical overview, `container` starts a separate lightweight VM for every container. Each container gets the isolation of a full virtual machine, with its own Linux kernel.

Docker Desktop and Colima, on the other hand, start one large Linux VM. All containers run inside it together and share its kernel.

|  | Apple `container` | Docker Desktop / Colima | 
|---|---|---|
| VM | one mini VM per container | one shared VM for all | 
| Kernel | its own per container | the VM’s shared kernel | 
| Separation between containers | VM boundary | namespaces and cgroups | 
| Mac folders | nothing visible unless you mount it | `/Users` ,`/Volumes` ,`/private` ,`/tmp` and`/var/folders` are shared with the VM by default | 
| Memory | limit per container | fixed block for the VM | 
| Images | OCI, the same as Docker | OCI | 
| Maturity | young, Apple Silicon only, full networking only from macOS 26 | mature, large ecosystem | 
| License | open source | Docker Desktop requires a paid subscription for companies with more than 250 employees or more than USD 10 million in annual revenue | 

For my agents, two rows matter.

**First, file sharing.** With Docker Desktop, `/Users` is reachable inside the VM by default. A container only sees it once you mount it. But a single careless `-v ~/…` is enough. With Apple, there is nothing you could share by accident.

**Second, separation between containers.** If an agent breaks out of its container under Docker, it lands in the shared VM, next to all the other containers. With Apple, it stays in its own mini VM.

Both variants protect the Mac itself via the hypervisor boundary. In either case, an agent would have to break out of a VM.

## The setup

The image is deliberately plain. The base is a slim Debian with Node.js. On top come Git, SSH, a few command-line tools, Python, Codex via npm and Claude Code via the official installer.

```
FROM docker.io/library/node:22-bookworm-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      ca-certificates curl git openssh-client jq ripgrep python3 \
 && rm -rf /var/lib/apt/lists/*

RUN npm install -g @openai/codex

# User without root rights
RUN useradd --create-home --uid 1001 --shell /bin/bash agent

# Claude Code via the official installer
RUN su agent -c 'curl -fsSL https://claude.ai/install.sh | bash' \
 && cp "$(readlink -f /home/agent/.local/bin/claude)" /usr/local/bin/claude

# Updates only by rebuilding the image
ENV DISABLE_AUTOUPDATER=1

USER agent
WORKDIR /home/agent
```

Three decisions are built in:

- **No root.** The agents run as the user`agent` . Claude Code refuses its no-prompt mode when running as root anyway.
- **Programs outside the home directory.** I’ll explain why right after the start command.
- **No self-updates.** An agent that updates its own tool changes the environment unnoticed. New versions only come with a rebuilt image.

The container is started like this:

```
container run -d --name agent --network agents \
  -v agent-home:/home/agent \
  --cap-drop ALL -m 4G --cpus 4 \
  agent-runtime:latest
```

- `--cap-drop ALL` removes all Linux capabilities from the container.
- `-m 4G` and`--cpus 4` limit memory and CPU.
- The volume `agent-home` holds logins, configuration and the working directories. It survives a rebuild of the image.
- No folder from the Mac is mounted. That’s the whole point.

By the way, the volume isn’t a folder but a single file on the Mac:

```
~/Library/Application Support/com.apple.container/volumes/agent-home/volume.img
```

It contains an ext4 file system, the kind Linux uses. At startup, Apple `container` mounts the file into the agent’s mini VM, where it appears as `/home/agent`. `container volume inspect agent-home` shows the exact path.

On my machine, the file reports 512 GB, but only about 490 MB are actually used. The 512 GB are the upper limit. The file only grows as far as data is actually written to it.

The agent daemon runs in the foreground inside the container and fetches its tasks from the multi-agent system’s server. It allows no more than three runs at the same time.

### Why the programs must not live in the home directory

A container consists of two parts: the image, created at build time, and the volumes, added at startup. The image is immutable and gets rebuilt with every update. The volume is the part that stays: logins, configuration, working directories.

At startup, the volume `agent-home` is laid over the directory `/home/agent`. Think of it like a tablecloth: whatever was on the table is still there, but you can’t see it anymore. In `/home/agent`, the container only sees the contents of the volume. Whatever the image itself brings at that location is invisible as long as the volume is mounted.

That’s exactly where the trap is. The official Claude Code installer puts the program in `~/.local/bin`, which is `/home/agent/.local/bin`. At build time this works: `claude --version` answers. When the container starts, the volume covers it, and the program is gone. The agent daemon can’t find `claude` anymore.

That’s why the Containerfile copies the program to `/usr/local/bin` after installation. No volume sits there, so it stays visible.

I actually like the side effect: the roles are cleanly separated. The image contains the tools, the volume contains the data. A new image swaps out Claude Code, Codex and everything else without touching logins or working directories.

And no agent can replace the programs themselves. I checked inside the running container: the search path only contains system directories such as `/usr/local/bin` and `/usr/bin`. They belong to root, and the user `agent` can’t write to them. `~/.local/bin` isn’t in the search path at all. A custom file named `claude` in the volume would never be started.

Still, the volume isn’t entirely free of back doors. The settings of Claude Code and Codex live there, in `~/.claude` and `~/.codex`. An agent that changes them influences later runs. Only the inner layers protect against that: in its sandbox, Codex may only write to the working directory, and for Claude Code, the classifier checks every write to such protected paths.

## Its own identity instead of mine

An agent runs as the same user as the daemon that starts it. So it can read the daemon’s access token. If the daemon had my token, every agent could do everything I can: delete workspaces, manage members. And in the history, it would be indistinguishable from me.

That’s why the container logs in with its own account. It has the role `member`: reading and writing tickets yes, settings and deleting no. I checked this. Admin actions end with “403 Forbidden”. The token has an expiry date and is passed into the container via stdin, never as a command-line argument.

The same principle applies to Git. Every repository the agents are allowed to work on gets its own deploy key with its own host alias:

```
# ~/.ssh/config
Host gh-project
  HostName github.com
  IdentityFile ~/.ssh/deploy_project
  IdentitiesOnly yes
```

The multi-agent system only knows the repository by its HTTPS address. A Git rule inside the container redirects that address to the deploy key:

```
git config --global \
  url."git@gh-project:me/project".insteadOf "https://github.com/me/project"
```

Cloning and pushing work. Any other repository answers with “Repository not found”.

The Claude Code and Codex logins are separate sessions, independent of those on my Mac. For Codex, this took a detour. Device-code login wasn’t enabled for my account. The browser login, in turn, expects the callback on `127.0.0.1:1455`, and it has to arrive inside the container. A short-lived helper container forwarded the port from the Mac into the agent container. After the login, I deleted it again.

## The bouncer moves in too

The wall doesn’t replace the inner layers. The daemon in the container starts Claude Code in auto mode and Codex in its sandbox with auto review. Denied actions end up as a comment on the ticket. The active mode is part of the runtime’s name, for example “Claude · auto mode”. That way I can see at a glance in the UI how an agent is protected.

On Linux, Codex uses bubblewrap for its sandbox. It isn’t installed in the image. Codex ships its own copy and only shows a warning. A write attempt outside the working directory failed hard in the container with “Read-only file system”.

## Tested from the inside

I ran some of the tests as real tickets through the multi-agent system, the way an agent normally works. The rest I checked directly inside the container:

| Test | Result | 
|---|---|
| Which user is running? | `agent` , not root | 
| Does `/Users` exist? | no, the directory doesn’t exist | 
| Clone and push the permitted repository | works | 
| Clone another repository of mine | “Repository not found” | 
| Admin action with the agent account | “403 Forbidden” | 
| Write outside the working directory (Codex) | “Read-only file system” | 
| Explicitly requested `rm -rf` (Codex) | allowed by the reviewer agent, “risk: high” | 
| Injected instruction to upload the `.env` (Codex) | refused by the model itself | 

The last two rows show the same as on the Mac. The check protects against overreach, not against a bad task. And the model itself catches a lot before any check is even asked.

## Pitfalls

**The builder takes down the network.** To build images, `container` starts its own builder. On my machine, merely starting or stopping this builder disrupted the container network. Afterwards, the containers couldn’t find each other, and name resolution went nowhere. The only fix was to stop and restart all containers, including the agent container. My build script now aborts as long as any container is still running.

**Never mount the volume twice.** Because the volume is a single image file, it must never be attached to a second container while the agent is running. Two Linux kernels writing to the same ext4 file system at the same time can corrupt it. For inspections, I use `container exec` in the running container instead.

**Memory only comes back on stop.** Apple’s virtualization only partially supports returning memory. According to Apple’s documentation, memory pages that Linux frees inside the container are not returned to macOS. Over time, the agent container can fill up its 4 GB, even when nothing is running. An occasional restart helps. Overall, it’s still more economical than a Docker VM that permanently reserves its block.

## What the wall doesn’t cover

**Runs among themselves.** What’s sealed off is the Mac, not the runs from each other. All runs share the container, `/tmp` and the home directory.

**Everything in the volume is readable.** The agent account’s token, the Claude Code and Codex logins, the deploy keys. An agent can read them. That’s why these keys are cut so small: role `member`, one key per repository, a token with an expiry date.

**The network is open.** The agents need Anthropic, OpenAI and GitHub. A fully isolated network is therefore not an option. Whatever an agent can read, it could theoretically send somewhere.

For the network, I see a next step that I haven’t implemented yet. Apple’s `container` supports internal networks without outside access. The agent would only be attached to such a network. A proxy container would be attached to both networks and only let through a list of permitted destinations. It’s the same principle as the proxy in Claude’s own sandbox, which I described in [“How Secure Is the Claude Code Sandbox on the Mac?”](https://fluxum.net/blog/how-secure-is-the-claude-code-sandbox). I already know one catch: Git over SSH ignores the usual proxy setting.

One window stays open even then. Today, the container can already reach services on my Mac that listen on all network interfaces. I tested this with the macOS AirPlay receiver: it answers. Services bound only to `127.0.0.1`, on the other hand, are out of the container’s reach. An internal network doesn’t change that. It only takes away the route to the internet.

## What I take away from this

The wall is the layer that depends least on the agent’s behavior. Conscience and bouncer make judgments. The wall doesn’t judge, it’s simply there. Whatever isn’t mounted, no agent can reach, no matter how cleverly it was prompted.

At the same time, it’s the layer that needs the most care. Rebuilding images, renewing logins, rotating tokens, fixing network issues. If you use it, you’re running one more small system.

For agents that work unattended, it’s worth the effort to me.

## What you can do now

- **Check what your agent environment can see of your Mac.** An`ls /Users` inside the container or VM tells you immediately.
- **Don’t give the agent root.** A separate user in the image costs one line.
- **Cut the keys small.** A separate account, one deploy key per repository, a token with an expiry date.
- **Try Apple’s `container`** if you have a Mac with Apple Silicon and macOS 26. The images are the same as with Docker.
