Skip to content
Development
Skill

/android-emulator-harness

Bring up an Android app in a headless emulator on Linux/WSL and drive it for automated integration testing, the Android analog of Playwright for web. Boots an AVD under KVM, installs an APK (standalone or dev-client+Metro), drives the UI with Maestro (resilient

From plugin
simiancraft-skills
716 skills4 agents
Install
$ npx -y skills add simiancraft/simiancraft-skills --skill android-emulator-harness --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/android-emulator-harness

Context preview

The summary Claude sees to decide when to auto-load this skill.

Bring up an Android app in a headless emulator on Linux/WSL and drive it for automated integration testing, the Android analog of Playwright for web. Boots an AVD under KVM, installs an APK (standalone or dev-client+Metro), drives the UI with Maestro (resilient

SKILL.md

android-emulator-harness.SKILL.md
name: android-emulator-harness
description: >-
  Bring up an Android app in a headless emulator on Linux/WSL and drive it for
  automated integration testing, the Android analog of Playwright for web.
  Boots an AVD under KVM, installs an APK (standalone or dev-client+Metro), drives
  the UI with Maestro (resilient tap-by-label/wait/assert), and gates on logcat +
  screenshots that come back for inspection. Use for ANY Android/Expo/React-Native
  project (not just one app) when the task is "run the app on an emulator", "drive
  the Android UI", "smoke-test a screen", "reproduce a tap-and-crash", "automate an
  Android flow", or "set up Android UI testing". Project-agnostic base; layer a
  domain skill on top for specialized inputs (see android-emulator-mask-testing for
  camera/segmentation). Validated on Linux/WSL with KVM, Maestro 2.x, and JDK 17.

Android Emulator Harness (headless, WSL/Linux, Maestro-driven)

Specializations (e.g. camera/mask) sit ON TOP of this kernel and override only the parts they must.

> **Runtime/package manager.** Examples use `bun`/`bunx`; substitute your own > runner (`npm`/`npx`, `pnpm`/`pnpm dlx`, or `yarn`) wherever they appear. The > Android tooling itself (`adb`, `emulator`, Maestro, the JDK) is unaffected.

Environment gotchas (verify once per machine)

  • **KVM group.** x86/x86_64 emulation needs `/dev/kvm`; the user must be in the

`kvm` group: `sudo gpasswd -a $USER kvm` (needs a real terminal for the password). No relogin if you launch under `sg kvm -c "..."`. Symptom if missing: `x86_64 emulation currently requires hardware acceleration!`.

  • **JDK 17 must be the *active* `java`** for `sdkmanager`/`avdmanager`/Maestro. Maestro

2.x aborts on Java 8 with `ERROR: Java 17 or higher is required`; it reads the `java` on PATH, so exporting `JAVA_HOME` alone is not enough; prepend it to PATH. Symptom: bare `~/.maestro/bin/maestro --version` fails until you do.

  • **EAS builds/artifacts.** If `eas` isn't on PATH, install `eas-cli` and use it

directly, or use the Expo MCP `mcp__expo-mcp__*` tools if your host provides them.

  • **GPU.** Under WSL there is usually no GPU passthrough (`/dev/dri` absent); on

native Linux you may have one. Either way the default software GLES is fine for UI/logic; only GPU-compute workloads (e.g. MediaPipe) need the `swangle` override, which the specialized camera skill covers.

  • **Resolve binaries explicitly** and put JDK 17 on PATH; don't trust a stale PATH

`emulator` or the system `java`. This one block gets you to an operating state:

  # ANDROID_HOME varies by OS: Linux (Android Studio default) $HOME/Android/Sdk, macOS $HOME/Library/Android/sdk
  export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
  # JDK 17 home is OS/distro-specific; point this at wherever your JDK 17 lives.
  #   Debian/Ubuntu: /usr/lib/jvm/java-17-openjdk-amd64   macOS: $(/usr/libexec/java_home -v 17)
  export JAVA_HOME="${JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
  export PATH="$JAVA_HOME/bin:$PATH"          # so Maestro's wrapper sees Java 17
  SDK="$ANDROID_HOME"; EMU="$SDK/emulator/emulator"; ADB="$SDK/platform-tools/adb"
  # modern SDKs install to cmdline-tools/latest/bin; older/hand-installed to cmdline-tools/tools/bin
  CLT="$(ls -d "$SDK"/cmdline-tools/latest/bin 2>/dev/null || ls -d "$SDK"/cmdline-tools/*/bin 2>/dev/null | head -1)"
  SDKMGR="$CLT/sdkmanager"; AVDMGR="$CLT/avdmanager"
  MAESTRO="$HOME/.maestro/bin/maestro"
  # preflight: java -version → 17; $ADB version; $MAESTRO --version → 2.x

1. Create an AVD (one-time)

Default to **x86_64** (faster under KVM, widest native-lib coverage). Use 32-bit `x86` ONLY when a required native lib lacks an x86_64 variant (see android-emulator-mask-testing).

yes | "$SDKMGR" "system-images;android-34;google_apis;x86_64" "platforms;android-34"
# device profile must exist in this SDK's catalog; pixel_3 is safe on older SDKs, pixel_6 on newer
echo no | "$AVDMGR" create avd -n harness -k "system-images;android-34;google_apis;x86_64" -d pixel_6 --force

2. Boot headless under KVM

sg kvm -c "nohup $EMU -avd harness \
  -no-window -no-audio -no-boot-anim -no-snapshot \
  -gpu swiftshader_indirect \
  -accel on -port 5554 > /tmp/emulator.log 2>&1 &"
$ADB wait-for-device
for i in $(seq 1 48); do
  [ "$($ADB shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ] && break; sleep 5
done
$ADB shell settings put global window_animation_scale 0   # determinism: kill animations
$ADB shell settings put global transition_animation_scale 0
$ADB shell settings put global animator_duration_scale 0

Snapshot the warmed device once for fast restarts: launch without `-no-snapshot`, let it boot, then future runs reuse the snapshot instead of cold-booting.

3. Acquire + launch the app

APKs come from: a local `./android/gradlew assembleDebug|Release`, or an EAS artifact (`eas build:run -p android --latest` downloads AND installs to the running emulator; `eas build:download --build-id <id>` fetches only; the Expo MCP `mcp__expo-mcp__build_list` works too). Inspect a build's git commit (`eas build:list --json`) BEFORE diagnosing; a stale binary vs fresh JS is a common false bug.

AAPT2="$(ls "$SDK"/build-tools/*/aapt2 2>/dev/null | sort -V | tail -1)"   # newest installed build-tools
PKG=$("$AAPT2" dump badging app.apk | sed -n "s/package: name='\([^']*\)'.*/\1/p")
$ADB install -r -g app.apk            # -g grants runtime perms (CAMERA, etc.) up front
$ADB shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1

**Launch-type fork (check the landing activity):** `$ADB shell dumpsys activity activities | grep topResumedActivity`

  • `.MainActivity` → standalone, JS embedded. Done.
  • `…DevLauncherActivity` → an Expo **dev** build; needs Metro. Start it

(`bunx expo start` in the app dir), `adb reverse tcp:8081 tcp:8081`, then open the dev-client launch URL. The URL is your app's own custom scheme with the `ex

Read more
Ships withsimiancraft-skills

Claude Code skills for the full arc of a change: farm to table, with receipts. Curated Claude Code skills and agents from simiancraft. Most skill collections are grab bags. This one has a spine: it carries a change through its whole life.

Get the whole plugin

Other skills on simiancraft-skills.