Run Claude Code in full-permission ("YOLO") mode, safely isolated inside a Docker container with full Android build support.
Inspired by Claude Dev Container.
- Linux: tested on Kubuntu 24.04 LTS.
- macOS (Apple Silicon): requires Docker Desktop with amd64 emulation (Rosetta recommended). See macOS notes for the differences.
- ADB forwarded to the host's ADB server
- Opt-in host emulator control via a validated broker (no emulator inside the container)
- Files created by Claude are owned by your host user (UID/GID remapping)
- Reuses your host Claude auth, config, and plugins/skills - no re-login or setup dialog. Config is copied in, so in-container changes can't escape to the host.
- Per-project TOML config, auto-detected from
local.properties - Reuses existing containers; prompts before creating new ones
yolo-claude-androidwrapper, launchable from any project directory- Reuses MCP servers (optionally, disabled by default)
- Docker 20.10+
- Python 3.11+
- Android SDK installed on the host
- Claude Code installed on the host (
~/.local/share/claude/must exist) adb(Android SDK platform-tools)socat- Linux only, for ADB connectivity from the container (sudo apt install socator equivalent). Not needed on macOS.
git clone https://github.com/romychab/yolo-claude-android.git
cd yolo-claude-android
./install.shinstall.sh is idempotent. It checks prerequisites, builds the
yolo-claude-android:latest image if missing (--rebuild forces it), symlinks the
launcher into ~/.local/bin, and checks that directory is on your PATH.
From your Android project root:
cd /path/to/your/android/project
yolo-claude-androidIf claude-isolation.conf is missing, the launcher auto-detects settings from
local.properties and $JAVA_HOME. To configure manually, copy
claude-isolation.conf.example to claude-isolation.conf and set android_sdk_path.
On first run you get a compact summary and a confirmation prompt (add --show-command
to see the full docker run). Later runs reuse the container; if you edit the config or
rebuild the image, the launcher offers to recreate it. Force a fresh container with
--recreate.
yolo # = claude --dangerously-skip-permissions; forwards extra args (yolo --resume)Each interactive shell prints a short reminder of the yolo shortcut.
Your host Claude session is reused: auth (~/.claude), config (~/.claude.json), and
plugins/skills are copied in, so there's no re-login or setup dialog. The copies are
writable but isolated from the host - see How it works and the
security model.
- OAuth (default).
~/.claude/.credentials.jsonis bind-mounted read-write (tokens only), so a refresh inside the container stays in sync with the host. - API key. Set
ANTHROPIC_API_KEYin the environment you launch from; the launcher forwards it by name only, so the value never appears indocker runargs or--show-commandoutput. The value is captured at container creation - rotate the key and you must--recreate.
Place this file in your project root (git-ignored by this repo). See
claude-isolation.conf.example for all options.
android_sdk_path = "/opt/android-sdk"
jdk_version = 21 # 17 or 21 (default 21)
include_local_maven = false
permission_mode = "bypass"
[[extra_mounts]]
path = "/home/user/keystores"
readonly = true
[network]
restrict = true
allow = ["internal.maven.example.com"]
# see other options listed in claude-isolation.conf.exampleBecause these settings are fixed at container creation, changing any of them prompts a recreate on the next launch.
How the yolo shortcut launches Claude:
bypass(default) ->claude --dangerously-skip-permissions(full YOLO).- If your account/org disallows bypass (e.g. Teams/Enterprise, where the flag is
silently ignored), pick a mode passed to
claude --permission-mode:acceptEdits(recommended fallback),auto,plan,default,dontAsk.
By default the container can control a running emulator over ADB but not launch one.
Setting allow_host_control = true starts a host-side broker that runs the real
emulator on your behalf; inside the container emulator is a shim that forwards to it.
Because this allows host execution, it's opt-in and tightly scoped: the broker validates
every request against an allowlist (launch by known AVD name + a curated set of safe boot
flags; -list-avds, -version, -accel-check). Escape-hatch flags (-qemu, anything
path-bearing) are rejected, and a per-host secret token limits it to this tool's
containers. Linux and macOS (binds to 127.0.0.1 on macOS).
The broker is host-global, started on demand and reused across projects. Manage it with
yolo-claude-android --stop-broker / --restart-broker (both exit without launching).
By default outbound access is unrestricted. restrict = true confines egress to a
whitelist, enforced with iptables (default-deny) and an ipset populated live by dnsmasq
(so rotating CDN IPs are covered). A built-in set of hosts for Android development and
Claude Code (Maven Central, Google's Maven, Gradle, Anthropic, GitHub, npm) is always
allowed; allow entries are added on top. DNS, the host gateway (for ADB), and
established connections are always permitted.
Enforcement is verified at startup; if it can't be confirmed, the container refuses to start rather than run unrestricted.
Off by default. When disabled, the launcher copies your ~/.claude.json into the
container with all mcpServers stripped — so MCP secrets never enter the
container unless you ask. Enable it explicitly:
[mcp]
enabled = true
env = ["GITHUB_TOKEN"] # host env var NAMES forwarded by name only (value never in args)When enabled, the container gets your global mcpServers plus the entry for this
project (other projects' servers and secrets stay out). The image bundles Node.js, so
npx-based servers and python-based servers both run. For a remote MCP server with
[network] restrict = true, add its host to [network].allow.
Secrets reach servers two ways: names listed in [mcp].env are forwarded by name only
(kept out of docker run args and --show-command), and any inline env in the
included mcpServers rides along.
Security: enabling MCP widens the risk surface. A local (stdio) server is a process
with the agent's full access (project read-write, ADB, copied-config read), and npx
runs arbitrary npm packages at runtime. A remote server is an exfiltration/injection
channel that restrict scopes but does not prevent. Any MCP secret is readable by the
agent and every other MCP server in the container. Enable only servers you trust for
autonomous use, and use least-privilege tokens.
- Emulation: the image is arm64, but Google's SDK tools (e.g.
aapt2) are x86_64-only and run under Docker Desktop emulation. Enable Settings -> General -> "Use Rosetta for x86_64/amd64 emulation" and install Rosetta (softwareupdate --install-rosetta).install.shaborts without amd64 emulation and warns if Rosetta is absent. - Android SDK: the container uses its own Linux SDK on a persistent
android-sdk-cachevolume (the host SDK's darwin binaries can't run in Linux).android_sdk_pathis still used, but only host-side to locateadb/emulator. - Login: the OAuth token lives in the login Keychain (service
Claude Code-credentials), not a file. The launcher reads it into a0600per-container file, bind-mounts it as~/.claude/.credentials.json, and a watcher writes in-container refreshes back. macOS may show a one-time "security wants to use confidential information" prompt - click "Always Allow". - Claude binary: the host binary is a darwin build, so a Linux build is baked into the
image (your
~/.claudeconfig/session is still shared). - adb version match: the in-container adb is fetched to match your host adb revision. Upgrade host platform-tools and the launcher offers to recreate the container.
- ADB & emulator: work as on Linux via
host.docker.internal(nosocatneeded).
This tool runs Claude with --dangerously-skip-permissions. The container isolates your
host filesystem and processes, but a few things are intentionally reachable - treat
them as fully exposed to whatever Claude (or a prompt injection) does:
- The project directory and read-write mounts. Bind-mounted read-write at their real
host paths; Claude can create, modify, and delete files. Keep secrets out, or mount
readonly = true. The project mount is also an escape path: a modifiedgradlew, Gradle task, or.git/hooks/*runs on the host next time you build/commit - review changes before doing so. - Your host Claude config (read exposure).
~/.claudeand~/.claude.jsonare copied in (host copies mounted read-only as seeds). The container can read everything there, including auth tokens, but anything it writes stays in the container - a plantedhooks, plugin, ormcpServersentry cannot reach the host. Exception:~/.claude/.credentials.json(tokens only) is bind-mounted read-write. - Attached devices via ADB. The container drives your host ADB server, so it can do
anything
adbcan - including destructive ops (uninstall,adb shell, wipe data, push/pull) on any attached device. Prefer emulators over real devices. On Linux the ADB bridge is on the Docker bridge gateway, reachable by other containers on the host - avoid running untrusted containers alongside it.
The network is unrestricted by default, so an in-container session can read your auth
tokens and send them anywhere. [network] restrict = true confines egress, but it is a
scoping control, not an anti-exfiltration guarantee (the default allowlist includes
broadly-writable hosts like github.com), and the host stays reachable on the Docker
bridge gateway regardless.
Hardening (--security-opt no-new-privileges, --pids-limit) is defense-in-depth, not a
substitute for the above. The robust mitigation is to keep secrets and unrelated data out
of mounted paths and prefer emulators over real devices.
Host Container
────────────────────────────────────────────────────────────
claude_android (package) ubuntu:24.04 + JDK 17/21
├─ reads claude-isolation.conf ├─ /home/developer (UID=yours)
├─ checks ADB server ├─ ~/.gradle -> named volume (gradle-cache)
├─ docker run / exec ├─ Android SDK (read-only, same path)
└─ attaches terminal ├─ ~/.claude (copied in; writes stay)
├─ ~/.claude.json (copied in; writes stay)
├─ ~/.claude/.credentials.json (read-write, tokens)
├─ host ~/.claude(.json) symlinked into place
├─ project dir (read-write, same path)
ADB server (port 5037) <----------- adb via host.docker.internal
The container runs as developer (HOME=/home/developer). Host config records absolute
paths under your host home, so the entrypoint symlinks your host home's .claude and
.claude.json to the in-container copies, letting skills and plugins resolve unmodified.
Config is copied in, not bind-mounted (host-escape hardening). The host ~/.claude
and ~/.claude.json are mounted read-only as seeds; on first start the entrypoint copies
them into a writable container-local ~/.claude. Those writes never reach the host, so a
planted hook/plugin/mcpServers entry can't run on your host later. The one exception is
~/.claude/.credentials.json (tokens only), bind-mounted read-write. Side effect:
in-container changes to settings/plugins/session state are not persisted back.
The container uses the host's ADB server, so adb inside it drives host-attached
devices/emulators.
On Linux, the host ADB server listens only on 127.0.0.1:5037, which the container
can't reach directly. The launcher starts a detached socat forwarder on
<bridge-gateway>:5037 -> 127.0.0.1:5037 (host ADB stays localhost-only, exposed only on
docker0). Requires socat (sudo apt install socat); without it, builds work but
on-device ADB doesn't. The forwarder is reused across runs; stop it with
pkill -f 'TCP-LISTEN:5037'.
On macOS (Docker Desktop) host.docker.internal routes to the host natively,
so no bridge or socat is needed.
| Situation | Behaviour |
|---|---|
| Container running | Opens additional shell via docker exec |
| Container stopped | Restarts and attaches via docker start -ai |
| No container | Shows docker run command, prompts for confirmation |
- "Docker image not found" - run
./install.shfrom the repo root. - "ADB server failed to start" - run
adb start-servermanually and retry. adbsaysfailed to connect to 'host.docker.internal:5037'(Linux) - the ADB bridge isn't up. Installsocatand relaunch; verify withss -lnt | grep 172.17.0.1:5037.- A tool can't reach a host with
[network] restrict = true- add it toallowand recreate. A[firewall] FATALat startup means enforcement couldn't be verified; check the daemon hasNET_ADMIN/NET_RAW(added automatically when restriction is enabled). - Files owned by root - shouldn't happen; check
HOST_UIDwithdocker inspect <container-name> | grep HOST_UID. - First-run setup dialog / missing skills appear in the container - recreate the
container so the latest mounts and
HOST_HOMEapply, then relaunch.
Revert the installation from the repo root:
cd /path/to/yolo-claude-android
./uninstall.shIt lists everything it will remove: all claude-android-* containers (running ones
flagged), the gradle-cache and android-sdk-cache volumes, the
yolo-claude-android:latest image, the ~/.local/bin/yolo-claude-android symlink, and
the ~/.cache/yolo-claude-android/ state directory - then asks for confirmation. It also
stops the emulator broker and the ADB bridge. Pass -y/--yes to skip the prompt, or
-h/--help for usage.
Per-project claude-isolation.conf files are left in place; delete them by hand if you
no longer need them.
The launcher is a pure-stdlib Python package (claude_android); the only dev dependency
is pytest.
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytestThe image-side scripts (image/) and Dockerfile are exercised by building the image
(./install.sh --rebuild) and launching a container, not by the Python tests.
