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
- Run VS Code Claude extension in the Claude container for easy VS Code integration
- 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 (devcontainers, docker/podman compose) to easily modify the setup
- Use versioned Claude customizations (
CLAUDE.md, skill, MCP configurations) - Automatically install marketplaces and plugins on launch
- Allow using Claude Code on the CLI or in VS Code extension
Exhaustive Claude Code configuration
.claude/configuration directory- Plugins
- Custom hooks
- Custom skills
- Secure MCP integration
System requirements:
dockerwith Compose v2, orpodman4.7+ andpodman-composedevcontainercli. See devcontainers/cli- If using VS Code:
- The devcontainer extension installed (VS Code should prompt for installing it automatically)
- The standard
xxdutility
- 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-code,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
- Run
ccc-codeto open VS Code with the Claude Code extension running in the container
After running ccc or ccc-code Docker (or Podman depending on you value of CLAUDE_DEVCONTAINER_ENGINE) should show several running containers:
devcontainer-mcp-*for the different MCP containersdevcontainer-claude-desktop-notificationIf using Docker on Linuxvsc-claude_devcontainer-[uuid]for each directory where you ranccc
In vsc-claude_devcontainer-[uuid] 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
To 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`VS Code's Claude Code extension can run confined inside the agent container used by the CLI. Rather than the usual "Reopen in Container" flow (which expects a .devcontainer/devcontainer.json inside the repo), VS Code attaches to the container we create.
In a terminal navigate to the directory you want to use and run ccc-code. This opens VS Code UI.
The first time running this command VS Code might prompt you to install the Devcontainer extension and to trust the Claude extension which install automatically (See customizations.vscode.extensions in devcontainer.json.
And the bottom left of the UI you should see VS Code connected to a remote environment. The first time might take several minutes while the containers are being built.
Warning The VS Code integration relies on an undocumented feature. This might break in the future and would benefit from a stronger integation.
You can configure all the claude agents by editing the files in claude/. All the files in this directory are symlinked by postCreate.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. ThepostCreate.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. ThepostCreate.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
postCreate.shto 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)
To provide the agent with more system tools (shellcheck, jq, ...) you have two options:
- Devcontainer features: The agent container is built with
devcontainer.json, this allows to use standard features to add new tools (this is how we installclaude-codeoruvfor example) - Dockerfile: For tools which are not available as devcontainer features you can change the agent's Dockerfile which is applied after applying the devcontainer features. (This is how we install
glabcli for example)
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 at container creation withpostCreate.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.
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.