Drive an Appium session from the shell. Start a session against an app, then send each Appium command as a single shell invocation -- no client-script scaffolding required.
Install Appium and at least one driver first; aco spawns the appium binary
from your PATH. See the Appium install guide
for details.
npm i -g appium # needs Node.js LTS and npm >=10
which appium # confirm it's on PATH
appium driver install xcuitest # iOS (requires Xcode + command line tools)
appium driver install uiautomator2 # Android (requires Android SDK + JDK)
appium driver doctor xcuitest # verify a driver's toolchainnpm i -g @tai2/aco
# or run without installing:
npx @tai2/aco --helpInstall the aco plugin so Claude Code can drive sessions for you:
claude plugin marketplace add tai2/aco
claude plugin install aco@acoThen ask your agent things like "tap the login button on the simulator" or
"screenshot the current screen" — it shells out to aco.
List iOS Simulators, Android AVDs, and connected real devices. Pass a row's
NAME to --device-name (iOS) / --avd (Android emulator), or its ID to
--udid (real device or simulator).
aco device list # both platforms, only "available" rows
aco device list --platform ios # iOS Simulators + connected iPhones/iPads
aco device list --state all --json # everything, machine-readable# iOS simulator (foreground; Ctrl-C tears it down)
aco session start --platform ios --app /tmp/MyApp.app.zip --device-name "iPhone 15"
# Android emulator
aco session start --platform android --app com.example.app --app-activity .MainActivity --avd Pixel_8_API_34
# iOS real device (requires code signing for the on-device WebDriverAgent build)
aco session start --platform ios --udid <40-char-udid> \
--app /tmp/MyApp.ipa --xcode-org-id ABCDE12345
# Android real device (auto-selected when one is plugged in and no --avd given)
aco session start --platform android --app com.example.app --app-activity .MainActivity
# Background instead of foreground
aco session start --detach --platform ios --app /tmp/MyApp.app
# Forward extra Appium server flags
aco session start --platform android --log-level debug --use-plugins images
# Remote server / device farm: --server-url attaches to a running Appium
# instead of spawning one. --auth sends BASIC auth (only on session creation;
# or via ACO_REMOTE_USERNAME / ACO_REMOTE_PASSWORD).
aco session start --platform android --server-url https://grid.example.com/wd/hub \
--auth user:accessKey --cap deviceName="Pixel 8"
# Device farm with vendor-specific nested caps (LambdaTest's "lt:options",
# BrowserStack's "bstack:options", ...): pass the whole W3C caps object with
# --caps-json (or --caps-json @file). Replaces the aco-built caps; --platform
# is still required.
aco session start --platform android \
--server-url https://mobile-hub.lambdatest.com/wd/hub --auth user:accessKey \
--caps-json '{"platformName":"android","lt:options":{"isRealMobile":true,"platformVersion":"13","deviceName":"Pixel 8","app":"lt://APP_ID"}}'- With neither
--udidnor--avd, a connected real device wins; otherwise the first Android AVD is auto-booted. Explicit--udid/--avdalways win. - iOS real devices need
--xcode-org-id(plus optional--xcode-signing-id,--allow-provisioning-device-registration,--updated-wda-bundle-id). --logstreams the Appium log to stdout.aco session start --helplists the forwarded server flags (--log-level,--relaxed-security, the*-timeoutknobs, ...).
--session, --server-url, and --platform are optional with a live local
session; pass them explicitly for remote/grid sessions.
aco source # full page source
aco source --xpath '//XCUIElementTypeButton[@name="Login"]' # filter locally
aco elements # labelled elements + tap selectors
aco elements --json | jq -r '.[].selector' # just the selectors
aco screenshot --out ./shot.png
aco tap --x 100 --y 200
aco tap --selector 'accessibility id:login.button'
aco swipe --direction up # within the default scroll view
aco swipe --direction left --label home.carousel # within a labelled element
aco send-keys --selector 'accessibility id:login.username' --text 'alice' # clears, then types
aco send-keys --label login.username --text '!' --no-clear # append instead
aco scroll-into-view "accessibility id:gestures.row.29" --direction up # swipe until visible
aco actions --gesture "move 200 600 0, down, move 200 200 300, up" # raw W3C pointer
aco actions --type "hello" # raw W3C key
aco element find --using "accessibility id" --value "Login"
aco element click --element <element-id>
aco context list
aco context switch --name WEBVIEW_com.example # enter the web context, then:
aco web url https://example.com # navigate the active WebView
# Explicit targeting (remote/grid):
aco source --session <sid> --server-url http://10.0.0.5:4799 --platform iosEvery mobile: extension the drivers advertise is a first-class command under a
platform namespace. Flags map 1:1 to the driver's params and coerce to the
declared type.
aco ios --help # all XCUITest mobile: extensions
aco android --help # all UiAutomator2 / Android extensions
aco ios swipe --direction up
aco ios scroll --toVisible true --distance 0.5
aco android shell --command "getprop ro.product.model"
aco ios <cmd> --help # see each param's typemobile: names are snake-cased to subcommands (mobile: doubleTap →
aco ios double-tap); --<param> flags keep the driver's camelCase names.
aco mobile list # what the connected driver actually advertises
aco mobile call --name "mobile: swipe" --args '{"direction":"up"}' # unvalidated escape hatchaco session list # startedAt, platform, pid, alive, url, sessionId
aco session list --json
aco session list --prune # delete records for dead servers / crashed children
aco session stop # stop the latest live session
aco session stop --session <sid> # stop a specific one
aco session stop --all # stop every stored sessionInsecure feature 'adb_shell' not enabledwhen callingmobile: shellon Android: start the session with--cap appium:allowInsecure='["adb_shell"]'.appium not found on PATHwhen runningaco session start: install Appium (npm i -g appium) and verifywhich appiumresolves.unknown command (script)fromaco mobile callor a generatedaco ios/aco androidcommand: the connected server runs a driver version that does not expose that extension. Runaco mobile listto see what it advertises.
Tool versions are pinned in mise.toml -- install mise
and run mise install to match.
pnpm install
pnpm dev --help # one-shot run via tsx
pnpm dev:watch --help # rerun on file changes
pnpm typecheck # tsc --noEmit
pnpm test # vitest run
pnpm gen:extensions # regenerate src/data/extensions-*.json from driver source
pnpm build # produce dist/cli.js via tsupSee CLAUDE.md for the driver-snapshot workflow and the rules for adding new
mobile: wrappers.
An example Expo-based AUT (App Under Test) lives under aut/, with one
screen per aco command family.
pnpm aut:install # install AUT deps (~200MB, one-time)
pnpm aut:prebuild # generate aut/ios and aut/android
pnpm aut:build:ios # build .app for the iOS Simulator
pnpm aut:build:android # build .apk for an Android emulatorSee aut/README.md for the screen-to-command map.
MIT