adbrent: one Android device per AI agent
iammosab.github.io/adbrent: a short tour, with a simulation you can try.
Stop parallel AI coding agents from fighting over the same Android phone or emulator.
adbrent is a device lock for adb: each coding-agent session automatically gets its own
Android device, and keeps it until the session ends. It is built for and tested with
Claude Code; other agents use it through their PATH.
When several agent sessions run on one machine, they all see the same adb devices list
and end up driving the same phone: one installs an APK while another is halfway through a
UI test. adbrent gives each session its own device. The first adb command a session
runs rents a free device, later commands go to that same device, and any other session
that targets it is refused with a message naming the devices that are free. Agents never
hit adb: more than one device/emulator, and never need to be told which serial to use.

$ adb shell getprop ro.product.model
adbrent: rented 3A021FDJH000XY (Pixel 8) to this session; plain `adb` now goes to it. `adbrent release` when done, `adbrent help` for more.
Pixel 8
$ adb -s R5CT20ABCDE install app.apk
adbrent: R5CT20ABCDE (SM S911B) is already rented by another agent (claude session 3c45fc60 in shop-app, for 4m). Do not use it.
adbrent: free now: emulator-5554 (sdk gphone64 arm64). Use plain `adb`, or `adb -s <serial>`.
adbrent: working for that agent? Get a code from its `adbrent share` and run `adbrent join <code>`.
- Nothing for agents to learn. They keep typing
adb. - No daemon. One 0.7 MB binary; a rental is one small file. A command on a device the session already holds takes about 1.5 ms longer.
- Costs agents almost nothing to read. Nothing is added to a session's context when it starts. A session hears from adbrent once, in one line, when it first rents a device, and after that only if it is refused.
- Self-healing. A session that closes or crashes loses its rental immediately.
- Sees use it does not manage. Before handing out a device, adbrent asks the device
whether anything is running on it over adb. A phone that Gradle, Android Studio, scrcpy
or a plain
adbelsewhere is using is left alone.
Install
You need macOS or Linux, and adb on your PATH (Android SDK platform-tools). adbrent
lives in ~/.adbrent and does not modify your Android SDK.
In Claude Code
Inside a Claude Code session, run:
/plugin marketplace add iammosab/adbrent
/plugin install adbrent@adbrent
Then restart your sessions. The first session to start fetches the adbrent program into
~/.adbrent:
- On a Mac (Apple Silicon or Intel) it downloads the prebuilt binary from this repository's GitHub release and checks it against the SHA-256 recorded in the plugin.
- Anywhere else it builds it with
cargo, which takes about half a minute and needs a recent stable Rust.
If neither works, the session is told that adbrent is not active.
The same two steps from a terminal:
With cargo
The first command builds the binary into ~/.cargo/bin. The second copies it to
~/.adbrent, creates the adb stand-in, and installs the same Claude Code plugin for
you. Use this route for other agents too; adbrent install --shim-only skips the plugin.
Check that it works
Start a new Claude Code session and ask it to run adb shell getprop ro.product.model.
It should report that a device was rented to the session. In a terminal,
should then show that session as the renter, and show the device free again once the session ends. Sessions that were open before the install keep using plain adb until they are restarted.
Other agents and harnesses
Put the stand-in first on the PATH that the agent runs commands with, for example in the shell profile or launch script the agent uses:
The renter is the agent process the command runs under. Codex, opencode, Gemini CLI and
several others are recognised by name; anything else falls back to the nearest parent
process that is not a shell. Set ADBRENT_SESSION=<id> in a session's environment when the harness runs several
sessions inside one process, or when its agents start adb in detached background jobs:
a job cut loose from the agent's process can only be matched to its session by that id.
(Claude Code sessions carry such an id already.)
Update
In Claude Code, /plugin marketplace update adbrent, then restart your sessions: the
next one fetches the matching program. With cargo, repeat the two install commands with
--force on the first.
Uninstall
removes the Claude Code plugin and ~/.adbrent, rentals included. If you installed with
cargo, also run cargo uninstall adbrent.
What agents see
| Command | Behaviour |
|---|---|
adb <cmd> with no device named |
Goes to the session's device, renting a free one first if it has none |
adb -s <serial> <cmd> |
Rents that device if free; refused if another session holds it |
adb devices |
Lists the session's own devices and the free ones |
adb kill-server |
Refused while another session holds a device |
-t <id>, -d, -e and ANDROID_SERIAL are understood the same way. Commands that
address no device (adb version, adb connect, adb forward --list) pass straight
through.
Plain adb hands out a free device that nobody is using, phones and tablets first: a TV,
a watch or a car unit is handed out only when nothing else is free. When a task needs a
particular device, the agent should look at adbrent status and name it with
adb -s <serial>. adbrent help says this in a short section written for agents.
Commands
adbrent status [--json] Devices and who rents each one
adbrent rent [SERIAL] [--wait[=SECONDS]] [--force]
Rent a device and print its serial; --wait queues for one;
--force takes one that looks in use outside adbrent
adbrent watch [SERIAL...] [--once] Print a line each time a device is rented or released;
--once stops at the first release
adbrent release [SERIAL...] Hand back your devices (all of them by default)
adbrent release --force SERIAL Take a device back from whoever holds it
adbrent release --force --all Clear every rental
adbrent share [SERIAL] Print a code that lets another session use your device
adbrent join CODE Use the device shared under CODE alongside its renter
adbrent exclude [SERIAL] Put a device off limits to agents (no SERIAL: list;
--remove SERIAL: undo)
adbrent serve [--port N] Live status page on http://127.0.0.1:7878
adbrent ignore [TEXT] Stop counting on-device commands containing TEXT as
outside use (no TEXT: list; --remove TEXT: undo)
adbrent log [-n N] [-f] [--json] What happened: rentals, refusals, waits, feedback;
-f keeps printing as things happen
adbrent report [--hours N] How agents got on with adbrent, and what to look at
adbrent debug [on|off] Also log every adb and adbrent command agents run
adbrent feedback [TEXT] Record a problem or a suggestion (no TEXT: list them)
adbrent feedback --issue Print a GitHub issue link filled in from the latest one
adbrent env Shell line that puts the adb stand-in on PATH
adbrent install [--shim-only] Set up ~/.adbrent and the Claude Code plugin
adbrent uninstall Remove both
adbrent status is the view for the person in charge:
$ adbrent status
SERIAL MODEL STATE RENTED BY
3A021FDJH000XY Pixel 8 device claude session 3c45fc60 in shop-app, 4m
R5CT20ABCDE SM S911B device nobody, but in use outside adbrent (logcat)
emulator-5554 sdk gphone64 arm64 device -
Waiting for a device
When every device is taken, an agent does not have to keep asking.
- To get the next free device:
adbrent rent --waitreturns holding a device as soon as one is released (adbrent rent <serial> --waitwaits for a particular one, and--wait=600gives up after ten minutes). Run as a background command, it lets the agent carry on and be told the moment it has a device. Refusals say so. - To be told without renting:
adbrent watchprints a line each time a device is rented or released, for examplereleased 3A021FDJH000XY (Pixel 8).adbrent watch --oncestops at the first release, and naming serials limits it to those devices.
Both only read the rental files while they wait, so waiting asks nothing of the devices. Waiting agents are not queued in order: whichever asks first after a release gets the device.
Live status page
opens a page on http://127.0.0.1:7878 that shows every device, who rents it and for how
long, what is using a device outside adbrent, and a feed of what agents ran into
(refusals, forced rentals, errors and feedback), which you can narrow to feedback alone
or widen to everything that happened. A rented device has a Take
back button that asks you to confirm. The page keeps itself current, follows your light
or dark setting, loads nothing from the network, and is served to your own machine only.

