If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.
This add-on integrates Claude Code, Anthropic’s AI coding CLI, into your DDEV project as a sandboxed sidecar container. Claude runs with --dangerously-skip-permissions (YOLO mode) safely contained behind an iptables + ipset + dnsmasq firewall, so an agent off the rails cannot exfiltrate your .env, your SSH keys, or anything else outside the project tree.
The default sidecar is minimum viable: Claude Code + firewall on a pre-built debian:bookworm-slim base image. Project-specific tools (PHP, gh, Playwright, etc.) are opt-in via a small extras catalog or a Dockerfile escape hatch.
ddev add-on get makraz/ddev-claude
ddev restart
To pin to a specific version (see Releases for what’s available):
ddev add-on get makraz/[email protected]
ddev restart
After ddev add-on get, commit the changes to your project’s .ddev/ directory.
| Command | Description |
|---|---|
ddev claude |
Interactive Claude Code session (YOLO mode, default). |
ddev claude safe |
Same, but without --dangerously-skip-permissions. |
ddev claude shell |
Drop into bash inside the sidecar (firewall active). |
ddev claude exec <cmd> |
Run one command in the sidecar non-interactively. |
ddev claude rebuild |
Regenerate .ddev/claude/Dockerfile after editing .ddev/claude.yaml. |
ddev claude help |
Print the help text. |
ddev claude <args> |
Anything else passes through to the claude CLI (e.g. --resume). |
The sidecar is reachable as the claude service on the DDEV default network. Auth tokens and settings persist at .ddev/.claude/ (gitignored by default).
Set on your host shell before ddev start / ddev restart. The sidecar’s docker-compose.claude.yaml forwards them into the container.
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY |
unset | API key. Optional — OAuth flow runs on first launch if unset. |
GITHUB_PERSONAL_ACCESS_TOKEN |
unset | Forwarded to the sidecar so the agent can git push to private repos. Also exported as GH_TOKEN for gh and GitHub MCP fragments added via the escape hatch. |
EXTRA_ALLOWED_DOMAINS |
unset | Space-separated extra outbound domains, allow-listed at runtime. Prefer .ddev/claude.yaml’s extra_allowed_domains: for project-level settings. |
PLAYWRIGHT_BASE_URL |
https://web |
Pre-set inside the container so Playwright/MCP fragments added via the escape hatch hit the DDEV web service by default. Override on the host shell if needed. |
CLAUDE_SAFE |
0 |
Read by the ddev claude host command. Set to 1 to opt out of YOLO mode for a single invocation (equivalent to ddev claude safe). |
Per-project configuration lives in .ddev/claude.yaml (user-editable):
# Available extras: php
extras:
- php
# Additional outbound domains the runtime firewall should allow.
extra_allowed_domains:
- sentry.io
- api.stripe.com
If the file is absent or both lists are empty, the sidecar is built minimum-viable.
| Extra | What it installs | Adds to firewall allow-list |
|---|---|---|
php |
PHP 8.5 CLI + Composer + common extensions (bcmath, curl, gd, intl, mbstring, mysql, soap, xml, xsl, zip) via Ondřej Surý’s apt repo. | packagist.org, repo.packagist.org |
Edit .ddev/claude.yaml, run ddev claude rebuild (or ddev restart), and the image is regenerated.
For tooling not in the catalog, drop fragments into .ddev/claude.local/:
.ddev/claude.local/Dockerfile.fragment:
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
gnupg lsb-release \
&& mkdir -p -m 755 /etc/apt/keyrings \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update && apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/*
(github.com is already in the default allow-list — no extra-domains.list entry needed.)
.ddev/claude.local/Dockerfile.fragment:
USER root
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
.ddev/claude.local/extra-domains.list:
deb.nodesource.com
registry.npmjs.org
.ddev/claude.local/Dockerfile.fragment:
USER root
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN mkdir -p "$PLAYWRIGHT_BROWSERS_PATH" \
&& npx --yes playwright install --with-deps chromium \
&& npm install -g @playwright/mcp chrome-devtools-mcp \
&& chmod -R a+rX "$PLAYWRIGHT_BROWSERS_PATH"
.ddev/claude.local/extra-domains.list:
storage.googleapis.com
(Requires the Node.js fragment first.)
A claude sidecar built from a pre-built multi-arch base image (ghcr.io/makraz/ddev-claude-base:<version>, published from this repo) containing:
git, bash, sudo, curl, ca-certificates.iptables, ipset, dnsmasq, dnsutils, iproute2 for the firewall stack.claude user (uid 1000) with a NOPASSWD sudoers entry scoped to /usr/local/bin/init-firewall.sh.An outbound firewall (default-DROP policy) that allows only:
github.com, api.github.comanthropic.com, claude.aiweb, db, sibling add-ons)..domains files) and your .ddev/claude.yaml adds via extra_allowed_domains.The image tag is pinned to the addon version 1:1. Installing ddev-claude@<tag> always pulls ddev-claude-base:<tag> — no floating :latest. The exact Claude Code build baked into a given image is recorded in the OCI label io.makraz.ddev-claude.claude-version and visible via docker inspect.
After ddev claude starts, run a smoke test inside the session:
curl -sS --max-time 5 https://api.github.com # should succeed
curl -sS --max-time 3 https://example.com || echo BLOCKED # should be BLOCKED
sudo iptables -L OUTPUT -n | head -1 # should say "policy DROP"
If example.com is reachable or the iptables policy is ACCEPT, the firewall is not active — stop and investigate before trusting the agent with autonomous work.
init-firewall.sh (run as root via the NOPASSWD sudoers entry) sets up:
allowed-ipv4 (hash:ip) and allowed-net (hash:net).dig resolves the default + extra domains, populating allowed-ipv4.127.0.0.1, upstreams to 127.0.0.11 (Docker’s embedded DNS, so DDEV service names like web/db resolve), 1.1.1.1, 8.8.8.8. Each allow-listed domain is bound via ipset=/<domain>/allowed-ipv4, so future resolutions automatically extend the allow-list — handles CDN IP rotation.example.com (must fail).The published image (ghcr.io/makraz/ddev-claude-base:<version>) is built from image/Dockerfile by .github/workflows/publish-image.yml on every git tag push. To iterate on image/Dockerfile without publishing:
# Build locally with the tag the addon expects:
TAG=$(grep -m1 'FROM ghcr.io/.*/ddev-claude-base:' .ddev/claude/Dockerfile.base | sed 's|.*:||')
docker build -t "ghcr.io/makraz/ddev-claude-base:${TAG}" image/
# ddev restart picks up the local image (Docker's default `missing`
# pull policy uses local images when present).
ddev restart
To cut a release:
git tag v0.3.0-beta.1 # or v0.3.0 for stable
git push origin v0.3.0-beta.1
# publish-image.yml builds + pushes the image to GHCR (~5 min, multi-arch).
gh release create v0.3.0-beta.1 --prerelease --notes-file release-notes.md
init-firewall.sh if dual-stack is required..git and .env* are bind-mounted: the agent can read (and potentially commit) anything in your project directory. Keep secrets out of the working tree, or use CLAUDE_SAFE=1 for untrusted tasks.claude user inside the container is uid 1000. If your host user uses a different uid, file ownership may look unusual on .ddev/.claude/..ddev/.claude/ holds auth state: a determined agent could plant configuration there (e.g. a malicious MCP entry in ~/.claude/settings.json) that runs in the next session. Such code still runs under the same firewall + uid, so it cannot break out, but the persistence vector is real.ddev add-on remove claude
ddev restart
Add-on-managed files are removed. User-managed files are preserved (delete manually if desired):
rm -rf .ddev/claude.yaml .ddev/.claude/ .ddev/claude.local/
Contributed and maintained by @makraz
Firewall sandboxing approach inspired by Anthropic’s official Claude Code devcontainer reference.
If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.