Skip to content

Repository files navigation

YOLO Claude Android

Run Claude Code in full-permission ("YOLO") mode, safely isolated inside a Docker container with full Android build support.

Inspired by Claude Dev Container.

YOLO Claude Android Logo

Platform support

  • Linux: tested on Kubuntu 24.04 LTS.
  • macOS (Apple Silicon): requires Docker Desktop with amd64 emulation (Rosetta recommended). See macOS notes for the differences.

Features

  • 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-android wrapper, launchable from any project directory
  • Reuses MCP servers (optionally, disabled by default)

Prerequisites

  • 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 socat or equivalent). Not needed on macOS.

Quick Start

1. Install

git clone https://github.com/romychab/yolo-claude-android.git
cd yolo-claude-android
./install.sh

install.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.

2. Launch

From your Android project root:

cd /path/to/your/android/project
yolo-claude-android

If 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.

3. Run Claude inside the container

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.

Authentication

  • OAuth (default). ~/.claude/.credentials.json is bind-mounted read-write (tokens only), so a refresh inside the container stays in sync with the host.
  • API key. Set ANTHROPIC_API_KEY in the environment you launch from; the launcher forwards it by name only, so the value never appears in docker run args or --show-command output. The value is captured at container creation - rotate the key and you must --recreate.

Configuration (claude-isolation.conf)

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.example

Because these settings are fixed at container creation, changing any of them prompts a recreate on the next launch.

permission_mode

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.

Emulator control ([emulator])

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).

Network egress ([network])

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.

MCP servers ([mcp])

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.

macOS (Apple Silicon) notes

  • 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.sh aborts without amd64 emulation and warns if Rosetta is absent.
  • Android SDK: the container uses its own Linux SDK on a persistent android-sdk-cache volume (the host SDK's darwin binaries can't run in Linux). android_sdk_path is still used, but only host-side to locate adb/emulator.
  • Login: the OAuth token lives in the login Keychain (service Claude Code-credentials), not a file. The launcher reads it into a 0600 per-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 ~/.claude config/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 (no socat needed).

Security model & warnings

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 modified gradlew, 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). ~/.claude and ~/.claude.json are 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 planted hooks, plugin, or mcpServers entry 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 adb can - 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.

How it works

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.

ADB connectivity

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.

Container lifecycle

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

Troubleshooting

  • "Docker image not found" - run ./install.sh from the repo root.
  • "ADB server failed to start" - run adb start-server manually and retry.
  • adb says failed to connect to 'host.docker.internal:5037' (Linux) - the ADB bridge isn't up. Install socat and relaunch; verify with ss -lnt | grep 172.17.0.1:5037.
  • A tool can't reach a host with [network] restrict = true - add it to allow and recreate. A [firewall] FATAL at startup means enforcement couldn't be verified; check the daemon has NET_ADMIN/NET_RAW (added automatically when restriction is enabled).
  • Files owned by root - shouldn't happen; check HOST_UID with docker 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_HOME apply, then relaunch.

Uninstall

Revert the installation from the repo root:

cd /path/to/yolo-claude-android
./uninstall.sh

It 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.

Development

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]"
pytest

The image-side scripts (image/) and Dockerfile are exercised by building the image (./install.sh --rebuild) and launching a container, not by the Python tests.

About

Dockerized, sandboxed Claude Code for Android/Gradle projects. YOLO mode without touching a host.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages