Your Playwright tests and AI agents drive it with the browser tools they already use. No Appium, no new tool.
Stock Playwright driving a real iOS Simulator from another machine — the same DOM an AI agent drives too.
Get started · What it does · Using it · Docs · Known limits
Needs a Mac with Xcode (and the Android SDK for Android) — devicedeck doctor says what's missing.
# 1. Install, then start DeviceDeck with your app build (.app for a simulator, .apk for an emulator)
curl -fsSL https://open.devicelab.dev/install/devicedeck | bash
export PATH="$HOME/.devicedeck/bin:$PATH" # or open a new terminal
devicedeck --app path/to/MyApp.app
# 2. Give Claude Code the browser tool and DeviceDeck's plugin
claude mcp add playwright npx @playwright/mcp@latest
claude plugin marketplace add devicelab-dev/DeviceDeck
claude plugin install devicedeck@devicedeck-marketplace
# then start claude: /mcp should list both playwright and devicedeck
# 3. Ask Claude — it finds your app, boots a simulator, launches it, drives it, writes the test
# "Write a Playwright test that logs in to my app"
# 4. Run it
npx playwright testNeed a simulator build of your app, or the step-by-step? See docs/getting-started.md.
Using Gemini CLI, Codex, VS Code or Cursor? Only step 2 changes — see Other agents.
Prefer to use devices by hand? Open the console at http://127.0.0.1:8787.
In a Playwright project, add DeviceDeck as a dev dependency, so every machine and CI job runs the
same version — no install script, nothing downloaded after npm install:
npm install --save-dev devicedeck
npx devicedeck --app path/to/MyApp.appOr try it without installing: npx devicedeck --app path/to/MyApp.app. For the Claude plugin, which
starts devicedeck mcp itself, install it globally so it is on your PATH: npm install -g devicedeck. Playwright can also start it for you, in playwright.config.ts:
webServer: { command: 'npx devicedeck --app path/to/MyApp.app', url: 'http://127.0.0.1:8787', reuseExistingServer: true },Share your simulators. Run devicedeck on a Mac and everyone on your network can drive its
simulators and emulators from their own browser — tap, type, and Inspect the native UI — with
no Xcode, no Android Studio, and no device of their own.
Automate them with the tools you already use. The app's native UI is served as an ordinary web page, so Playwright, Cypress and Puppeteer tests drive it by selector, and your AI agent drives it through Playwright MCP — the same browser tool it uses for the web. Nothing mobile-specific to learn.
Record or write a Maestro flow. Use the app by hand in the console and DeviceDeck writes it down as a Maestro flow, with durable selectors — or ask your agent to write one: it explores the app through DeviceDeck, checks every selector on the device as it goes, and runs the result with maestro-runner. Either way the flow replays unchanged on real devices at devicelab.dev.
Set the device up for the test. Dark mode, a GPS location, app permissions granted without the
system prompt, and deep links — from the console, the page URL (?appearance=dark&grant=location),
the HTTP API or the agent's tools.
One binary behind all of it — nothing to fork, no Xcode project to open.
The native UI tree is mirrored as real DOM — accessibility ids become data-testid, element
types become ARIA roles — so stock selectors drive it, whether from an agent or a test:
await page.goto(`/device/${udid}?app=dev.devicelab.testhive`);
await page.getByRole('textbox', { name: 'Username' }).fill('devicelab');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page.getByText('Hello, devicelab!')).toBeVisible();A single Go binary streams the simulator or emulator to the browser and serves that DOM — the same page and selectors drive iOS and Android alike.
The agent drives the device the way it drives any web page: it snapshots the page, reasons over
the tree, and acts by ref — no coordinates, no vision model. An agent with no browser tool does the
same through DeviceDeck's own tools: snapshot, then tap or fill by the refs it returns. Here it
picks the right Add among five identical ones:
In Get started, steps 1, 3 and 4 are the same; only step 2 changes.
| Agent | Step 2 |
|---|---|
| Gemini CLI | gemini extensions install https://github.com/devicelab-dev/DeviceDeck |
| Codex CLI | codex mcp add playwright -- npx @playwright/mcp@latestcodex mcp add devicedeck -- devicedeck mcpnpx skills add devicelab-dev/DeviceDeck |
| VS Code / Copilot | code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'code --add-mcp '{"name":"devicedeck","command":"devicedeck","args":["mcp"]}'npx skills add devicelab-dev/DeviceDeck |
| Cursor, and others | add the same two MCP servers in the agent's settings, then npx skills add devicelab-dev/DeviceDeck — docs/agents.md |
Each agent gets the same three pieces: Playwright MCP to drive the device page, DeviceDeck's device tools to boot devices and launch apps, and its skills for writing Playwright tests, writing and recording Maestro flows, and triaging failures.
Point Playwright, Cypress or Puppeteer at http://127.0.0.1:8787/device/{udid}?app={bundleId},
run one worker per device, and select by data-testid (the app's accessibility id) or by role.
To record a test instead, or start from a template, see docs/testing.md.
The console lists every simulator, emulator and --app build on the Mac. Pick a device to boot and
stream it, Launch an app onto it, and drive it with your mouse and keyboard; Inspect shows
each element's id, role and text — the selectors your tests use; Settings switches dark mode, sets
the location, grants permissions and opens deep links. Teammates open the network address
devicedeck prints. Guide: docs/console.md.
Press Record, use the app, press Stop: DeviceDeck writes it as a Maestro flow with graded, durable selectors — replayable with maestro-runner and unchanged on real devices at devicelab.dev. Guide: docs/flows.md.
With npm: npm install --save-dev devicedeck in a project, or npm install -g devicedeck — see
Or install with npm. The host is a Mac; installing it on Linux (a CI job that
drives a remote Mac, say) is harmless.
The install script puts DeviceDeck in ~/.devicedeck and adds its bin folder to your PATH — no
sudo, and nothing else to install: the Android driver ships inside the binary. Pin a version with
curl -fsSL https://open.devicelab.dev/install/devicedeck | bash -s -- --version 0.1.0.
Or download an archive from Releases and run
it in place (the two sidecars sit beside the binary in bin/):
tar xzf devicedeck-<version>-darwin-arm64.tar.gz
./devicedeck-<version>-darwin-arm64/bin/devicedeckOr build from source (needs the Xcode toolchain for the Swift sidecars): make sidecar && make build.
devicedeck on its own starts the console at http://127.0.0.1:8787. --app takes .app simulator
builds, .apk files, or a folder of them, and installs each the first time it is launched on a
device. On start it prints the console link (and the network address teammates use), your apps, how
to connect Claude Code, and any missing tools with how to fix them.
Everything DeviceDeck writes lives in ~/.devicedeck (set DEVICEDECK_HOME to move it). To
uninstall, delete that folder and the # DeviceDeck line from your shell profile.
- A Mac to host. iOS Simulators,
simctl/CoreSimulator and the Swift sidecars are macOS-only, so the machine that runs the devices is a Mac. Clients can be any OS: the surface is a web page, so people, tests and agents drive it from Linux, Windows or another Mac. - Xcode (Xcode 27 included) with at least one iOS Simulator runtime — iOS 26.2 or newer is strongly recommended; on 18.6 the simulator's render server crashes under repeated capture.
- For Android: the Android SDK, with
adbandemulatoronPATH, and at least one virtual device.
devicedeck doctor checks all of this and says how to fix anything missing.
Early release. Both platforms drive end to end: Playwright logs into and checks out of TestHive on
iOS and Android, an agent writes those tests itself through Playwright MCP, and the console drives any
device by hand. click() and fill() return only once the device has acted, and a filled value is
read back from the device — on iOS and on Android's masked password fields. Agents also work with
DeviceDeck's own tools alone, and Maestro flows — recorded in the console or written by an agent —
replay with maestro-runner on both platforms. It is an ordinary web page, so other browser drivers
(Cypress, Puppeteer) see the same DOM, but this release is tested with Playwright.
It has run on a handful of Macs so far, so expect some first-contact problems — please open an issue with the log folder it prints. See Known limits.
Simulators and emulators only — no real hardware, no camera, biometrics or carrier. Within that, the ceiling is physics, not an artificial limit.
- The server is open by default. It listens on all interfaces (
0.0.0.0:8787), so anyone on your network can view and drive your devices — fine on a trusted office or home network. To limit it, start it with an access token (devicedeck --token <secret>orDEVICEDECK_TOKEN): the links it prints carry the token, and every other request needs it. On shared Wi-Fi you can also keep it to this Mac with--addr 127.0.0.1:8787. A token is a shared secret, not user accounts, and the server speaks plain HTTP — do not put it on the open internet as-is. - A driver that dies without closing its connection holds its device for up to half a minute, until a missed ping releases it — see docs/testing.md.
- Android video is ~13–18 fps and heavier than iOS. The emulator's gRPC screenshot stream offers
no video codec, so every frame is a full PNG rather than an H.264 delta. An H.264 path gives about
twice the frame rate at a tenth of the bandwidth; it is experimental, so opt in with
DEVICEDECK_ANDROID_CAPTURE=h264. Emulators DeviceDeck boots run headless, because macOS throttles an occluded window — and the emulator's window is occluded exactly when you are watching the browser. - A freshly launched app swallows touches for about a second after its screen is already in the
accessibility tree.
POST /app/launchwaits this window out, so a test that launches through it can act at once; a raw terminate-and-launch outside it cannot. - A raw relaunch does not reset app state — a native app stays logged in across
terminate-and-launch.
POST /app/launchwipes the app's data first by default, starting at a first-run screen; pass?reset=noto resume where it was left. - Two-finger gestures are dropped on Android. They work on iOS; the Android driver has no mapping for them, so they are discarded rather than guessed at.
- iOS system alerts in landscape (permission prompts) report their buttons in portrait coordinates, so they cannot be tapped reliably; answer them in portrait.
- Running a Maestro flow needs the device to itself. A device takes one driver at a time: end its
DeviceDeck session (or use another device) before
maestro-runner test. Running flows inside DeviceDeck's own session is planned.
Every command, flag, environment variable and HTTP endpoint: docs/cli-reference.md.
devicedeck doctor checks the tools DeviceDeck needs: Xcode, the iOS runtime, adb, an Android
emulator, Node.js, Claude Code and maestro-runner.
Every run writes a folder under ~/.devicedeck/logs (the path is printed at startup):
devicedeck.log with every request, device event and tool call, runner.log from the device
driver, one log per sidecar and device, and crash.log if the process panics. The terminal shows
only what needs attention; DEVICEDECK_LOG=info or debug shows more there too. The last 20 runs
are kept — attach the folder to an issue.
Claude wrote a web test, or says the folder is empty. It has no DeviceDeck tools: run /mcp in
Claude. If devicedeck is missing, the plugin is not installed — run all three commands of step 2
(marketplace add alone is not enough), then start Claude again. If it is listed but failing, the
devicedeck binary is not on the PATH Claude was started from. Also check that devicedeck is
running with your --app build in another terminal.
DeviceDeck runs on your Mac and keeps everything there: no account, no analytics, no telemetry.
The only request it makes off the machine is a check for a newer release at startup (a GET to
open.devicelab.dev with no device or app data); set DEVICEDECK_NO_UPDATE_CHECK=1 to turn it off.
Apache License 2.0 — see LICENSE. The DeviceLab device agents it installs on
simulators and emulators come with maestro-runner as prebuilt binaries: free to use, not open
source — see LICENSE-BINARIES.md.
maestro-runner for the device drivers, Apache-2.0.
The Swift sidecars derive from baguette (Apache-2.0) and
tapflow (MIT); ATTRIBUTION.md records what
was reused and where it lives.
Built by devicelab.dev
