The promise of agentic coding is compelling: describe a task, walk away, come back to a pull request. But giving an AI agent unrestricted access to your machine to make that happen is a different proposition entirely. You want the autonomy without the risk.

The solution is a sandbox. A container where Claude Code can do whatever it needs, install packages, run commands, make commits, delete files, while your actual machine stays untouched. This is how we built one on macOS, and the unexpected problem we had to solve along the way.

Why not Docker Desktop?

Docker Desktop is the obvious starting point for Docker on macOS. But it requires a paid license for commercial use, runs a persistent background service you didn't ask for, and wraps everything in a GUI that gets in the way if you live in the terminal.

Colima is the alternative. It's open source, installs with brew install colima, and exposes the exact same Docker socket that Docker Desktop does. Every Docker CLI command, every docker compose workflow, every tool that talks to the Docker daemon, they all work identically. You just don't pay for it and it doesn't run a menubar app.

The sandbox image

The container image is straightforward. A Debian-slim Node base for Claude Code, plus the tools an autonomous agent needs: git, the GitHub CLI for opening pull requests, standard utilities, and socat for a reason we'll get to.

FROM node:20-slim

RUN apt-get update && apt-get install -y \
    git curl wget ca-certificates bash zsh \
    jq ripgrep fzf sudo socat \
    && rm -rf /var/lib/apt/lists/*

# GitHub CLI
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
    | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] \
    https://cli.github.com/packages stable main" \
    | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \
    apt-get update && apt-get install -y gh && \
    rm -rf /var/lib/apt/lists/*

RUN npm install -g @anthropic-ai/claude-code

RUN mkdir -p /workspace /mnt/skills /mnt/commands

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /workspace
ENV SHELL=/bin/zsh

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["zsh"]

The entrypoint.sh runs at container startup and configures git identity, verifies tokens, and writes runtime config from environment variables. Secrets never get baked into the image, they come in through a .env file at runtime.

A shell function handles the full lifecycle:

claude-sandbox() {
  local project_path="${1:-$PWD}"
  local project_name
  project_name="$(basename "$project_path" | tr '[:upper:]' '[:lower:]')"

  docker run --rm -it \
    --name "claude-$project_name" \
    --env-file ~/Development/Claude/sandbox/.env \
    -e GIT_USER_NAME="${GIT_USER_NAME}" \
    -e GIT_USER_EMAIL="${GIT_USER_EMAIL}" \
    -v "$project_path":/workspace \
    -v ~/some-path-to-your/skills:/root/.claude/skills:ro \
    -v ~/some-path-to-your/commands:/root/.claude/commands:ro \
    -v ~/.ssh:/root/.ssh:ro \
    --add-host host.docker.internal:host-gateway \
    claude-sandbox:latest claude
}

Run claude-sandbox ~/some-path-to-your/project and you're in an isolated Claude Code session scoped to that project. Skills and commands from your host are available read-only. SSH keys work for pushing. The container is gone when you exit.

The Xcode MCP problem

For Swift and iOS development, you want Claude Code to be able to build and run tests. Apple provides xcrun mcpbridge, a stdio MCP server that bridges Claude Code directly to Xcode's tool service. Build a target, run the test suite, inspect the project structure, all from Claude Code without leaving the terminal.

The problem: xcrun mcpbridge is macOS-only. It talks to a running Xcode instance over a local socket. The sandbox is a Linux container. There's no way to run Xcode in there.

The obvious solution is to run xcrun mcpbridge on the host and expose it over HTTP so the container can reach it through host.docker.internal. Tools like mcp-proxy wrap any stdio MCP server in an HTTP/SSE interface.

It didn't work, not because of connectivity, but because of how Claude Code handles authentication. Claude Code treats any HTTP or SSE MCP server as a remote server and requires OAuth authentication before connecting, even when the server is running on localhost. There's no way to bypass this for HTTP transport.

Stdio transport has no such requirement. Claude Code connects to stdio MCP servers by spawning a local process and piping to its stdin/stdout. No authentication, no OAuth, no ceremony.

So the question becomes: can we make a stdio connection reach across the container boundary?

The socat bridge

Yes, with socat.

On the macOS host, socat listens on a TCP port. For each incoming connection, it forks a new xcrun mcpbridge process and bridges that process's stdin/stdout to the socket:

socat TCP-LISTEN:3001,fork,reuseaddr EXEC:"xcrun mcpbridge"

Inside the container, Claude Code connects to the MCP server using stdio transport. The "command" it runs is socat connecting back to the host:

{
  "mcpServers": {
    "xcode": {
      "type": "stdio",
      "command": "socat",
      "args": ["TCP:host.docker.internal:3001", "-"]
    }
  }
}

From Claude Code's perspective, it spawned a local process and is piping to it. No network authentication required. The container has socat installed. The host has socat listening. host.docker.internal routes to the Mac through Colima's virtual network.

The sandbox function starts the bridge before launching the container:

lsof -ti :3001 | xargs kill -9 2>/dev/null || true
socat TCP-LISTEN:3001,fork,reuseaddr EXEC:"xcrun mcpbridge" \
  > /tmp/mcp-bridge.log 2>&1 &
echo $! > /tmp/claude-sandbox-mcp-bridge.pid

And the entrypoint.sh writes the .mcp.json to the workspace on every start:

cat > /workspace/.mcp.json << EOF
{
  "mcpServers": {
    "xcode": {
      "type": "stdio",
      "command": "socat",
      "args": ["TCP:${XCODE_MCP_HOST:-host.docker.internal}:${XCODE_MCP_PORT:-3001}", "-"]
    }
  }
}
EOF

After allowing the connection in Xcode once, /mcp in Claude Code shows xcode · ✔ connected with all 20 Xcode tools available.

Permissions without interruptions

The last piece is making the sandbox actually autonomous. By default, Claude Code asks for confirmation before every tool call. That defeats the point.

Claude Code reads permissions from settings.json, not from CLAUDE.md (which is for instructions and context, not configuration). The entrypoint.sh writes a settings.json on startup:

cat > /root/.claude/settings.json << 'EOF'
{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(gh *)",
      "Bash(npm *)",
      "Bash(swift *)",
      "Bash(xcodebuild *)",
      "Bash(mkdir *)", "Bash(mv *)", "Bash(cp *)",
      "Bash(touch *)", "Bash(cat *)", "Bash(ls *)",
      "Bash(curl *)", "Bash(claude *)",
      "Read(*)", "Write(*)", "Edit(*)",
      "WebFetch(domain:api.github.com)",
      "WebFetch(domain:github.com)"
    ]
  }
}
EOF

Git operations, GitHub CLI, file reads and writes, web fetches to GitHub, all allowed without prompting. Destructive operations outside /workspace, system file modifications, still require confirmation. The container is the security boundary; the permissions reflect that.

The result

Running claude-sandbox ~/some-path-to-my/ios-app gives you a fully isolated Claude Code session that can read and write Swift source files, make commits, open pull requests, and trigger Xcode builds and test runs, all without security prompts interrupting autonomous operation.

The Xcode MCP bridge was the unexpected hard part. The intuitive solution (HTTP proxy) fails because of how Claude Code handles remote server authentication. The actual solution (socat stdio tunnel) works because it makes a networked process look like a local one.

All the config lives in a single git repo at ~/some-path-to-my/Claude/sandbox/. No Docker Desktop, no OAuth flows for local tools.

The sandbox is where Claude Code runs, your Mac is where Xcode runs and with Socat connecting them.