Running Claude Code Safely in Devcontainers

July 3, 2025

I've been experimenting with Claude Code lately and found myself wanting to use --dangerously-skip-permissions more often than I'm comfortable with. The permission prompts are helpful for security, but they slow down development flow when you're iterating rapidly. I started looking for ways to get the best of both worlds: the convenience of skipping permissions while reducing the blast radius of what Claude can actually do to my system.

My solution has been to run Claude Code inside devcontainers. Docker's isolation means that even if Claude goes rogue, it can't touch my host system files, install system packages, or mess with my main development environment. The container acts as a sandbox where Claude can work freely without putting my actual machine at risk.

Here's my setup. I'm using Docker Compose to orchestrate both the development container and any dependencies like databases, but you could simplify this to just a basic devcontainer if you don't need the orchestration.

The devcontainer.json configuration:

{
  "name": "Acme Monorepo",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspaces/acme-mono",
  "customizations": {
    "vscode": {
      "extensions": [
        "Anthropic.claude-code"
      ]
    }
  },
  "forwardPorts": [4200, 5434],
  "portsAttributes": {
    "4200": {
      "label": "Web App",
      "onAutoForward": "notify"
    },
    "5434": {
      "label": "PostgreSQL",
      "onAutoForward": "ignore"
    }
  },
  "postStartCommand": "npm install",
  "remoteEnv": {
    "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
  },
  "mounts": [
    "source=acme-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
    "source=${localWorkspaceFolder}/.claude,target=/home/node/.claude,type=bind",
    "source=${localWorkspaceFolder}/.claude.json,target=/home/node/.claude.json,type=bind"
  ],
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {
      "installDirectlyFromGitHubRelease": true,
      "version": "latest"
    }
  }
}

The most important part of this configuration is the mounts section. These bind mounts are crucial for maintaining Claude's authentication state across container rebuilds. Without them, you'd need to log in to Claude every time you rebuild your container, which defeats the purpose of a smooth development experience.

The first mount creates a Docker volume for node_modules, which improves performance by avoiding the overhead of syncing large dependency directories between host and container. The second and third mounts bind your local Claude configuration files into the container's home directory. This means Claude's API keys, session tokens, and preferences persist even when you tear down and rebuild the container.

Note that the Claude configuration files (.claude and .claude.json) will be created in your project root and should be added to your .gitignore. If you want a more seamless experience, you might experiment with mounting your entire home directory's dotfiles, though this approach may have mixed results and reduces some of the isolation benefits.

Here's the Dockerfile that sets up the development environment:

FROM node:20-bookworm-slim

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    sudo \
    ca-certificates \
    ripgrep \
    fd-find \
    jq \
    tree \
    bat \
    htop \
    unzip \
    && rm -rf "/var/lib/apt/lists/*"

# Install Claude Code globally
RUN npm install -g @anthropic-ai/claude-code

# Set up non-root user
ARG USERNAME=node
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Configure the node user
RUN groupmod --gid $USER_GID $USERNAME \
    && usermod --uid $USER_UID --gid $USER_GID $USERNAME \
    && chown -R $USER_UID:$USER_GID /home/$USERNAME

# Add node user to sudoers
RUN echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Set up workspace directory
RUN mkdir -p /workspaces/acme-mono
WORKDIR /workspaces/acme-mono

# Switch to node user for development
USER $USERNAME

The key here is that Claude Code runs as a non-root user inside the container, but even with sudo access, it's still contained within Docker's isolation boundaries. The container can't access your host filesystem beyond what you explicitly mount.

If you want to orchestrate additional services like databases, you can use docker-compose.yml:

version: '3.8'

services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - ../..:/workspaces:cached
      - acme-node-modules:/workspaces/acme-mono/node_modules
    command: sleep infinity
    ports:
      - "4200:4200"
      - "3000:3000"
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/acme

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: acme
    ports:
      - "5434:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  acme-node-modules:
  postgres-data:

This gives you a complete development environment where Claude can interact with your application and its dependencies without any risk to your host system. Claude will have access to your package scripts like npm run lint, npm run format, and npm run typecheck, which is crucial for maintaining code quality during longer agentic tasks.

Once you have this set up, you can run claude --dangerously-skip-permissions inside the container without worrying about Claude accidentally deleting important files or breaking your system configuration. The worst it can do is mess up the container, which you can rebuild in a few minutes.

The workflow becomes: open your project in VS Code, let the devcontainer build and start, then open a terminal inside the container and run Claude Code with skipped permissions. Claude gets full access to do its work, but that access is scoped to the containerized environment.

There are some important caveats to this approach. Claude's IDE integration features don't work out of the box in this setup, meaning you lose easy access to IDE diagnostics, the ability to see currently open files, and selected line ranges. Claude operates purely through the terminal interface, which is less convenient than the integrated IDE experience but still quite powerful for most development tasks.

A note on security: while this setup prevents Claude from damaging your host system, it doesn't eliminate all risks. Claude still has GitHub access through the mounted authentication, which could potentially be used for destructive actions on your repositories. It also has internet access, which opens the door to prompt injection attacks where malicious content could potentially influence Claude's behavior. Use this setup at your own risk.

For even more paranoid security, you could explore using Docker's networking features to limit internet access, or handle GitHub operations exclusively on the host machine even if it means losing some of the integrated development experience.

I've been using this setup for a few weeks now and it's struck a good balance between productivity and safety. Claude can work freely on my projects without me worrying about what it might accidentally touch on my actual machine. The container rebuilds are fast enough that even if something goes wrong, getting back to a clean state is painless.

If you're finding yourself wanting to skip Claude's permission prompts but are nervous about the security implications, I'd encourage you to try this approach. It takes about 15 minutes to set up and gives you a much more confident development experience with Claude.