Running Claude Code Safely in a DDEV Container
Claude Code is powerful but once you start using it regularly for development, the constant permissions checks start to be a drag. They pause execution while waiting for your input, so it can disrupt the flow of your work.
Running Claude with the --dangerously-skip-permissions flag means you have the option to YOLO it as needed and let the agent do all the work it needs to without pestering you for permission to run things. However (and it's a biggie); it has unrestricted access to your entire filesystem - SSH keys, dotfiles, credentials, other projects, everything.
I've seen some unfortunate incidents posted online where Claude has gone haywire and deleted precious family photos, entire repositories etc.
One approach to containing this is running Claude Code inside an isolated environment. Emil Burzo wrote about doing this with a Vagrant VM (https://blog.emilburzo.com/2026/01/running-claude-code-dangerously-safely/). Thanks for the inspiration, Emil. This guide uses Docker containers via DDEV, with VS Code attached directly to the container.
DDEV is an open-source tool that makes local PHP development trivial. It wraps Docker Compose behind a simple CLI — run ddev start and you get a fully configured web server, database, and PHP environment in seconds. It supports Laravel, WordPress, Drupal, and more, with trusted HTTPS, automatic database configuration, and per-project isolation out of the box.
Why Use This?
The problem: Claude Code with --dangerously-skip-permissions can read, write, and execute anything on your Mac. That includes your ~/.ssh keys, ~/.zshrc, browser data, and every other project in your home directory.
The solution: Run Claude Code inside a DDEV Docker container where:
- Your project directory is mounted into the container, but nothing else from your host
- Claude Code can only affect files within that mount
- You can safely use
--dangerously-skip-permissionsfor uninterrupted workflows - VS Code attaches directly to the container, so your editor experience feels native
What you get:
- Full Laravel stack: PHP 8.4, MariaDB, Nginx, Mailpit - managed by DDEV
- Trusted HTTPS on
.ddev.sitedomains with zero certificate configuration - Claude Code CLI and the VS Code extension working inside the container
- No VirtualBox, no Vagrant, no VM overhead - just Docker containers
- Container rebuilds in seconds, not minutes
DDEV handles SSL, DNS, database management, and multi-project support out of the box. And since you're developing inside a Linux container, the environment is closer to your production target than macOS ever will be.
Prerequisites
1. Install Docker Desktop
brew install --cask docker
Open Docker Desktop and let it finish starting. On Apple Silicon Macs, it runs natively.
2. Install DDEV
brew install ddev/ddev/ddev
Verify the installation:
ddev version
3. Install VS Code Extensions
You need two extensions:
- Dev Containers (
ms-vscode-remote.remote-containers) - lets VS Code attach to running Docker containers - Claude Code (
anthropic.claude-code) - the official Claude Code extension
Install both from the VS Code Extensions panel.
Quick Start
1. Configure DDEV for your project
For an existing Laravel project:
cd ~/code/myproject
ddev config --project-type=laravel --docroot=public
Open .ddev/config.yaml and set the Node.js version. Node.js 18+ is needed for Laravel's Vite tooling (Claude Code itself installs as a standalone binary and doesn't require Node.js at runtime):
nodejs_version: "20"
Your config should look something like this:
name: myproject
type: laravel
docroot: public
php_version: "8.4"
webserver_type: nginx-fpm
database:
type: mariadb
version: "11.8"
nodejs_version: "20"
composer_version: "2"
2. Install Claude Code in the container
DDEV lets you extend its web container image with custom Dockerfiles. Create the file .ddev/web-build/Dockerfile.claude:
# Install Claude Code CLI into the web container
RUN curl -fsSL https://claude.ai/install.sh | bash \
&& cp /root/.local/bin/claude /usr/local/bin/claude \
&& rm -rf /root/.local/bin/claude
This uses the official installer recommended by Anthropic (npm installation is deprecated). The extra cp step is important: the installer drops the binary in /root/.local/bin/, but when you ddev ssh into the container you're a different user and that path isn't on your PATH. Copying to /usr/local/bin makes claude available to all users.
Using a named Dockerfile (Dockerfile.claude rather than just Dockerfile) is good DDEV practice — DDEV processes all Dockerfiles in web-build/ alphabetically, so a descriptive name avoids conflicts with other customizations. The Dockerfile gets appended to DDEV's own image build during ddev start.
3. Set your API key (optional — API billing users only)
If you have a Claude Pro or Max subscription, you can skip this step entirely — you'll authenticate interactively with claude login on first run.
If you're using the Anthropic API with your own billing, create .ddev/config.local.yaml for environment variables you don't want in version control:
web_environment:
- ANTHROPIC_API_KEY=sk-ant-your-key-here
Note:
.ddev/config.local.yamlis gitignored by DDEV's default.ddev/.gitignore. Your API key stays out of version control.
4. Start DDEV
ddev start
The first start takes longer because Docker builds the custom image with Claude Code. Subsequent starts are fast.
5. Verify the setup
ddev ssh
claude --version
exit
Confirm Claude Code is installed and the version prints correctly.
6. Attach VS Code to the container
- Open VS Code
- Open the Command Palette (
Cmd+Shift+P) - Type Dev Containers: Attach to Running Container
- Select the DDEV web container - it will be named something like
ddev-myproject-web - VS Code reopens with its workspace inside the container
Now your VS Code terminal runs inside the container, the file explorer shows the project inside the container, and extensions run inside the container.
7. Install the Claude Code extension inside the container
Once VS Code is attached to the container, open the Extensions panel and install the Claude Code extension (anthropic.claude-code). VS Code will install it inside the container. This gives you the full Claude Code sidebar experience alongside the CLI.
8. Run Claude Code
From VS Code's integrated terminal (which is now inside the container):
cd /var/www/html
claude --dangerously-skip-permissions
If this is your first run and you haven't set an API key, Claude will prompt you to authenticate. Run claude login and follow the browser-based OAuth flow to sign in with your Claude Pro/Max account.
/var/www/html is where DDEV mounts your project. Claude Code can freely modify your project files but cannot escape the container to reach your host filesystem.
Multi-line Input in the Container
If you're used to pressing Shift+Enter to insert newlines in Claude Code's TUI, you'll notice it doesn't work inside the container. This affects ddev ssh, direct SSH, and any terminal session running inside Docker - it's not specific to your terminal emulator.
Why it happens: Claude Code v2.1+ detects your terminal type to enable Shift+Enter automatically in iTerm2, WezTerm, Ghostty, and Kitty. But it checks the TERM variable on the remote machine - inside the container that's just xterm, not your actual terminal. So the special key handling never kicks in. This is a known upstream issue.
Workarounds that work reliably in the container:
| Method | How |
|---|---|
Ctrl+J |
Press Ctrl+J to insert a newline. This sends an ASCII line feed directly - it works over SSH, docker exec, and tmux without any configuration. |
\ then Enter |
Type a backslash and press Enter. This is Claude Code's built-in escape for newlines and works everywhere. |
Ctrl+J is the closest to the native Shift+Enter feel. Once the upstream fix lands, Shift+Enter should just work.
Daily Workflow
Start your day
cd ~/code/myproject
ddev start
Then attach VS Code to the container (Command Palette → Dev Containers: Attach to Running Container). If VS Code remembers the previous session, it may reconnect automatically.
Work with Claude Code
From VS Code's integrated terminal:
claude --dangerously-skip-permissions
Or use the Claude Code sidebar panel in VS Code - it works inside the container just like it does on your host.
Common commands
Since your terminal is inside the container, you don't need the ddev prefix:
php artisan migrate
composer require some/package
npm run build
But from your Mac's terminal, DDEV provides shortcuts:
ddev artisan migrate
ddev composer require some/package
ddev npm run build
ddev launch # Open the site in your browser
Access in browser
Visit https://myproject.ddev.site. DDEV handles HTTPS with a locally trusted certificate - no Keychain imports, no mkcert setup, no browser warnings.
End your day
ddev stop # Stop this project's containers (preserves database)
# or
ddev poweroff # Stop all DDEV projects
Configuring Vite
If your Laravel project uses Vite for frontend assets, you need to expose its dev server port through DDEV. Add this to .ddev/config.yaml:
web_extra_exposed_ports:
- name: vite
container_port: 5173
http_port: 5172
https_port: 5173
Then update vite.config.js so the dev server binds correctly inside the container:
export default defineConfig({
server: {
host: '0.0.0.0',
hmr: {
host: 'myproject.ddev.site',
},
},
// ... rest of your config
});
Run the dev server from inside the container or via DDEV:
ddev npm run dev
Hot module replacement will work at https://myproject.ddev.site:5173.
DDEV Commands Reference
| Command | Description |
|---|---|
ddev start |
Start the project containers |
ddev stop |
Stop the project containers |
ddev restart |
Restart containers (applies config changes) |
ddev ssh |
Shell into the web container |
ddev artisan |
Run Laravel Artisan commands |
ddev composer |
Run Composer commands |
ddev npm |
Run npm commands |
ddev launch |
Open the site in your browser |
ddev describe |
Show project info, URLs, and ports |
ddev poweroff |
Stop all DDEV projects |
ddev debug rebuild |
Force a clean image rebuild (useful after Dockerfile changes) |
ddev delete |
Remove the project containers (keeps files) |
Database Access
Using DDEV commands
ddev mysql # Interactive MySQL shell
ddev mysql -uroot -proot # As root user
ddev exec mysql -u db -pdb -e "SHOW DATABASES;" # One-off query
Create a database
DDEV creates a default db database automatically. For additional databases:
ddev mysql -uroot -proot -e "CREATE DATABASE myother_db;"
Laravel .env settings
DDEV automatically configures your .env database settings for Laravel projects. The defaults are:
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=db
DB_USERNAME=db
DB_PASSWORD=db
External database tools
Tools like TablePlus, DBeaver, or Sequel Ace can connect to the database from your Mac. Run ddev describe to see the host port:
ddev describe
Look for the database port mapping - it will show something like 127.0.0.1:59002 → 3306. Use that host port in your database client.
Running Queue Workers
Option 1: DDEV daemons (recommended)
DDEV can run background processes that survive container restarts. Add this to .ddev/config.yaml:
web_extra_daemons:
- name: queue-worker
command: "php /var/www/html/artisan queue:work --sleep=3 --tries=3"
directory: /var/www/html
Then ddev restart to apply. The queue worker starts automatically with the container.
Option 2: tmux
From inside the container:
tmux new -s worker
php artisan queue:work
# Ctrl+B, D to detach
# tmux attach -t worker to reattach
Note that tmux sessions do not survive ddev restart.
Security Notes
What is isolated
- Filesystem: Claude Code runs inside the container and can only access files in
/var/www/html(your project mount). It cannot read your Mac's home directory, SSH keys, browser data, or other projects. - Safe to use
--dangerously-skip-permissions: The "danger" is contained within the container. Claude can modify project files freely but nothing else on your host. - Database data lives in Docker volumes: Destroyed with
ddev delete. Back up withddev export-dbif needed.
What is not fully isolated
- Shared kernel: Docker containers share the host's kernel, unlike a VM which runs its own. A container escape vulnerability (rare, but theoretically possible) could compromise the host. For most development scenarios, this is an acceptable trade-off.
- Project directory is fully accessible: If you have
.envfiles with production credentials in your project, Claude Code can read them. Don't put production secrets in your local environment. - Network access is unrestricted: By default, the container can make outbound network requests. The official Claude Code devcontainer setup includes a firewall script that restricts outbound connections to whitelisted domains only. You can adapt this for DDEV if you need network-level isolation, but it requires adding
NET_ADMINandNET_RAWcapabilities to the container.
Recommendations
- Never store production credentials in your local
.env - Don't mount additional host directories into DDEV
- If using an API key, store it in
.ddev/config.local.yaml(gitignored), notconfig.yaml - For maximum isolation, consider the official Claude Code devcontainer with firewall rules: https://github.com/anthropics/claude-code/tree/main/.devcontainer
Troubleshooting
Docker not running
docker info
# If Docker is not running:
open -a Docker
Wait for Docker Desktop to finish starting before running ddev start.
DDEV container won't start
ddev poweroff # Stop everything cleanly
ddev start # Try again
If issues persist, check for port conflicts with ddev describe or try ddev restart.
Claude Code not found in container
ddev ssh
which claude
If claude is missing, the most common cause is a PATH issue: the installer places the binary in /root/.local/bin/, which isn't on your PATH when you ddev ssh into the container. The fix is to ensure your Dockerfile.claude copies the binary to /usr/local/bin (see step 2 above).
If the Dockerfile is correct but the image is stale, force a clean rebuild:
ddev debug rebuild
This rebuilds the Docker image from scratch, ensuring your Dockerfile changes are applied.
Shift+Enter doesn't insert a newline
This is expected inside the container. See Multi-line Input in the Container above - use Ctrl+J or \ then Enter instead.
Port conflicts
ddev poweroff # Stop all DDEV projects
ddev start # Restart just this one
VS Code cannot attach to container
- Ensure Docker Desktop is running
- Ensure the container is running:
ddev describeshould show status "running" - Try the Command Palette → Dev Containers: Attach to Running Container again
- If the Dev Containers extension hangs, update it to the latest version - older versions have a known bug that causes hangs
Slow file performance on macOS
Docker's file sharing on macOS can be slow with large vendor/ or node_modules/ directories. Enable Mutagen for near-native performance:
ddev config --performance-mode=mutagen
ddev restart
Services Included
| Service | Details | Access |
|---|---|---|
| Nginx | Latest | https://myproject.ddev.site |
| PHP-FPM | 8.4 | Via Nginx |
| MariaDB | 11.8 | Host: db, User: db, Pass: db |
| Node.js | 20+ | Configured via nodejs_version |
| Mailpit | Included | https://myproject.ddev.site:8026 |
| Claude Code | Latest | claude command in container |
Redis is not included by default. Add it with
ddev get ddev/ddev-redis && ddev restart.
Customization
Adding Redis
ddev get ddev/ddev-redis
ddev restart
Changing PHP version
In .ddev/config.yaml:
php_version: "8.3"
Then ddev restart.
Changing Node.js version
nodejs_version: "22"
Then ddev restart.
Multiple projects
DDEV supports running multiple projects simultaneously. Each project gets its own .ddev.site subdomain, its own database, and its own isolated container. Just run ddev config and ddev start in each project directory.