Agentic Engineering Foundations · Part 1

Sandboxing AI Agents with Dev Containers

Explore devcontainers as a means to protect yourself from losing valuable data, whilst working with AI coding agents

Updated 7 min read

Series · part 1 of 3Agentic Engineering FoundationsAll parts
  1. Sandboxing AI Agents with Dev Containers
  2. Spec-Driven Development (SDD) for AI Coding Agents
  3. Tips for writing efficient skills for LLM agents

The agent reads everything

A coding agent reads whatever lands in its context: your code and docs, but also the READMEs and comments in your dependencies, issues and pull requests, and the web pages it fetches. Anyone who can write to one of those can leave instructions for it. The agent can’t always tell yours apart from planted ones, and it runs shell commands with your privileges.

The agents’ own guardrails have come a long way, and stray rm -rf ~/ runs are much rarer than they were. Prompt injection is what still worries me: it’s someone else deciding what the agent does on my machine. Run a swarm of agents and that’s several pairs of hands on the keyboard, any of which can be talked into it.

So I keep mine in a box. This article is about sandboxing AI coding agents in a dev container, a Docker container that holds the agent and the project’s tools and sees only what you mount into it. It also covers getting the agent’s notifications out of the box without opening it back up.

A box per project, a box per agent

This article is about the first kind of box: local development with a frontier agent like Claude Code or Codex, or an open-source harness like OpenCode running against your own models. Every new project starts with a .devcontainer. I build from scratch as a monolith and split code out only when scalability demands it, so one container runs all of the project’s applications, plus whatever the agent needs to run steadily.

The container keeps the agent away from my machine. Version control can bring back deleted code, and I can live with losing a commit. The rest of my system has no such backup, and outside a container, the agent runs shell commands with my user’s privileges. That’s not something to merely be careful about: it’s an open door for malicious instructions and command injection.

With the box in place, I set up the agent’s instructions and slash-commands, which part 2 of this series covers.

A swarm needs the second kind: a box per agent. If you self-host agents in an orchestration, one shared dev container is the wrong tool. These open-source projects manage a box per agent, on infrastructure you run:

  • Daytona(opens in new tab) - infrastructure for running AI-generated code in isolated sandboxes, with fast cold starts and no session time limit
  • Coder(opens in new tab) - self-hosted environments for developers and their agents
  • OpenSandbox(opens in new tab) - a sandbox control plane with Docker and Kubernetes backends, network policy, a credential broker and optional gVisor, Kata or Firecracker isolation
  • E2B(opens in new tab) - open-source sandboxes for agents, which you can self-host
  • Cube Sandbox(opens in new tab) - Tencent Cloud’s open-sourced sandbox service: KVM microVMs with their own kernel that start in under 60ms, compatible with the E2B SDK

Why a dev container

Dev containers are a well-trodden path: they make onboarding new team members easier, simplify setting up CI pipelines, and put an end to “it works on my machine”. There’s an open-source dev container toolchain(opens in new tab) that integrates almost natively with VS Code.

My reason is simpler and more serious. A Claude Code user on Reddit once lost their entire home directory(opens in new tab) when the agent ran rm -rf tests/ patches/ plan/ ~/ during a supposedly controlled code cleanup. Guardrails have improved a lot since, but the same command can still arrive through a prompt injection, and a container keeps it away from your files.

What a good dev container needs

You don’t need my Dockerfile: any coding agent can generate one for your stack with little help. What matters is what goes into it. These are the features I wouldn’t run a dev container without:

  • The agent lives inside. Install it in the image (@anthropic-ai/claude-code, @openai/codex or opencode-ai from npm), so every command it runs starts in the box. To pick up the last conversation:

    sh
    # Claude Code: continue, or start a new conversation when there's none
    docker compose -f .devcontainer/docker-compose.yml exec devcontainer sh -lc 'claude -c || claude'
    # Codex
    docker compose -f .devcontainer/docker-compose.yml exec devcontainer sh -lc 'codex resume --last'
    # OpenCode
    docker compose -f .devcontainer/docker-compose.yml exec devcontainer sh -lc 'opencode --continue'
  • Everything the project needs. The runtime, the package manager, git, and netcat for notifications. If the agent has to leave the box to build or test, the box has a hole.

  • A home that survives rebuilds. Mount a folder from the repository as the container user’s home. Auth sessions outlive an image rebuild, and dotfiles such as the agent’s settings and hooks (e.g. .claude/settings.json) and .zshrc with your aliases stay versioned with the project. Whitelist those files in .gitignore and ignore the rest, so sessions and tokens never get committed. A clean rebuild keeps you signed in:

    sh
    docker compose -f .devcontainer/docker-compose.yml build --no-cache
  • A container that stays up. A long-running default command, like sleep infinity, keeps it alive, so you can open shells and agent sessions into it whenever you need them:

    sh
    docker compose -f .devcontainer/docker-compose.yml up -d
    docker compose -f .devcontainer/docker-compose.yml exec devcontainer zsh
  • A shell you enjoy. I add zsh with oh-my-zsh(opens in new tab), since I spend a lot of time in there.

  • Nothing you wouldn’t hand the agent. No personal git credentials, SSH keys or host folders beyond the project. sudo for the container user is fine for debugging, never for production containers.

I run mine with plain docker-compose.yml rather than the devcontainer CLI(opens in new tab): forwarding ports and reaching other services on my Docker network was simpler, and I start the container from the terminal, not the editor. If you’d rather work from VS Code or a JetBrains IDE, both integrate with devcontainer.json.

For a hardened starting point, look at Anthropic’s reference dev container(opens in new tab) for Claude Code. Its firewall limits outbound traffic to the domains its script allows. The same idea works for any agent: allow only what it needs, such as your model provider or your own model server, the package registries and your git host.

Setting up agent notifications

Working with an agent would require your attention once in a while to know when a job has completed or a question by the AI is pending an answer.

Since the docker environment doesn’t have a native way to propagate notifications downstream to the host machine (or at least to my knowledge), I use a bit more complex setup, which relies on:

  • Having a shell script that accepts messages via sockets and runs a command to show notifications on my docker host machine.
  • Exposing the script using socat on port 4545.
  • The agent’s hooks, to emit a message to host.docker.internal:4545 using netcat.

Propagating notifications

To propagate messages from the devcontainer downstream to my machine, I pipe messages to the notifier.sh script with SOcket CAT(opens in new tab) on port 4545.

shStart the listener
pgrep -f 'socat TCP-LISTEN:4545' >/dev/null || \
    ( \
        nohup socat TCP-LISTEN:4545,reuseaddr,fork \
        EXEC:.devcontainer/notifier.sh >/tmp/hostcmd.log 2>&1 & \
        echo '🔔 Notifier started.'
    )
shStop the listener
pkill -f 'socat TCP-LISTEN:4545' && \
    echo '🔕 Notifier stopped.'

notifier.sh

The script is a whitelist. It reads one line from the socket, takes the first word as the command and the rest as its argument, and runs only the commands it knows. Mine knows one: notify. Anything else, such as printf 'rm -rf ~' | nc host.docker.internal 4545, gets DENIED and is never run.

sh.devcontainer/notifier.sh
#!/bin/bash
read -r CMD ARG

case "$CMD" in
  notify)
    terminal-notifier -title "Agent" -message "$ARG" -open "iterm2://session"
    ;;
  *)
    echo "DENIED"
    ;;
esac

terminal-notifier(opens in new tab) shows a clickable notification titled “Agent”, and clicking it focuses iTerm. Focusing the exact tab would be better, but I use tmux, and its sessions outlive the tabs they started in, so a session ID can point to a tab that’s long gone.

Connecting the agent’s hooks

Every agent has a place to run a command when it finishes or needs you:

Whichever you run, the hook does the same thing: netcat(opens in new tab) sends a line to the notifier.sh script on port 4545. Here’s the Claude Code version:

json.devcontainer/.home/.claude/settings.json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "printf 'notify blog (waiting)\n' | nc host.docker.internal 4545"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "printf 'notify blog (done)\n' | nc host.docker.internal 4545"
          }
        ]
      }
    ]
  }
}

Run everything in the box

My container doesn’t match my Mac: a different OS, and sometimes a different CPU architecture and tool versions. Dependencies installed on one side often need rebuilding on the other, so pnpm install gives different results inside the container and out. Pick one side, and make it the container, because that’s where the agent works:

  • Install, build and test inside the container. That’s where the agent does it, so you never rebuild dependencies to run tests on your machine. The same goes for local dev servers and, where possible, CI.
  • Run your applications inside the container too. Serve them from 0.0.0.0 and forward their ports, so you can open them on localhost on the host. This matters most on an ARM machine running containers built for x86, or when the host and the container run different versions of tools like pnpm or mix. Either way, you and the agent run, build and test the same code.
  • Let agents push with their own, limited credentials, never your personal ssh key. Protect main from merges and keep agents away from other agents’ and people’s pull requests. An access orchestrator is a viable extra module on top.

Keep the box shut

The agent still reads everything, and not all of it was written for you. A dev container limits what a planted instruction can reach. Setting one up is almost effortless, and it pays off twice: the agent, CI and your team share one environment, and your machine stays out of reach.

Around this post

Series · part 1 of 3

Agentic Engineering Foundations

  1. 1Sandboxing AI Agents with Dev Containers
  2. 2Spec-Driven Development (SDD) for AI Coding Agents
  3. 3Tips for writing efficient skills for LLM agents

Links in this post · 16

Discussed on Hacker News

Post history

Comments