kevin/thecrewx

ADB at scale, without wiping the wrong phone

Wrapping around 200 ADB operations taught me that the interesting engineering is not the commands — it is the guardrails and the parsing.

6 min read Device tooling

ADB will do almost anything to an Android device, which is the appeal and the problem. The command you need once a quarter is the one you will never remember, so it lives in a notes file and gets pasted with flags that were right for a different device.

I wrapped about 200 of those operations. Almost none of the difficulty was in the commands themselves.

The output formats are not stable

The first real surprise: ADB output changes between Android versions. Field order shifts, labels get renamed, sections appear and disappear. Code that scrapes dumpsys with a regex works perfectly against the one device you tested on.

The fix was to stop parsing at the call site. All parsing lives in one layer that knows about version differences:

class PackageParser:
    def parse(self, raw, api_level):
        if api_level >= 31:
            return self._parse_modern(raw)
        return self._parse_legacy(raw)

Operations ask for structured data and never see raw text. When a new Android version breaks something, one file changes.

Never guess which device

The most important design decision is also the smallest. With multiple devices attached, ADB defaults to the first serial. Convenient, and eventually catastrophic.

So: if exactly one device is attached, use it. If more than one is attached, ask — always, with no flag to skip it for destructive operations. Selecting a target is not a step worth optimising away.

Classify operations, do not check them individually

My first attempt put confirmation prompts at each dangerous call site. That works until you add the twentieth operation and forget one.

Danger became metadata instead:

@operation(name='packages.clear', danger=DESTRUCTIVE)
def clear_package_data(device, package):
    ...

The runner reads the class and handles confirmation. A new destructive operation is guarded the moment it is registered, because being guarded is not something the author has to remember to do.

Confirmations must name the target

Are you sure? [y/N] is worthless. People answer y reflexively — I do too.

A useful confirmation states the specific consequence and the specific target:

Clear all data for com.example.app
on SM-G991B (R5CN20XXXXX)?

This cannot be undone. Type the package name to continue:

Typing the package name is mildly annoying, which is exactly the point. Friction proportional to consequence.

Say what actually happened

Reporting Done! is a small betrayal. Real output means counts, paths, and anything skipped:

pulled 47 files (312.4 MB) to ./backup/DCIM
skipped 3 files (permission denied)
  /storage/emulated/0/Android/data/...

Now the result is verifiable, and the skipped files are visible instead of quietly missing from a backup you were relying on.


The toolkit is phonesurgeon. The guardrail model is the part I would keep if I rewrote everything else.

All writing Next: A framework beats twelve scripts