auto-device-lock
Skill7 starsChanged 3 months agoMITView on GitHub
Use at the start of every session, and before any command that touches physical hardware — adb, fastboot, scrcpy, screenshots, screen recording, esptool/idf.py/platformio, a serial console, xcrun devicectl — on a machine where more than one editor window or agent session may be running; when phones, dev boards or test devices are shared between projects; when a device behaves as though something else is driving it; when a lock looks stale after a crash; or when the user says \"/auto-device-lock\", mentions device conflicts, contention, or which phone a session should use.
In this repository18 other files it gives its agents.
- auto-agent-ecoSkill
- auto-agent-maxSkill
- auto-audit-securitySkill
- auto-auditSkill
- auto-balanceSkill
- auto-brand-paritySkill
- auto-commentSkill
- auto-docSkill
- auto-everythingSkill
- auto-issue-fixSkill
- auto-license-checkSkill
- auto-media-makerSkill
- auto-media-onboardingSkill
- auto-media-stingerSkill
- auto-rewriteSkill
- auto-skill-updateSkill
- auto-ui-uxSkill
- auto-webSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 6 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 50
Run this first, every session, without being asked:
- Distinctive: line 60
If this window used a device before, take it back:
- Distinctive: line 84
A multi-device claim is all-or-nothing: Ask for three phones and you either get all three or you get none and nothing is left held.
- Very specific: line 124
Use check as a shell condition rather than something you read and interpret.
- Distinctive: line 143
Every grant carries a monotonically increasing epoch.
- Distinctive: line 171
Do not release a device a background job is still using.
- Very specific: line 226
Check state, not just presence.
- Distinctive: line 288
A lock only binds the sessions that run it: If another window is on an older copy of this skill, or has no lock at all, it will take your …
- Very specific: line 299
Check python3 -V returns a version before relying on the helper in an unattended run.
- Very specific: line 301
If devlock is not installed at all, do not improvise a lock file format — a second, incompatible scheme is worse than none.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
/auto-device-lock
Claim a physical device before you touch it, hold it for as long as you need it, and give it back — so that five windows working on five apps can share three phones without corrupting each other's tests, and so the ESP32 that one project is flashing is not opened by another project mid-write.
The standing law: no device operation without a lease you have verified in this same command. Not "I claimed it earlier in this session" — earlier is not now. The gap between those two is where another session's flash lands in your board.
Everything here is built on one fact about how agent sessions run: every Bash call is a fresh
bash -c that exits within milliseconds, while the session itself lives for hours. A lock held by
the shell dies instantly; an environment variable exported in one call is gone by the next. So the
lease is bound to the session's own long-lived process, and the shell bindings are re-established on
every call that uses them.
The helper is devlock, shipped in this skill's directory. If it is missing — a user may have
installed only SKILL.md — say so and fall back to Step 8 rather than inventing a lock format.
It is not on PATH by default. Resolve it once at the top of the session and reuse that path, or
offer the user the one-time symlink:
DEVLOCK="$(ls -d ~/.claude/skills/auto-device-lock/devlock \
~/.agents/skills/auto-device-lock/devlock 2>/dev/null | head -1)"
ln -sfn "$DEVLOCK" ~/.local/bin/devlock # optional, once, if ~/.local/bin is on PATHThe examples below say devlock; use "$DEVLOCK" if you did not make the link.
Subagents never claim and never release. This rule is load-bearing, so it comes before the
mechanism. Every in-process subagent runs under the same claude process as its parent, so the
lease's owner triple is identical for all of them — it proves the session is alive, and it cannot tell
two subagents apart. The practical consequence: one subagent's release --all frees a device another
subagent is mid-flash on, and the registry will agree that was legitimate. The foreground session
claims and hands each subagent the serial to use; a subagent that believes it needs a device it was
not given should say so and stop, not claim one.
A nested claude -p spawned from a Bash call is the opposite case — it is its own process, so the
ancestry walk stops at the inner claude and it becomes a separate holder that contends with its
own parent. Pass the device down and let it use -s.
Step 0 — Every Session, Before Anything Else
Run this first, every session, without being asked:
devlock status # who holds what; reaps leases whose sessions died
devlock devices # what is attached, and its canonical idstatus prunes dead holders as a side effect, so a crashed session's phone is available again by the
time you have read the output. There is no cleanup command to remember and no daemon to be running.
If this window used a device before, take it back:
devlock affinity # what this window/repo used lastAffinity is not a nicety. A test suite whose screenshots come from a 6.1" phone on Monday and a tablet on Tuesday produces diffs that are all noise, and a recording that intercuts two devices looks like a bug report about the product. Same window, same repo, same device — unless someone else has it.
Step 1 — Claim Before You Touch
devlock claim <id> [<id>...] --why "what you are doing" # all or nothing
devlock claim <id> --wait 300 --why "..." # queue behind the holderName the devices by canonical id (devlock devices prints them). A serial, a /dev/tty*, a
by-id name or a model also resolve, and an ambiguous one is rejected rather than guessed.
--why is required in practice even though the flag is optional. It is the entire content of the
message another session sees when it collides with you, and "held by pid 41232" with no reason is what
makes a person kill a lock they should have waited for.
A multi-device claim is all-or-nothing. Ask for three phones and you either get all three or you get none and nothing is left held. This is not politeness; it is what stops the deadlock where you hold phone A waiting for phone B while another session holds B waiting for A. Devices are acquired in canonical-id order, which is the same order in every session, so a cycle cannot form.
Claiming a device you already hold is not an error and does not change your epoch. Run it twice; run it in a subagent. That is deliberate — you will run it twice.
Step 2 — Bind The Shell, Every Single Call
An export does not survive to your next Bash call. So every call that touches a device begins by
re-establishing the binding:
eval "$(devlock env)" || exit 1
adb -s "$ANDROID_SERIAL" shell screencap -p /sdcard/shot.pngdevlock env exits nonzero and exports nothing if the lease is gone, so that || exit 1 is a fence
check as well as a convenience. It is the cheapest correct habit in this skill.
Never run bare adb. With several devices attached it fails loudly, which is fine. With exactly
one attached it silently succeeds against whatever happens to be plugged in — so the command works
today, and on the day a colleague plugs in a second phone it starts flashing the wrong one. -s is
not optional, and ANDROID_SERIAL must come from devlock env, never from memory.
Holding several Android devices at once is supported and expected; in that case devlock env refuses
to pick one for you and prints the per-device lines instead. Pass -s explicitly.
Step 3 — Verify Before Each Batch Of Work
devlock check <id> && adb -s "$ANDROID_SERIAL" shell ... # the gate: one line, one code
devlock verify # everything you hold, one line each
# strict form: devlock env exports one variable per held device, named after
# its canonical id with every non-alphanumeric character replaced by _
eval "$(devlock env)"
devlock verify usb:22d9:EILF85KNDIXKGEDM --epoch "$DEVLOCK_EPOCH_usb_22d9_EILF85KNDIXKGEDM"Use check as a shell condition rather than something you read and interpret. It prints one line and
returns one code, and every non-OK line ends with the exact command that fixes it:
| Code | Meaning | |
| 0 | OK | you hold it, at your epoch, attached and healthy |
| 1 | HELD | someone else holds it |
| 2 | FREE | nobody holds it, including you |
| 4 | POLICY | refused: the device is on never_automate |
| 5 | ABSENT | leased to you but not attached |
| 6 | FENCED | your lease lapsed and was re-granted — your state is stale |
| 7 | UNHEALTHY | attached but not in a state that accepts commands |
check reaps as it runs, so a lease whose session died never blocks you, and a device you lost tells
you why you lost it — crashed, frozen, or deliberately taken.
Bare devlock verify is the one command that answers "am I allowed to touch what I am about to
touch". It exits nonzero if any answer is anything but OK, so it chains: devlock verify && ....
Every grant carries a monotonically increasing epoch. If your session was declared dead — frozen,
suspended, its host asleep, its process stopped — and the device was re-granted to someone else, the
epoch in the registry has moved past yours. verify fails, and that failure is the only thing standing
between a resumed session and a write into hardware that now belongs to another test.
This is the hole every naive lock has: liveness is checked once at claim time, and the holder is assumed live forever after. Check the epoch, not your memory.
verify says | What it means |
OK | you hold it, at your epoch, and it is attached and healthy |
NOT HELD | no lease at all — you never claimed it, or you released it |
STOLEN | someone else holds it now. Stop. Do not touch it |
FENCED | your lease lapsed and was re-granted. Your state is stale; re-claim and re-check |
ABSENT | leased to you but unplugged right now |
UNHEALTHY | attached but unauthorized / offline / recovery, not device |
Step 4 — Release, Including When It Goes Wrong
devlock release <id> # or: devlock release --allRelease when the work is done, not when the turn ends — but a crash needs no cleanup at all. A lease
is valid only while the process that took it is alive, proven by boot id, PID and process start
time. Kill the session and the lease evaporates on the next status, claim or devices from anyone.
There is no TTL to tune, no heartbeat to miss, and no stale lock file to delete by hand.
Do not release a device a background job is still using. The foreground task finishing is not the same event as the device going idle.
Step 5 — When The Device You Want Is Taken
In order:
- Use a different one.
devlock devicesshows what is free. Note in your report that the screenshots came from a different device than usual. - Wait.
--wait <seconds>queues with jittered backoff, and reports who is ahead of you. - Ask the human. Show them the holder, the repo and the reason.
- Steal — only with a human's explicit yes:
devlock steal <id> --reason "..." --confirm--confirm exists so that stealing cannot happen by reflex. A steal breaks a live session's test
mid-run; the victim finds out on its next verify, which is exactly why Step 3 is not optional.
Never work around a lock you could not get. Unplugging the device, restarting the adb server, or
telling yourself the other session looks idle are all the same mistake with different steps.
adb kill-server in particular drops every session's device at once, and the other sessions find out
by their command failing halfway through.
Step 6 — Identity Is The Part That Silently Goes Wrong
A lock keyed on an unstable name is worse than no lock, because it reports success while protecting
nothing. devlock keys on usb:<vendor>:<serial> derived from sysfs, and that choice is load-bearing:
transport_idis never identity. It is reassigned on every replug and everyadb reboot.- One physical device can appear in two namespaces at once. A phone exposing a CDC interface is
both an adb serial and a
/dev/ttyACM*. Lock it as a tty in one session and as an adb serial in another, and both sessions believe they have exclusive access to the same hardware. The canonical id collapses those into one lease covering the hardware, not the cable. /dev/serial/by-idis not unique. Two identical CH340 or CP2102 adapters commonly report the same serial, and some boards have noby-identry at all. Where the canonical id cannot be formed,devlockfalls back toID_PATH— a physical port, so moving the cable moves the identity, which is the honest trade.by-pathlists the same tty twice (usb-andusbv2-). Deduplicate by resolved device.- A board that reboots into a different USB personality after flashing — a CDC firmware replacing a UART bridge — comes back with a different id. Re-enumerate rather than trusting the pre-flash name.
- Network adb, emulators and iOS live in their own namespaces (
net:,emu:,ios:) because their identity has different failure modes.adb tcpipchanges a device's serial mid-session: re-claim afterwards rather than assuming the lease followed it. - Placeholder and short serials are common. A literal
0123456789ABCDEF, or a six-digit counter, is not unique to a unit.devlock devicesflags these; when you see that warning, plug in a second unit of the same model and confirm the ids differ before trusting the lock on either. ttyACM0is a recycled minor number, not a name. The same board can bettyACM0today andttyACM1after a reboot while its canonical id holds. Never key anything on the number.- An empty
adb devicesdoes not mean the phones are gone. It usually means another adb server owns them. Do not conclude a device is absent from an empty list, and do not "fix" it by killing the server.
Check state, not just presence. unauthorized, offline, recovery and sideload accept some
commands and silently drop others; devlock claim refuses them unless you pass --allow-unhealthy.
adb root, adb reboot bootloader, recovery and sideload each move the device into a different
namespace — fastboot devices is not adb devices — so re-claim after the hop rather than assuming
the lease followed it.
Environment
| Variable | |
DEVLOCK_DIR | Where the lease registry lives. Default $XDG_RUNTIME_DIR/auto-device-lock. Point it somewhere scratch to test without touching real leases |
DEVLOCK_POLICY | Path to the policy file, overriding ~/.local/state/auto-device-lock/policy.json |
DEVLOCK_SESSION_PID | The process whose life the lease is bound to. Only for a detached child that has no agent ancestor to find |
ANDROID_SERIAL, DEVLOCK_PORT, DEVLOCK_EPOCH_* | Written by devlock env, read by you. Never set them by hand — ANDROID_SERIAL set by hand routes around the lock entirely |
devlock devices runs adb devices, which starts an adb server if none is running. That is
normally what you want, but it is a side effect: on a machine where the policy is one server, run it
after the server is up rather than letting an arbitrary session start it.
Step 7 — Devices That Must Never Be Automated
Some hardware is off limits — a tablet that is someone's daily driver, a board wired into a rig, a
phone with a customer's data on it. Record that once, in ~/.local/state/auto-device-lock/policy.json:
{ "never_automate": ["usb:04e8:R52Y5057J4Y"],
"aliases": { "test-phone": "usb:22d9:EILF85KNDIXKGEDM" } }claim refuses a forbidden device outright. This is the right place for a rule the user has already
told you once — a preference that lives only in a conversation is a preference that gets violated in
the next session.
Step 8 — Always-On, And What To Do Without The Helper
The skill fires on its description, which covers the common cases. To make it genuinely unconditional, offer to install the two hooks — and ask before editing the user's settings:
// ~/.claude/settings.json
"SessionStart": [ { "hooks": [ { "type": "command", "timeout": 10,
"command": "~/.claude/skills/auto-device-lock/devlock-hook session-start # auto-device-lock" } ] } ],
"PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "timeout": 10,
"command": "~/.claude/skills/auto-device-lock/devlock-hook pre-bash # auto-device-lock" } ] } ]The first prints the device situation into every session's context. The second denies a Bash call that reaches hardware this session has not leased, with a message naming the command to fix it. That is what turns a convention into something an agent cannot absent-mindedly skip. Both fail open: any internal error allows the command through, because a guard that blocks work when it breaks is a guard the user disables within the hour.
Set the timeout and keep the # auto-device-lock marker. Hook timeout defaults are not uniform
across events and can be far longer than you would guess — a hook with none set was measured being
waited on for two full minutes. The marker is how you find these entries again to remove them, since
this skill ships no installer.
Note the entry point is devlock-hook, not devlock. It is a shell prefilter that matches the
command against the device tools with a case and only starts Python when one hits — 3 ms on an
ordinary command instead of 55 ms. A guard that taxes every git status gets turned off, and a guard
that is off protects nothing.
A lock only binds the sessions that run it. If another window is on an older copy of this skill, or has no lock at all, it will take your device without ever seeing your lease. When that is possible, say so in your report rather than implying the hardware was protected — and get the skill installed everywhere before relying on it. Two schemes that cannot see each other are worse than one everybody runs, because both report success.
Two environment variables route around the lock entirely: ANDROID_SERIAL selects a device without
-s, and ANDROID_ADB_SERVER_PORT points at a different adb server whose devices this registry
knows nothing about. Only ever set ANDROID_SERIAL from devlock env, and never set the port.
On a stock macOS, python3 may be a Command Line Tools stub that pops a GUI installer when invoked.
Check python3 -V returns a version before relying on the helper in an unattended run.
If devlock is not installed at all, do not improvise a lock file format — a second, incompatible
scheme is worse than none. Announce the constraint, ask the user which device this window owns, use
-s with that serial exclusively, and say in your report that arbitration was manual.
Working With The Other Skills
/auto-balance decides how much of the machine this session may use — cores, memory, how many
agents. This skill decides which hardware it may touch. When both apply, balance sizes the session
and this one claims the devices; neither overrides the other.
Rationalization Table
| Excuse | Reality |
"Only one device is plugged in, adb shell is unambiguous" | Today. The day a second appears, that command silently drives the wrong phone |
| "I claimed it at the start of the session" | Earlier is not now. verify with your epoch, or you are guessing |
| "I exported ANDROID_SERIAL already" | Every Bash call is a new process. It is gone. eval "$(devlock env)" |
| "The lock file is stale, I'll delete it" | There are no stale locks. If it is still listed, its session is alive |
| "The holder crashed, so I must clean up" | A dead holder's lease is already invalid. Run devlock status and take it |
| "The other session looks idle" | Idle is not finished. It may be waiting on a build or a human |
| "I'll restart the adb server to clear this up" | That drops every session's device mid-command |
| "I only need it for one screenshot" | One screenshot during another session's install is what corrupts the install |
| "I'll unplug it and plug it back in" | Same as stealing, minus the record of who did it |
| "Two sessions reading logcat can't conflict" | They can and do — and one of them is about to do more than read |
| "by-id is a stable name" | Not for two identical adapters sharing a serial. That is the common case, not the rare one |
| "I locked the tty, so adb is a different device" | It is the same physical unit. That is what the canonical id is for |
| "My subagent released it, so I'm done" | Subagents do not claim or release. They share your PID; the registry cannot tell them apart |
"adb devices is empty, the phone is unplugged" | Another adb server owns it. An empty list is not an absence |
| "The lock said free, so nobody is using it" | Only sessions running this skill appear. A window without it is invisible |
| "I'll claim all four in case I need them" | Four windows are waiting. Claim what you use, release what you finish |
| "The board is only being read, not written" | Opening the port resets most boards. There is no read-only open |
Red Flags — Stop
- Any
adb,fastboot,esptool,idf.py,scrcpyor serial-console command with no lease held - Bare
adbwith no-s, or anANDROID_SERIALyou did not get fromdevlock envthis call - A device operation after a
verifythat returned anything butOK - Deleting, editing or hand-writing anything in the lock directory
adb kill-server, unplugging, or power-cycling a device to get around a lock- Stealing without
--reasonand a human's explicit yes - Claiming a device listed in
never_automate - Probing a serial port by opening it to see whether it is free — that resets the board; use the
registry, or
fuser/lsof - Releasing while a background job of yours is still driving the device
- A subagent running
devlock claimordevlock releaseinstead of using the serial it was handed - Setting
ANDROID_SERIALby hand, or settingANDROID_ADB_SERVER_PORTat all - Concluding a device is gone because
adb devicescame back empty - Trusting a lock on a device whose serial
devlock devicesflagged as weak, without checking - Finishing a session still holding devices you stopped using an hour ago
- Reporting screenshots or recordings without saying which device produced them
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

