This README.md is written by a human for other humans.
This repo is a collection of scripts to run Claude Code and its MCP servers in isolated containers.
- Run Claude Code in a dedicated container by host directory
- Run MCP servers as long-lived singletons - Allows isolated credential access
- Supports Docker and Podman runtimes
Important Not tested on MacOS yet. Tested exclusively on Ubuntu 24.
The goal is to allow users to safely run Claude Code and its MCP servers.
Security
- Restrict Claude Code container access to a single directory of the host
- Run MCP servers in dedicated isolated containers so that the MCP credentials are only available to the tools needing them
- Run dangerous tools (e.g. requiring access to host D-BUS) in isolated containers to limit the host
Developper experience
- Use standard tooling (docker/podman compose) to easily modify the setup
- Use versioned Claude customizations (
CLAUDE.md, skill, MCP configurations) - Automatically install marketplaces and plugins on launch
Exhaustive Claude Code configuration
.claude/configuration directory- Plugins
- Custom hooks
- Custom skills
- Secure MCP integration
System requirements:
dockerwith Compose v2, orpodman4.7+ andpodman-compose
- Clone this repo anywhere, e.g.:
git clone <this-repo-url> ~/claude_devcontainer
- Add these two lines to your shell rc file (
~/.bashrc,~/.zshrc):# Update the export to match the clone path export CLAUDE_DEVCONTAINER_HOME="$HOME/claude_devcontainer" source "$CLAUDE_DEVCONTAINER_HOME/shell-init.sh" # If you want to use podman instead of docker export CLAUDE_DEVCONTAINER_ENGINE='podman'
- Open a new shell (or
sourceyour rc file). This defines theccc,ccc-compose, andccc-rebuildcommands.
Once you followed the setup:
- Navigate to any directory in your terminal
- Run
cccto open Claude CLI with the current directory mounted in the container- The first run requires to build the different Docker images which takes several minutes
After running ccc Docker (or Podman) should show several running containers:
devcontainer-mcp-*for the different MCP containersdevcontainer-claude-desktop-notificationIf using Docker on Linux<dirname>-<hash>-agent-1for each directory where you ranccc(<dirname>is the current directory's basename,<hash>disambiguates directories that share a basename)
In <dirname>-<hash>-agent-1 the current directory of the host is mounted to /workdir in the container.
To inspect/manage the singleton services without going through the full flow use the helper ccc-compose:
ccc-compose ps
ccc-compose logs -f
ccc-compose downThe setup supports both Podman and Docker to run the containers.
The selection for the engine is as follow:
- Use Docker if its installed and its deamon is reachable
- Else use Podman if installed.
This can be overriden by setting the environment variable CLAUDE_DEVCONTAINER_ENGINE to either docker or podman
The agent container's name is derived from the workspace directory's path. If you rename or move a directory you previously ran ccc in, the next ccc there creates a new container under a new name — the old one is left running/stopped under its old name. It holds no per-workspace state (Claude session data and glab auth live in shared volumes, not in the container), so it's safe to remove:
The agent container's name is derived from the workspace directory's path. After renaming or moving a directory ccc will create a new agent container.
That leaves a useless container with the old name. This container is safe to remove.
docker ps -a
docker compose -p <old-project-name> downTo use Claude Code as a cli you need to run ccc. This command creates the required containers and starts a new shell in the container running Claude Code.
Once the shell is open you need to run one of the following commands
claude # Start the claude cli
cc # Alias to `claude`
ccd # Alias to `claude --dangerously-skip-permissions`You can configure all the claude agents by editing the files in claude/. All the files in this directory are symlinked by provision.sh to the container when it is created. Modifying this directory requires to run ccc-rebuild to get the changes in the containers.
CLAUDE.mdis the user-scoped configuration Claude will always have in its context.settings.jsonClaude's settings file (permissions, hooks, env variables, ...)skills/contains the skills you createplugins.jsonThis is a custom file for this repository. Theprovision.shreads this file and runsclaude plugin marketplace add ...andclaude plugin install ... --scope userfor each entry so that Claude Code always has your configured plugins.mcp-servers.jsonThis is a custom file for this repository. Theprovision.shreads this file and inject it into the container's$HOME/.claude.jsonfile so that Claude Code always has your configured MCP servers.statusline-command.shThis is the script which controls what the command line displays in the Claude Code cli. It is linked by thestatusLinekey insettings.jsonnotifications/This is a custom script used to allow Claude Code cli sending Desktop notifications on Linux. See the dedicated section in this README
When updating the setup you need to run ccc-rebuild to get the configurations included in the repo.
Warning ccc-rebuild closes your current containers. When developing on this repo you might want to have two local clones:
- One powering your current agent
- One running the developments, you can override you aliases and env variables to point to this directory in terminals where you do the testing.
This avoids the workflow where you start your agent in the container -> You make it change the repository -> You run ccc-rebuild which kills your agent -> But the changes broke the build and now you can't prompt your agent to fix the problem.
This setup supports two different types of MCP servers "agent sidecar servers" and "isolated servers".
These are MCP servers running directly in the container agent. For example LSP servers need to run this way because some of them need to be configured directly in the repo (e.g. The typescript LSP needs node_modules with typescript installed in the /workdir)
To expose LSP servers to Claude Code we use mcp-language-server.
- Update
mise.tomlto add the installation of the server - Update
mcp-servers.jsonto makemcp-language-serverexpose the LSP
"python": {
"command": "mcp-language-server",
"args": ["--workspace", "/workdir", "--lsp", "pyright-langserver", "--", "--stdio"]
}These are MCP servers running in their own containers isolated from the Claude Code agent. They are used to provide access to external toolings requiring authentication and/or dansgerous tools requiring host access.
To allow the communication between the agent container and the MCP containers we use
- The stdio transport capability of MCP servers
- The standard
socatutility to connect the agent stdout to the MCP server stdin
- In
devcontainer/create a new directory to store the MCP container Docker file - Create the new docker file
# Install the MCP FROM docker.io/library/node:22-alpine RUN npm install -g @upstash/context7-mcp # Install socat RUN apk add --no-cache socat USER node # Expose the MCP server's port EXPOSE 3002 # Use socat to create a new process with the MCP and write on its stdin CMD ["socat", "TCP-LISTEN:3002,fork,reuseaddr", "EXEC:context7-mcp"] - Update
docker-compose.ymlanddocker-compose.podman.ymlto include the new container to the setup. - Update
mcp-servers.jsonto usenetcatto send the MCP queries to the container's socat
"context7": {
"command": "nc",
"args": ["mcp-context7", "3002"]
},Some MCP servers need credentials (e.g. Context7 API Key). We want to prevent the agent from ever accessing this data.
We use a directory to store the credentials on a safe path in the
user's filesystem. The default path is $HOME/.config/claude-devcontainer
but that can be overriden with the env variable $MCP_CREDS_ENV_DIR.
This directory must never be mounted in the devcontainer (i.e. don't run
ccc inside $HOME/.config/claude-devcontainer);
The files in this directory are meant to be simple KEY=value and are then
used in devcontainer/docker-compose.yml
to provide the credentials to the right MCP with env_file.
Setup example for Context7:
# Create the config directory if you haven't done it yet
mkdir -p ~/.config/claude-devcontainer
# Create the new env file with the credentials
cat > ~/.config/claude-devcontainer/context7.env <<'EOF'
CONTEXT7_API_KEY=your-key-here
EOF
chmod 600 ~/.config/claude-devcontainer/context7.envIn devcontainer/scripts/lib/env.sh create the env variable which will be passed to docker compose.
# Should be there already
MCP_CREDS_ENV_DIR="${MCP_CREDS_ENV_DIR:-$HOME/.config/claude-devcontainer}"
# The variable contains the path to the env file and is used
# in docker-compose.yml in the next step
export CONTEXT7_ENV_FILE="$MCP_CREDS_ENV_DIR/context7.env"
[ -f "$CONTEXT7_ENV_FILE" ] || CONTEXT7_ENV_FILE=/dev/nullIn devcontainer/docker.compose.yml AND in devcontainer/docker-compose.podman.yml add the env file to the mcp block:
mcp-context7:
build:
context: ./mcp-context7
# [...]
env_file:
- ${CONTEXT7_ENV_FILE} # Credentials - see scripts/lib/env.sh)You can recreate only the mcp container to check your changes
ccc-compose up -d --build --force-recreate mcp-context7And check the variable is populated in the container
ccc-compose run mcp-context7 env | grep CONTEXT7_API_KEYThe MCP should now be authenticated. (Maybe you need to restart Claude? To be tested)
We use mise.toml to define all the packages, language runtimes and
other tools (shellcheck, jq, ...) which must be installed in the agent container.
The system automatically runs mise to pick up repo-specific dependencies.
This setup includes an experimental desktop notification feature. For now it works only on Linux+Docker.
- The compose files spawn a
claude-desktop-notificationservice defined indevcontainer/claude-desktop-notification - This services runs
socatto listen to a Unix socket and pipe its connections to thehandle-notify.shscript's stdin. handle-notify.shdoes two things:- It plays
bell.wavusing PulseAudio viapaplay - It runs a desktop notification with
notify-send
- It plays
On the agent side:
settings.jsonconfigures two hooks:NotificationandStopwhich make Claude call the scriptbell-notify.shwhen it is waiting for the user's input.bell-notify.shis symlinked byprovision.sh- When invoked
bell-notify.shusessocatto send a message to theclaude-desktop-notificationcontainer which triggers the notification
To run paplay the service needs to access the D-BUS and PulseAudio deamons of the host, which requires tweaking its isolation in docker-compose.yml and mounting the daemons sockets to the container.
If this is causing problems to you
- Remove the container from
docker-compose.ymlordocker-compose.podman.yml - Remove the
hookinstructions fromsettings.json - Run
ccc-rebuild
If this is still a problem, contact the author of this repo.
For now the agent container runs with network=host that allows it to reach other
docker containers created in external repos we need to work with.
But this is not ideal. We would like the agent to reach only the repo-specific networks but we want to avoid creating tying the configuration of the repos and the agents.
I haven't found a good solution yet.
UPDATE: We removed the devcontainer dependency completely. With the new system using
pure docker/podman, mise for setup, and fixed image names in docker-compose the problem
seems less annoying. Keeping an eye on that.
ORIGINAL LOG:
The rebuild time can get fairly long:
- The agent image is built from
agent/Dockerfile, then the devcontainers CLI layers the features fromdevcontainer.jsonon top. - A simple change to the base image or to
postCreate.shtriggers a full rebuild, which is slow because the feature layers get rebuilt too.
This is normal devcontainer behavior but can get annoying when iterating on this setup.
Reviewed options:
-
Local devcontainer feature. The idea was to create a Devcontainer local feature in this repo and use it in
devcontainer.json. That didn't work because local features must be part of the.devcontainerdirectory of the workdir ( microsoft/vscode-remote-release#11356) which isn't compatible with our centralized Devcontainer setup. -
A base image split The idea was to split
agent/Dockerfilein two images to benefit from more caching. But this adds complexity to the build process and seems clunky -
Getting rid of devcontainer altogether The dev container setup makes it super easy to integrate in VS Code. We want to avoid ntegrating a bare container in VS Code manually.
For now the Claude Code configurations are included directly in this repo which isn't ideal for a collaborative setup where we want teammates to use this repo as a base -rarely modifying it- and allow them to have their own configurations stored in a separate dotfiles repo.