Sharing a device with a helper session
A rental belongs to one session. When that session hands part of its work to another session (a delegated task, a helper agent in its own process), the helper would be refused the device. The renter can let it in:
$ adbrent share # in the session that rents the device
4f9a1c
$ adbrent join 4f9a1c # in the helper session
3A021FDJH000XY
The helper can then use that device, with or without naming it, for as long as the rental lasts. It cannot end the rental; when the renter releases the device or its session ends, the sharing ends too. A refused agent is told about this in the refusal itself.
Keeping a device away from agents
puts a device off limits: plain adb never picks it, naming it is refused, and agents do
not see it in adb devices. adbrent exclude lists the excluded devices and
adbrent exclude --remove <serial> brings one back. Use it for your personal phone, or
for a device you want left alone for a while.
Seeing how agents get on with it
adbrent keeps a log on your machine, in ~/.adbrent/log.jsonl, of what happens at the
desk: rentals and releases (with how long each device was held), refusals and why, waits,
forced rentals, shared devices, errors, and anything an agent reports.
-
adbrent logshows the recent entries, andadbrent log -fkeeps printing as things happen, so you can watch agents work. -
adbrent reportsums up the last 24 hours (--hours Nfor another span) and points out what usually needs fixing:$ adbrent report The last 24 hours: 2 sessions, 2 rentals, 4 refusals. Refusals: 3 outside use, 1 rented. Worth a look: - 2 times an agent went back to a device it had just been refused. The refusal may not be clear enough, or the agent has no other device to go to. - 1 time an agent forced its way onto a device after being refused it. Check whether those refusals were wrong (`adbrent log`), and consider `adbrent ignore` for helpers that stay running. What agents told you: - Refused on the Pixel 4a three times, but that test run is my own (claude session 9f12ab07 in chat-app) -
adbrent debug onturns on a trace for when you want the detail: everyadbcommand and everyadbrentcommand agents run is logged too, with who ran it, which device it went to, whether it was passed on or stopped, and how long adbrent took. It adds about 0.15 ms per command. Command lines can contain what agents type into apps, so turn it off when you are done:adbrent debug off. -
Agents can report a problem themselves with
adbrent feedback "<what happened>". They are told about this in the one refusal that can be mistaken (a device that looks in use outside adbrent), and inadbrent help.adbrent feedbacklists what they wrote. -
adbrent feedback --issueprints a link to a GitHub issue filled in with the latest report and the few log entries before it, for you to read and submit.
None of this is shown to agents, so it costs them nothing, and nothing leaves your machine unless you open that link and submit it.
How it works
~/.adbrent/shim/adb is a symlink to the adbrent binary. Run under that name, it works
out which device the command targets, checks ~/.adbrent/leases/<serial>.json, and then
execs the real adb, so signals, pipes and exit codes behave exactly as before. A rental
records its owner's process id and start time. When that process is gone the rental is
ignored and overwritten by the next renter, so nothing has to clean up after a crash.
The Claude Code plugin lives in plugin/ and adds three hooks. Each goes
through plugin/scripts/hook.sh, which fetches the program when
a session starts without the right version (ADBRENT_NO_FETCH=1 turns that off) and
then hands the hook to it:
- SessionStart registers the session and puts the stand-in on the session's PATH. It adds nothing to the session's context, so sessions that never touch Android pay nothing.
- PreToolUse on MCP tools checks the device named in a tool call (
udid,serial,device_idand similar arguments). Tool servers talk to adb on their own, so this is what keeps them inside the rental. - SessionEnd hands the session's devices back.
Use outside adbrent
Not everything goes through the stand-in. So the first time a session takes a device, adbrent runs one short command on it and looks at two things:
- Commands running over adb right now. Every adb stream (a shell,
logcat, a test run, scrcpy's server) is a child of the device'sadbd, whichever program on the computer started it. If there is one, the device is in use: plainadbskips it, and naming it is refused with the reason.adbrent rent --force <serial>overrules that. - When the device last saw input. A device touched in the last two minutes is handed out only when no quieter one is free. It is never refused for this alone.
Two things are set aside, because they are not someone else using the device:
- Leftovers from a rental. Device tools often leave a helper running after the session that started it has ended (a screen-mirroring agent, a test instrumentation). adbrent remembers when each device was rented, and a command that started during a rental is not counted against the next renter.
- Helpers you tell it to ignore.
adbrent ignore screen-sharing-agentstops counting any on-device command whose command line contains that text.adbrent ignorelists what is ignored andadbrent ignore --remove <text>undoes it.
adbrent status shows the same thing for every unrented device, with the name of what is
running. The check costs 0.3 to 0.7 seconds, once, when a device is first rented; later
commands skip it. Set ADBRENT_NO_PROBE=1 to turn it off.
It sees what is happening at that moment. A program that uses a device in short bursts, with nothing running in between, looks idle between bursts.
FAQ
How do I run multiple Claude Code sessions against different Android devices?
Install adbrent and start the sessions. Each one rents a different connected device or
emulator on its first adb command, with no -s <serial> and no per-session setup. If
there are more sessions than devices, the extra ones are told so, and told to run
adbrent rent --wait in the background: it returns holding a device as soon as one is
released.
Does it work with Android emulators?
Yes. Anything adb devices lists can be rented: USB devices, wireless devices, emulators,
Android TV. adb -e asks for an emulator and adb -d for a USB device.
Does it work with agents other than Claude Code?
It works with anything that runs the adb command, once the stand-in is on that agent's
PATH (see Other agents and harnesses). Claude Code is the
one it has been tested with, and the only one with a plugin that sets this up for you.
Does it work with MCP servers that control devices?
For Claude Code, yes: a hook checks the device named in each MCP tool call, so device-control servers stay inside the rental. A tool that chooses a device without naming it cannot be checked.
What about scrcpy, Gradle and Android Studio?
They are not blocked, but they are noticed. A device with scrcpy, a test run or a logcat
going is treated as in use, and agents are kept off it. Opening scrcpy on a phone is a
quick way to reserve it for yourself.
Is this a device farm?
No. It is a lock, not a lab: it shares the devices plugged into one machine between the agents running on that machine. It has no server, no queue service and no remote access.
What happens when an agent crashes while holding a device?
The rental is tied to the agent's process. When the process is gone, the device is free the next time anyone asks for it.
Limits
- The stand-in covers whatever runs the
adbcommand found on PATH. A program that calls adb by its full path, or speaks to the adb server directly over its socket (Gradle'sinstallDebug, Android Studio), is not stopped from using a rented device, though adbrent notices while it is using one. Point those at a rented device withANDROID_SERIAL=$(adbrent rent). - An MCP tool that picks a device itself, without naming one in its arguments, is not checked.
- Only Claude Code has been tested end to end. Other agents work through PATH and are recognised by name, but have not been tested the same way.
- macOS and Linux only. Windows is not supported.
- Waiting agents are not queued in order: whichever asks first after a release gets the device.
- Inside Claude Code's Bash sandbox, adbrent can read rentals but cannot record new ones
unless
~/.adbrentis writable (sandbox.filesystem.allowWrite). A command whose rental cannot be recorded is stopped, with that fix in the message, because running it would leave the device looking free to everyone else.ADBRENT_ALLOW_UNRECORDED=1restores the old behaviour of warning and running anyway. - Rentals are local to one machine and one user account.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
adbrent: cannot find the real adb on PATH |
adb is not installed, or only the stand-in is on PATH. Install platform-tools, or set ADBRENT_REAL_ADB=/path/to/adb. |
| A Claude Code session still drives any device | The session started before adbrent install; restart it. which adb inside the session should print ~/.adbrent/shim/adb. |
| A device stays rented by a session that is idle | The session is still open. Close it, or run adbrent release --force <serial>. |
| A device is reported in use outside adbrent, and you know it is not | Something left a command running on it; the message and adbrent status name it. Stop it, tell adbrent to disregard it with adbrent ignore <part of its name>, or run adbrent rent --force <serial>. |
cannot record a rental ... so the command was not run |
adbrent could not write to ~/.adbrent, usually because the session runs sandboxed. Add ~/.adbrent to sandbox.filesystem.allowWrite in your Claude Code settings. |
| A helper or delegated session is refused its parent's device | Run adbrent share in the parent and adbrent join <code> in the helper. |
this adb is an adbrent stand-in that another adbrent took for the real adb |
Two copies of adbrent are on one PATH and no real adb was found after them. Put platform-tools on PATH, or set ADBRENT_REAL_ADB to the real adb. |
| The plugin did not install | Run the two steps yourself: claude plugin marketplace add iammosab/adbrent and claude plugin install adbrent@adbrent. |
| A session says adbrent is not active | The program could not be downloaded or built. Check the network, or install it with cargo as above. |
Development
ADBRENT_HOME moves the state directory and ADBRENT_REAL_ADB names the adb to hand off
to; the tests use both.
License
MIT