A single, ergonomic command-line interface for all Android development tasks. acli wraps sdkmanager, avdmanager, adb, fastboot, and Gradle so you never have to memorize package paths, flag syntax, or which binary lives where.
$ acli doctor
✓ ANDROID_HOME is set (/Users/you/Library/Android/sdk)
✓ Java is installed (openjdk version "21.0.10")
✓ adb found
✓ sdkmanager found
✓ avdmanager found
✓ emulator found
✓ fastboot found
✗ SDK licenses not fully accepted
Run: acli sdk licenses
✓ ADB server running
✓ Connected devices (1 device)
- Why acli
- Runtime Dependencies
- Build Dependencies
- Installation
- Environment Setup
- Command Reference
- Global Flags
- JSON Output and Automation
- AI Agent Integration (Claude Code)
- Development Guide
- Creating a Release
- Project Structure
- Configuration
- Shell Completion
- Self-Update
- Error Handling
Android's command-line tooling is fragmented across six separate binaries with inconsistent interfaces:
| Tool | Location | Problem |
|---|---|---|
sdkmanager |
cmdline-tools/latest/bin/ |
Package paths like "system-images;android-35;google_apis;x86_64" |
avdmanager |
cmdline-tools/latest/bin/ |
Requires knowing exact image IDs |
adb |
platform-tools/ |
error: more than one device/emulator with no guidance |
fastboot |
platform-tools/ |
Only works in bootloader mode; easy to brick devices |
emulator |
emulator/ |
Dozens of flags, no wait-for-boot |
gradlew |
project root | Must cd to project root; opaque error messages |
acli solves this with:
- One binary — no PATH juggling across SDK subdirectories
- Ergonomic subcommands —
acli sdk install "platforms;android-35"instead of searching for the right path - Contextual error messages — instead of passing raw Java stack traces,
aclimaps 15+ known error patterns to human-readable explanations with exact fix commands --jsonflag — every command emits structured JSON for use in CI pipelines and AI agentsacli doctor— instant environment health check
acli is a thin wrapper around Android's tooling. The SDK command-line tools can be installed automatically via acli sdk bootstrap; everything else must be installed separately.
| Dependency | Version | Purpose | Install |
|---|---|---|---|
Android SDK Command-Line Tools (sdkmanager, avdmanager) |
Any recent | acli sdk, acli avd; also required for all other SDK-dependent commands |
acli sdk bootstrap ← start here |
Android Platform Tools (adb, fastboot) |
Any recent | acli device, acli flash, acli instrument |
acli sdk install platform-tools |
| Java (JDK) | 17 or newer (21 recommended) | Required by sdkmanager, avdmanager, and Gradle |
SDKMAN — see below |
| Dependency | Purpose |
|---|---|
Android Emulator (emulator binary) |
acli avd start / acli avd stop — install with acli sdk install emulator |
Gradle wrapper (gradlew in project root) |
All acli build commands |
SDKMAN is the recommended way to install and manage Java versions on macOS and Linux. It handles download, extraction, and shell configuration automatically and makes switching between Java versions trivial.
# 1. Install SDKMAN
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
# 2. Install Java 21 (Temurin — recommended for Android development)
sdk install java 21-tem
# 3. Verify
java -versionTo see all available JDK distributions and versions:
sdk list javaacli finds your Android SDK root automatically in this order:
$ANDROID_HOMEenvironment variable$ANDROID_SDK_ROOTenvironment variable- Well-known platform paths:
- macOS:
~/Library/Android/sdk - Linux:
~/Android/Sdk,/opt/android-sdk - Windows:
%LOCALAPPDATA%\Android\Sdk
- macOS:
Run acli doctor to verify that everything is found correctly.
You only need these if you are building acli from source.
| Dependency | Version | Purpose |
|---|---|---|
| Go | 1.22 or newer | Compiler and toolchain |
| git | Any | Version injection in the binary via git describe |
| make | Any (optional) | Convenience targets; go build works directly without it |
All Go library dependencies are declared in go.mod and downloaded automatically by go mod download or go build. No external package manager is required.
Direct library dependencies (see go.mod for pinned versions):
| Library | Purpose |
|---|---|
github.com/spf13/cobra |
CLI framework: command tree, --help, shell completion |
github.com/spf13/viper |
Config file and environment variable management |
github.com/charmbracelet/lipgloss |
Color-coded terminal output panels |
golang.org/x/term |
TTY detection (switches between human and JSON output) |
Run this on macOS or Linux — it detects your OS and architecture automatically:
curl -fsSL https://raw.githubusercontent.com/ErikHellman/android-cli/main/install.sh | bashWindows (PowerShell):
irm https://raw.githubusercontent.com/ErikHellman/android-cli/main/install.ps1 | iexInstalls to %LOCALAPPDATA%\Programs\acli\ and adds it to your user PATH — no administrator rights required.
Windows via WSL: If you use Windows Subsystem for Linux, run the macOS/Linux command above inside your WSL terminal instead.
git clone https://github.com/ErikHellman/android-cli.git
cd acli
make install # builds and installs to $GOPATH/binOr without make:
go install github.com/ErikHellman/android-cli/cmd/acli@latestacli --help
acli doctorSet $ANDROID_HOME so acli can find your SDK:
# macOS / Linux — add to ~/.zshrc or ~/.bashrc
export ANDROID_HOME=~/Library/Android/sdk # macOS default
# export ANDROID_HOME=~/Android/Sdk # Linux default
export PATH="$ANDROID_HOME/platform-tools:$PATH" # adds adb, fastboot to PATHThen reload your shell and confirm:
acli doctoracli sdk list # all packages
acli sdk list --installed # only installed packages
acli sdk list --available # only packages available to install
acli sdk list --updates # packages with available updates
acli sdk list --channel canary # include canary channel
acli sdk install "platforms;android-35"
acli sdk install "build-tools;35.0.0" "platform-tools"
acli sdk install "system-images;android-35;google_apis;x86_64"
acli sdk install "ndk;26.1.10909125"
acli sdk uninstall "platforms;android-33"
acli sdk update # update all installed packages
acli sdk licenses # accept all pending licenses (CI-safe)acli avd list # all AVDs
acli avd list --running # only running emulators
acli avd create Pixel9 --api 35 # arm64-v8a by default
acli avd create MyPhone --api 34 --device pixel_7 --abi x86_64
acli avd create TestPhone --api 35 --sdcard 512M --force
acli avd create MyAuto --api 35-ext15 --tag android-automotive-playstore --device automotive_1408p_landscape_with_google_apis
acli avd start Pixel9 # launch emulator window
acli avd start Pixel9 --headless # no window (CI mode)
acli avd start Pixel9 --headless --wait-boot # block until boot completes
acli avd start Pixel9 --port 5556 # custom ADB port
acli avd stop emulator-5554
acli avd delete Pixel9
acli avd images # list installable system images
acli avd images --api 35 # filter by API levelacli device list # all connected devices/emulators
# Target a specific device with -d / --device or $ACLI_DEVICE
acli -d emulator-5554 device shell # interactive shell
acli -d emulator-5554 device shell dumpsys battery
acli device install app-debug.apk
acli device install app-debug.apk --grant-all --reinstall
acli device uninstall com.example.app
acli device logs # all logcat output
acli device logs --follow --level E # stream errors only
acli device logs MyApp --level D # filter to one tag
acli device logs --clear # clear buffer first
acli device push ./data.json /sdcard/data.json
acli device pull /sdcard/screenshot.png ./local/
acli device screenshot # saves to screenshot-<timestamp>.png
acli device screenshot output.png
acli device record # records to screenrecord-<timestamp>.mp4
acli device record --duration 30 demo.mp4
acli device reboot
acli device reboot --bootloader # into fastboot mode
acli device reboot --recovery
acli device forward 8080 8080 # host:8080 → device:8080
acli device reverse 3000 3000 # device:3000 → host:3000
acli device pair 192.168.1.5:37000 # Android 11+ wireless pairing
acli device connect 192.168.1.5:5555
acli device info # model, OS version, ABI, serialacli app list # all packages
acli app list --third-party # user-installed only
acli app list --filter myapp
acli app launch com.example.app
acli app launch com.example.app --activity .MainActivity --wait
acli app stop com.example.app
acli app clear com.example.app # wipe data + cache
acli app grant com.example.app android.permission.CAMERA
acli app revoke com.example.app android.permission.CAMERA
acli app deep-link "https://example.com/product/123"acli automatically walks up from the current directory to find the project root (the directory containing settings.gradle or build.gradle).
acli build assemble # debug APK
acli build assemble --variant release
acli build assemble --module :feature:login --variant debug
acli build test # unit + instrumented tests
acli build test --unit
acli build test --instrumented # requires connected device
acli build clean
acli build lint
acli build lint --fix
acli build bundle --variant release # Android App Bundle (.aab)
acli build run dependencies # arbitrary Gradle task
acli build run :app:generateDebugSourcesThe device must be in fastboot/bootloader mode first (acli device reboot --bootloader).
acli flash list # devices in fastboot mode
acli flash image boot boot.img
acli flash factory ~/Downloads/shiba-factory.zip
acli flash reboot # back to Android
acli flash reboot --bootloader
acli flash unlock # OEM bootloader unlock (destructive — prompts for confirmation)# Battery simulation
acli instrument battery --level 15
acli instrument battery --status discharging
acli instrument battery --level 5 --status discharging
acli instrument battery --reset # restore real values
# Network simulation (emulators only)
acli instrument network --speed edge
acli instrument network --speed gprs --latency umts
# GPS mock location
acli instrument location --lat 37.7749 --lng -122.4194
# Input events
acli instrument input text "Hello World"
acli instrument input tap 540 960
acli instrument input key KEYCODE_HOMEacli skills install # project scope (.claude/skills/acli/SKILL.md)
acli skills install --scope user # user scope (~/.claude/skills/acli/SKILL.md)
acli skills list # show installation statusacli doctor # human-readable checklist
acli doctor --json # machine-readable (for CI)acli update check # compare current vs latest
acli update install # download and replace binary
acli update install --version v1.2.0 # install a specific versionacli completion bash > /etc/bash_completion.d/acli
acli completion zsh > "${fpath[1]}/_acli"
acli completion fish > ~/.config/fish/completions/acli.fishThese flags work with every command:
| Flag | Description |
|---|---|
-d, --device <serial> |
Target a specific device by ADB serial. Overrides $ACLI_DEVICE |
--json |
Emit all output as JSON to stdout; errors go to stderr |
-v, --verbose |
Show underlying error details and subprocess output |
--no-color |
Disable color output (auto-disabled when not a TTY) |
Device targeting is resolved in this order: --device flag → $ACLI_DEVICE env var → default_device in ~/.acli/config.yaml.
Pass --json to any command for machine-readable output. This is useful for CI pipelines and AI agents.
# List devices as JSON
acli device list --json
# [{"serial":"emulator-5554","state":"device","model":"sdk_gphone64_arm64",...}]
# Check environment health in CI
acli doctor --json
# {"checks":[{"label":"ANDROID_HOME is set","ok":true,"detail":"..."},{"label":"adb found","ok":true},...]}
# List installed SDK packages
acli sdk list --installed --jsonError format — all errors are written to stderr as structured JSON when --json is active:
{
"error": {
"code": "device_not_found",
"message": "No Android device or emulator is connected.",
"detail": "ADB cannot find a target device...",
"fix": ["acli device list", "acli avd start <avd-name>", "adb start-server"],
"docs": "https://developer.android.com/tools/adb#devicestatus"
}
}Exit codes are POSIX-standard and consistent:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Usage error (bad arguments or flags) |
| 3 | Device not found or ambiguous |
| 4 | SDK / environment not configured |
| 5 | Build failure |
| 6 | Process timeout |
acli ships with a built-in Claude Code skill that gives AI agents native control over the Android environment.
# Install for the current project (committed to .claude/skills/)
acli skills install
# Or install globally for all your projects
acli skills install --scope userOnce installed, Claude Code will automatically use acli commands when you ask Android-related questions, or you can invoke it directly with /acli. The skill grants Bash(acli *) permission so the agent can run any acli subcommand without individual approval prompts.
The skill template is also available at assets/skills/acli/SKILL.md.
- Go 1.22 or newer (
go version) make(optional but recommended)- An Android SDK installation for manual testing
git clone https://github.com/ErikHellman/android-cli.git
cd acli
# Download dependencies (no network needed after this)
go mod download
# Build the binary into dist/
make build
# or: go build -o dist/acli ./cmd/acli
# Run all unit tests
make test
# or: go test ./... -v
# Install to $GOPATH/bin
make install| Target | Description |
|---|---|
make build |
Build dist/acli with version info from git describe |
make install |
Build and install to $GOPATH/bin |
make test |
Run all tests with -v -count=1 |
make lint |
Run go vet ./... |
make clean |
Remove dist/ |
make release |
Cross-compile for all platforms into dist/ |
make doctor |
Print Go version and module info |
# All tests
go test ./...
# Specific package
go test ./pkg/aclerr/... -v
go test ./pkg/runner/... -v
go test ./pkg/output/... -v
# With race detector
go test -race ./...The unit tests in pkg/ cover:
pkg/aclerr— all 15 error catalog patterns,AcliErrormethods, exit code mappingpkg/runner— subprocess capture, passthrough, env, stdin, timeout, working directory, binary-not-foundpkg/output— JSON error format, JSON table schema, JSON checklist, human error rendering, nil error safety
Integration tests (requiring a real Android SDK) are not automated but can be exercised manually with acli doctor after configuring $ANDROID_HOME.
Adding a new command:
- Create a
new<Name>Cmd() *cobra.Commandfunction in a file underinternal/cmd/. - Register it in
internal/cmd/root.goinsideRootCmd.AddCommand(...). - If the command needs underlying Android tooling, add a service method in the appropriate
internal/<domain>/service.go.
Adding a new error pattern:
- Add a constant to
pkg/aclerr/errors.goif a newErrorCodeis needed. - Add an
ErrorPatternentry to thecatalogslice inpkg/aclerr/catalog.go. - Add a test case to
pkg/aclerr/errors_test.go.
Changing output format:
All rendering goes through pkg/output. The Renderer methods branch on r.JSON for machine vs. human output, so changing one path does not affect the other.
make releaseThis produces binaries in dist/ for:
acli-darwin-arm64(macOS Apple Silicon)acli-darwin-amd64(macOS Intel)acli-linux-amd64acli-linux-arm64acli-windows-amd64.exe
The version string displayed by acli --version and used by acli update check is injected at build time via -ldflags:
-ldflags "-X main.version=$(git describe --tags) -X main.commit=$(git rev-parse --short HEAD)"
make build and make release handle this automatically. If built outside of make without -ldflags, the version will be reported as dev.
Releases are fully automated via .github/workflows/release.yml. The only manual step is tagging.
1. Ensure main is green
Confirm the CI workflow is passing on main before tagging.
2. Create an annotated tag
git tag -a v1.2.3 -m "Short description of what changed"Use semver with a v prefix. The tag message becomes the seed for the release notes (the workflow also appends the commit log since the previous tag).
3. Push the tag
git push origin v1.2.3This is the trigger. Pushing the tag starts the release workflow automatically — no further action is required.
What the workflow does:
- Runs
make test— the release is aborted if any test fails - Runs
make release— cross-compiles 5 platform binaries intodist/ - Generates a
checksums.txt(all binaries) and individual<binary>.sha256sidecars (used byacli update install) - Generates release notes from
git log <prev-tag>..HEAD --oneline --no-merges - Creates a GitHub Release named after the tag and uploads all artifacts
Verifying the release
After the workflow completes (~2 minutes), check the Releases page and confirm:
- All 5 binaries are present
checksums.txtand.sha256sidecars are attachedacli update checkreports the new version
android-cli/
├── cmd/
│ └── acli/
│ └── main.go # Entry point; injects version/commit
├── internal/
│ ├── cmd/ # Cobra command definitions (one file per domain)
│ │ ├── root.go # Root command, global flags, PersistentPreRunE
│ │ ├── sdk.go # acli sdk *
│ │ ├── avd.go # acli avd *
│ │ ├── device.go # acli device *
│ │ ├── app.go # acli app *
│ │ ├── build.go # acli build *
│ │ ├── project.go # acli project init
│ │ ├── flash.go # acli flash *
│ │ ├── instrument.go # acli instrument *
│ │ ├── skills.go # acli skills *
│ │ ├── doctor.go # acli doctor
│ │ └── update.go # acli update *
│ ├── sdk/service.go # sdkmanager wrapper + output parser
│ ├── avd/service.go # avdmanager + emulator wrapper
│ ├── device/service.go # adb wrapper + device list parser
│ ├── build/service.go # gradlew wrapper + project root discovery
│ ├── flash/service.go # fastboot wrapper
│ └── instrument/service.go # adb shell instrumentation commands
├── pkg/
│ ├── aclerr/
│ │ ├── errors.go # AcliError type, ErrorCode constants, exit codes
│ │ ├── catalog.go # 15+ regex patterns → structured errors
│ │ └── errors_test.go
│ ├── output/
│ │ ├── output.go # Renderer: TTY detect, lipgloss panels, JSON mode
│ │ └── output_test.go
│ ├── runner/
│ │ ├── runner.go # Subprocess manager: capture, passthrough, timeout
│ │ └── runner_test.go
│ ├── android/
│ │ └── locator.go # SDK root discovery, binary path resolution
│ ├── config/
│ │ └── config.go # Viper config (~/.acli/config.yaml)
│ └── update/
│ └── updater.go # GitHub Releases API + atomic binary replacement
├── assets/
│ └── skills/
│ └── acli/
│ └── SKILL.md # Claude Code skill template
├── dist/ # Built binaries (gitignored)
├── go.mod
├── go.sum
└── Makefile
acli reads ~/.acli/config.yaml and environment variables prefixed with ACLI_. Environment variables take precedence over the config file.
# ~/.acli/config.yaml
# Default device serial to target when --device is not specified.
# Equivalent to setting $ACLI_DEVICE in your shell.
default_device: "emulator-5554"
# Override Android SDK root (normally auto-discovered).
# Equivalent to $ANDROID_HOME.
sdk_root: ""
# GitHub repository used for self-update checks.
github_repo: "android-cli/acli"Zsh:
acli completion zsh > "${fpath[1]}/_acli"
# Restart your shell or: autoload -U compinit && compinitBash:
acli completion bash > /etc/bash_completion.d/acli
# or for a single user:
acli completion bash > ~/.bash_completionFish:
acli completion fish > ~/.config/fish/completions/acli.fishacli update check # prints current version vs. latest GitHub release
acli update install # downloads and atomically replaces the current binaryThe update command:
- Queries the GitHub Releases API for the latest release
- Downloads the asset matching the current OS and architecture
- Verifies the SHA256 checksum (if a
.sha256asset is present) - Atomically replaces the running binary
acli intercepts raw tool output and maps known failure modes to actionable messages. For example, when a Gradle build runs out of memory:
Before (raw Gradle output):
> Task :app:compileDebugKotlin FAILED
...
java.lang.OutOfMemoryError: Java heap space
at ...50 lines of stack trace...
After (acli):
╭─ Error: out_of_memory ──────────────────────────────────╮
│ │
│ Gradle ran out of memory. │
│ │
│ The JVM heap was exhausted during the build. Increase │
│ the heap size in gradle.properties. │
│ │
│ Try: │
│ echo 'org.gradle.jvmargs=-Xmx4g' >> gradle.properties│
│ acli build assemble │
│ │
╰───────────────────────────────────────────────────────────╯
In --json mode the same error is emitted to stderr as structured JSON, making it trivially parseable in CI or by an AI agent.
The error catalog covers: device not found, multiple devices, unauthorized device, device offline, APK install failures (version conflict, insufficient storage, and others), SDK license not accepted, SDK package not found, network errors, AVD not found, emulator port in use, Gradle build failures, Gradle OOM, and Gradle wrapper not found.