I do almost all of my development inside a devcontainer.
Visual Studio Code opens a container, and I run Claude Code inside it.
When I had Claude Code make the demo GIF for my README, the whole recording happened inside the container too.
I started using devcontainers long before coding agents came along.
My reason for using them has changed since then.
Now it is that I can decide how far an agent reaches, one container and one volume at a time.
This post describes where I draw those lines.
How I got started with devcontainers will be a separate post.
Summary
- Keeping an agent's reach inside a container lets me use auto mode without worrying. The agent is cut off from my browser sessions and API keys, and experimental tools never land on the host.
- Named volumes let me separate the environment that reads from the one that writes. In the OpenSpec setup I'm trying on my own, the spec container reads the code under development, and the development containers read the specs.
- I don't recommend devcontainers to everyone. They can be tuned in fine detail, so there is a lot to learn. Fixing the configuration, though, is something you can hand to an agent.
Keep secrets and experiments out of the agent's reach
I have three reasons for keeping the agent inside a container.
The first is to make it easy to try development tools.
Tools around coding agents come and go quickly, and I often want to try something experimental.
I'd rather not install those tools on my Mac.
If they live in a container and don't work out, I can just rebuild the container.
The second is to keep secrets out of reach.
My browser on the host has plenty of logged-in sessions, and the host also holds various API keys.
This is also why I chose Playwright inside the container over computer use when making the demo GIF.
The third is accidental file deletion.
I hear about it less these days, but I did hear several stories of agents deleting files.
If only the files inside the container are in play, the damage stays inside that container.
With all three in place, I can use auto mode without worrying.
Even when the agent asks me for confirmation less often, I still know what it can touch.
There is one hole I leave open on purpose.
Claude Code's configuration directory (~/.claude) is bind-mounted from the host.
That way the container can use the authentication I completed on the host, and I can look back across the sessions of every project later.
Use named volumes to decide who reads and who writes
Once environments live in separate containers, some things need to be shared between them.
I connect them with named volumes.
In repositories that use compose, devcontainer.json points at a compose file through dockerComposeFile, and that file refers to a network and volumes created outside the project.
By default, Docker Compose creates a project and a network for each compose file.
Bringing a whole project up or down is convenient, but you can't rebuild just one service.
So I manage networks and volumes separately from the projects.
The simplest thing to share is credentials.
In this blog's repository, the GitHub CLI and dev.to credentials live in named volumes.
"mounts": [
"source=gh-config,target=/home/node/.config/gh,type=volume",
"source=devto-config,target=/home/node/.config/devto,type=volume"
]
The volumes outlive the container, so rebuilding it doesn't mean logging in again.
At the same time, the credentials stay off my Mac and out of the repository.
The source code itself lives in a named volume too.
Bind mounts on a Mac are slow.
With the code in a volume, another container can see the same code just by mounting that volume.
Example: a spec container that only reads the code
On my own, I'm trying out OpenSpec.
OpenSpec is a tool for writing the spec before the code, and it accumulates change proposals (called changes) in a spec repository.
Writing a change requires reading the existing source code.
I also wanted the agent to read the code as it currently stands in development.
So I mount the volume that holds the development container's source code into the spec container as well.
The agent in the spec container doesn't get to rewrite that source code.
In the spec container's Claude Code settings (settings.json), the source code volume is not listed in additionalDirectories.
The agent that writes specs only reads the code.
There is also a mount in the other direction.
The development containers mount the spec volume.
That lets the development side check how the specs are progressing.
The backend and the frontend are developed independently in their own containers, and both see the same specs.
| Container | Mounts | What the agent writes |
|---|---|---|
| Spec | The spec volume and the volume with the code under development | Specs only |
| Backend development | The backend source code and the spec volume | Backend code |
| Frontend development | The frontend source code and the spec volume | Frontend code |
Containers decide what an agent can touch, and volumes decide which environment reads and which one writes.
With the read and write directions set per container, I can run several agents side by side without them trampling each other's workspaces.
Keep the production image minimal and bring your own tools
There is also a line around what goes into the container.
The image I ship to production should contain only the files the service needs to run.
A development environment, on the other hand, needs room for whatever tools help me develop.
So I build on the production Dockerfile and add development tools with devcontainer features and dotfiles.
I manage dotfiles with yadm and tool versions with mise.
One reason I bring my dotfiles into the container is worktrees.
Developing with agents means using a lot of worktrees.
For reopening and switching between them, I want the tools my hands already know, such as fzf.
The work may happen inside a container, but the environment I operate by hand should feel the same as always.
Devcontainers are not for everyone
Devcontainers have their annoyances.
Changing devcontainer.json means rebuilding the container.
Every small tweak means waiting for a rebuild, so I'm careful when I edit it.
Disk usage doesn't bother me.
They also limit your choice of editor.
Newer IDEs built for AI agents, such as Orca, are awkward to combine with devcontainers.
That said, the Claude Code extension for Visual Studio Code is improving quickly too.
Would I recommend devcontainers to everyone? No.
They can be tuned in fine detail, which makes them hard to use casually, and beginners have a lot to learn.
Still, if you ask Claude Code or Codex to fix your devcontainer configuration, they do a good job.
If you want your development environment as code that you can reproduce, I do recommend them.
Takeaways
I keep using devcontainers because they let me decide, with containers and volumes, how much I hand to an agent.
Inside a container, the agent can work in auto mode while my secrets and experimental tools stay separated from the host.
Connected by named volumes, environments can share the same code and specs while keeping the reader and the writer apart.
The OpenSpec setup is still an experiment.
For a while, I'll watch how splitting the spec and development containers changes the way I write and work through changes.
Top comments (0)