Skip to content

CLI Reference

All Beetroot subcommands. Every verb accepts --help for full usage. The CLI is built on Typer, so --help renders as boxed sections with color (via Rich); flag and argument tables in this reference mirror the same shape.

After uv tool install, invocations are plain beetroot <verb> — the tool venv puts beetroot directly on your PATH. (Contributors hacking on Beetroot from an editable uv sync checkout use uv run beetroot <verb> instead; see CLAUDE.md.)

Beetroot's path model is Docker-inspired: an instance is any directory on disk containing a beetroot.yaml. The CLI discovers the current instance by walking up from cwd like git walks up to find .git. The cross-instance registry — name → absolute path — lives at ~/.config/beetroot/instances.json (respects XDG_CONFIG_HOME).

Top-level flags

Flag Description
--install-completion Install shell completion for the current shell (auto-detected). Run once per shell.
--show-completion Print the completion script without installing it.
--help Render the top-level help.

See Installation → Shell completion for the recommended setup.

Exit codes

This is the stable v1.0 contract. Scripts wrapping the CLI can rely on these codes:

Code Meaning
0 Success
1 Not found / domain error (instance not in registry, file missing, network error, etc.)
2 Capability error — the backend does not support the requested verb (e.g. up against an adb-adopted device)

create

Initialize a new instance.

beetroot create <name> [--path DIR] [--from-data PATH] [--lifecycle durable|ephemeral]
Argument / Flag Type Description
name positional Instance name (used as the Docker project name and the default directory name)
--path path Where to create the instance directory. Default: ./<name>. Resolved against cwd.
--from-data path Copy an existing data directory as the new instance's /data.
--lifecycle durable | ephemeral Stamp the persistence intent into the generated beetroot.yaml. Omitted → the key isn't written and the instance is durable by default. See lifecycle — it's a label + guardrails, not a runtime switch (down never wipes /data for either).

What it does:

  1. Validates the name isn't already registered.
  2. Creates the instance directory and writes a minimal beetroot.yaml into it (api_version + android.version, plus lifecycle when --lifecycle is passed; every other field falls back to schema defaults). To start from a richer baseline, copy one of the example YAMLs over the generated file and run beetroot apply <name>.
  3. Allocates the lowest free port index.
  4. Registers name → absolute_path in ~/.config/beetroot/instances.json.
  5. If --from-data is given, copies the directory into <instance>/data/.
  6. Renders .env, downloads the Frida binary, downloads + stages modules.

Output:

[beetroot] created alpha at /home/you/alpha (index 0, ADB localhost:5555, Frida localhost:27042)
[beetroot] next: beetroot up alpha

register

Adopt an existing instance directory under the global registry.

beetroot register <path> [--name NAME]
Argument / Flag Type Description
path positional Path to a directory containing beetroot.yaml
--name string Registry name (default: basename of the path)

Useful for picking up an instance dir cloned from a teammate, or after recovering from a corrupted registry. Allocates a port index just like create.


adopt

Adopt a rooted Android device (real phone, third-party emulator, adb connect-ed network device) that's already reachable via the host adb CLI. Unlike create/register, no on-disk instance directory is made — the device is managed outside Beetroot. The adopted instance gets its own port index, so a follow-up beetroot frida-addr <name> reports the same port a redroid instance with the same index would have got.

beetroot adopt <serial> [--name NAME] [--verify]
Argument / Flag Type Description
serial positional adb serial (e.g. emulator-5554, 192.168.1.10:5555)
--name string Registry name. Defaults to adb-<serial> (lowercased, colons folded to hyphens, truncated to 24 chars). Required for IPv4-shaped serials (the default-name builder leaves dots in place and the registry-name grammar rejects them).
--verify, -V flag Check that the serial is listed in adb devices as device before writing the registry row. If not found, exits 1 without registering. Default: off (allows registering a device before it connects).

Verbs that need an on-disk container (up, down, restart, apply, destroy, snapshot) raise BackendCapabilityError against an adopted device and exit with code 2 — distinct from the standard "instance not found" exit 1, so wrapping scripts can distinguish. Use beetroot shell <name> / beetroot install-frida <name> / beetroot frida-addr <name> / beetroot module <name> for the universal verbs.

Adopted devices show up in beetroot ls like any other instance — KIND is adb, the ADB column shows the serial, and PATH is - (no on-disk directory). See ls.


apply

Re-render .env and re-stage Frida + modules from an edited beetroot.yaml.

beetroot apply <name>
Argument Type Description
name positional Instance name

Run this after editing the instance's beetroot.yaml. It re-downloads any modules whose URLs or sha256s changed, re-downloads Frida if the version changed, and re-renders .env. Idempotent — safe to run multiple times.

After apply, restart to pick up the changes:

beetroot down <name> && beetroot up <name>

up

Start one or more instances.

beetroot up <name> [<name> ...]
Argument / Flag Type Description
names positional (one or more) Instance names to start
--all flag Act on every registered instance.

Runs docker compose -p <name> -f <bundled-template> --project-directory <instance-dir> --env-file <instance-dir>/.env up -d for each instance. The bundled template lives inside the beetroot wheel, not at any project root.

No auto-rebuild

beetroot up does not rebuild the Docker image. To rebuild before starting, run beetroot build explicitly first — the verbs are decoupled so up stays fast and predictable.

Output:

[beetroot] alpha up — ADB localhost:5555, Frida localhost:27042

down

Stop one or more instances. Data is preserved.

beetroot down <name> [<name> ...]
Argument / Flag Type Description
names positional (one or more) Instance names to stop
--all flag Act on every registered instance.

Runs docker compose down. The instance's data/ directory is untouched.

When using --all, instances backed by non-Lifecycle backends (e.g. adb-adopted devices) are skipped with a one-line advisory to stderr — only redroid instances are stopped. Orphan or unresolvable registry rows (a redroid instance whose beetroot.yaml was deleted, or an unknown backend kind) are likewise skipped with a one-line advisory rather than aborting the whole fan-out. Explicit single-name invocations still raise a clear error.

Output:

[beetroot] alpha down (data preserved)

restart

Stop then start one or more instances. Useful for picking up Magisk-module changes or a fresh apply.

beetroot restart <name> [<name> ...]
Argument / Flag Type Description
names positional (one or more) Instance names to restart
--all flag Act on every registered instance.

Equivalent to running beetroot down <name> followed by beetroot up <name> for each named instance, but issued as a single verb.

Output:

[beetroot] alpha restarted

destroy

Stop and permanently delete an instance and all its data.

beetroot destroy <name> [-y]
Argument / Flag Type Description
name positional Instance name
-y, --yes flag Skip the confirmation prompt

Destructive

This deletes the entire instance directory including /data. There is no undo. Use beetroot down to stop without deleting.

Steps:

  1. (Optional) Prompts for confirmation unless -y.
  2. Runs docker compose down -v to remove the container and any named volumes.
  3. Deletes the instance directory with shutil.rmtree.
  4. Removes the entry from the registry, freeing the port index.

reset

Drop an instance's /data (accumulated app state) while keeping the instance itself — the gated, first-class form of the old "rm -rf data/" fresh-start recipe.

beetroot reset <name> [-y]
Argument / Flag Type Description
name positional Instance name
-y, --yes flag Skip the confirmation prompt

Unlike destroy, reset keeps the instance's identity (registry row, port index) and its staged tooling — frida-server and modules/ live outside /data, so they survive. It's the explicit counterpart to the silent boot_cache /data revert.

Steps:

  1. (Optional) Prompts for confirmation unless -y.
  2. Stops the container (docker compose down, idempotent).
  3. Wipes and recreates the bind-mounted data/ directory. redroid regenerates a clean /data from the base image on the next up.

The instance is left stopped — run beetroot up <name> afterwards for a fresh /data.

redroid only (for now)

reset is a redroid-backend verb. binder: vm keeps /data inside the guest (pending the split-data-disk work) and adb-adopted devices have no host-side /data, so both report a capability error. Manage state on those backends directly (a real device via its UI; a vm via a rebuild).


forget

Deregister an instance from the registry without touching its host directory.

beetroot forget <name>
Argument / Flag Type Description
name positional Instance name to deregister

Removes the registry row and frees its port index. No host-directory teardown, no docker compose down, no data deletion. Works for both redroid and adb-backed instances.

This is the companion to beetroot adopt — the safe way to remove an adb-adopted device from the registry when you no longer want Beetroot to track it. For redroid instances where you want to destroy the data too, use beetroot destroy instead.

Output:

[beetroot] forgot phone (registry row removed; host directory untouched)

ls

List all known instances — every backend kind, so adb-adopted devices appear next to redroid containers.

beetroot ls [--json]
Flag Description
--json Emit JSON instead of a table. Suitable for piping to jq or Python.

Status is queried live, never cached: redroid rows from docker compose ps, adb rows from adb devices (available when the serial is listed in state device, unavailable otherwise).

Table output:

NAME          KIND     IDX  ADB                   FRIDA                 STATUS        PATH
alpha         redroid  0    localhost:5555        localhost:27042       running       /home/you/alpha
bravo         redroid  1    localhost:5565        localhost:27052       exited        /tmp/scratch/bravo
phone         adb      2    emulator-5554         localhost:27062       available     -

For adb rows the ADB column shows the device serial verbatim (the value adb -s targets — there is no host:port form), FRIDA shows the allocated host forward port for the row's index, and PATH is - because an adopted device has no on-disk instance directory.

JSON output (abbreviated):

{
  "alpha": {
    "kind": "redroid",
    "path": "/home/you/alpha",
    "index": 0,
    "adb": "localhost:5555",
    "frida": "localhost:27042",
    "adb_address": "localhost:5555",
    "frida_address": "localhost:27042",
    "status": "running",
    "created_at": "2025-01-15T10:30:00+00:00"
  },
  "phone": {
    "kind": "adb",
    "index": 2,
    "serial": "emulator-5554",
    "adb_address": "emulator-5554",
    "frida_address": "localhost:27062",
    "is_available": true,
    "created_at": "2025-01-15T10:30:00+00:00"
  }
}

Adb-kind rows use the same shape as beetroot status for an adopted device: serial plus the Protocol-surface fields (adb_address, frida_address, is_available) and an always-empty stealth_paths ({}). They omit the entire ports dict and the redroid-only fields; the v0.3 back-compat keys (path / adb / frida) exist only on redroid rows.


logs

Tail an instance's logs. The source depends on the backend:

  • redroid (binder: host|auto) — passes through to docker compose logs.
  • micro-VM (binder: vm) — prints the guest's persisted QEMU serial console (<instance>/qemu-console.log): the kernel boot trace, guest-init output, and the in-guest redroid container's stdout. The console is captured by beetroot up and survives after it returns, so this is how you watch — or post-mortem — a slow TCG boot.
beetroot logs <name> [-f]
Argument / Flag Type Description
name positional Instance name
-f, --follow flag Follow log output (docker compose logs -f for redroid; tail -f of the console for the micro-VM)

Useful for watching entrypoint.sh output during boot:

beetroot logs alpha -f
# Watch for: [*] Android boot detected. Applying Beetroot configuration...

For a binder: vm instance, the same command surfaces the emulated boot (it's only supported once the VM has been started at least once, so the console log exists):

beetroot logs vmphone -f
# Watch for: kernel boot → guest-init → redroid container stdout

shell

Open an interactive ADB shell into an instance, or run a one-shot command.

beetroot shell <name> [-- <args>...]
Argument Type Description
name positional Instance name
args remainder Optional extra args forwarded to adb shell. Use -c 'cmd' for non-interactive invocation.

Calls adb connect localhost:<adb_port> then adb -s localhost:<adb_port> shell [args...]. Requires adb on your PATH.

Examples:

beetroot shell alpha            # interactive shell
beetroot shell alpha -c 'id'   # one-shot command

status

Print a JSON snapshot of a single instance.

beetroot status <name>
Argument Type Description
name positional Instance name

Output is JSON to stdout (v0.4 has no human-readable mode — pipe to jq). Exits 0 on success; exits 1 if name is not in the registry.

Redroid-kind rows include name, kind, index, created_at, ports (a dict with adb / frida / frida_control keys), status, adb_address, frida_address, stealth_paths, plus the v0.3 back-compat keys (path, adb, frida).

Adb-kind rows omit the entire ports dict and the redroid-only fields (path, status, adb, frida); they include serial and the Protocol-surface fields (adb_address, frida_address, is_available). They still carry a stealth_paths key (always empty: {}).


doctor

Run aggregated health checks for an instance.

beetroot doctor <name>
Argument Type Description
name positional Instance name

Output is one <check>: pass|fail|skip [reason] line per check. Exits 0 if every check passes; otherwise the exit code is the count of fail results (capped at 255). skip rows do not count toward the exit code.

Redroid checks: compose.status, host.binder, adb.connect, frida.handshake, magisk.zygisk, magisk.denylist.com.google.android.gms (skipped if the package isn't in magisk.denylist).

Adb checks: adb.serial, frida.handshake, magisk.zygisk, magisk.denylist.com.google.android.gms. compose.status is not applicable.

VM (binder: vm) checks: vm.process (is QEMU alive?), vm.accel (KVM vs the slow-TCG note), vm.qemu (the QEMU emulator is on PATH), vm.artifacts (the guest kernel + rootfs exist — else a beetroot build --vm-kernel hint), and adb.connect (connect-then-verify against the forwarded loopback port, not the USB-style adb.serial). The Frida and Magisk rows are omitted — the network-isolated guest has no Frida and boots a plain redroid image with no Magisk.


modes

Survey the host and report which Beetroot run-modes it supports — before you create an instance or pick a binder mode. Host-level and instance-independent, unlike doctor <name> (which health-checks one existing instance).

beetroot modes [--json]
Option Type Description
--json flag Emit the support matrix as JSON instead of a table.

Probes the host binder driver, KVM, and the QEMU / Docker / adb binaries, then reports each mode as supported / needs-setup / unsupported / unknown with a reason and remedy. Always exits 0 — it reports, it does not gate.

The modes reported are redroid (binder: host / auto), redroid (binder: vm, KVM accel), redroid (binder: vm, TCG accel), and adb backend (adopt remote device). See Binder & run-modes for what each one needs and why.


frida-addr

Print an instance's Frida address (localhost:<frida_port>) to stdout, so you can drive the native frida CLI yourself.

beetroot frida-addr <name>
Argument Type Description
name positional Instance name

frida-addr does the one thing the old beetroot frida passthrough verb existed for — resolving an instance's stride-allocated Frida port — without wrapping frida. That matters: a passthrough wrapper forwards arguments opaquely, which silently breaks frida's own shell completion, --help, and flag validation. Emitting just the address lets you invoke native frida directly, keeping all of its ergonomics (see issue #109). The emitter needs nothing installed; to actually attach you need the host frida CLI (uv tool install 'beetroot[frida]' or uv tool install frida-tools).

Examples:

beetroot frida-addr alpha
# → localhost:27042

frida -H "$(beetroot frida-addr alpha)" -n com.target.app
frida -H "$(beetroot frida-addr alpha)" -f com.target.app --no-pause -l script.js
frida -H "$(beetroot frida-addr alpha)" -ps    # list processes

The same value is also the frida_address field of beetroot status <name> (JSON).


install-frida

Push and launch frida-server on an adb-adopted device. Downloads the requested frida release, adb pushes the binary, launches it as root, and forwards the host Frida port so frida -H "$(beetroot frida-addr <name>)" reaches it. This is the CLI path the adopt hint advertises — it wraps the AdbDevice.install_frida() API.

beetroot install-frida <name> --version <tag>
Argument / Flag Type Description
name positional Instance name
--version string, required frida release tag to push and launch (e.g. 16.4.10)

--version is required: an adb-adopted device has no beetroot.yaml to fall back to for a default, so omitting it exits with a friendly error: line. A missing adb on PATH likewise surfaces as error: + exit 1.

Example:

beetroot install-frida phone --version 16.4.10
frida -H "$(beetroot frida-addr phone)" -n com.target.app

module

Install a Magisk module — append + re-stage (redroid), push (adb), or auto-install via root (adb, --auto-install).

beetroot module <name> <source>... [--sha256 HEX]... [--auto-install]
Argument Type Description
name positional Instance name
source positional, repeatable Redroid instances: https:// or http:// URL, or a path to a .zip (relative paths resolve against the instance directory). adb-adopted devices: path to an existing .zip on the host filesystem — URLs are not accepted, and relative paths resolve against your current working directory. Multiple sources are allowed with --auto-install only.
--sha256 option, repeatable Expected sha256 hex digest of the zip. With --auto-install, repeat once per source (or omit entirely); a mismatching zip is never pushed. Without the flag, ignored for adb pushes (verify the hash yourself) and verified at staging time for redroid instances.
--auto-install flag adb backend only: install via root (su -c magisk --install-module) instead of the safe push-to-Downloads default.

Redroid instances: the module is appended to the instance's beetroot.yaml and immediately staged into its modules/ directory. Restart to flash:

beetroot down <name> && beetroot up <name>

adb-adopted devices (default): the zip is pushed to /sdcard/Download/<name>.zip and a one-line "install via the Magisk app → Modules tab" instruction is printed — no root interaction.

adb-adopted devices (--auto-install): each zip is pushed to a synthesized temp name under /data/local/tmp/ (beetroot-module-<N>.zip — the local filename never reaches the device shell) and installed with su -c magisk --install-module <zip> (Magisk stages it into /data/adb/modules_update/<id>/ for the next reboot); the temp zip is removed afterwards. Every module gets its own ok: (stdout) or failed: (stderr) report line; a failed module doesn't stop the rest, and the verb exits 1 if any module failed. Before anything is pushed, a cheap pre-flight probe diagnoses whole-device problems with a single friendly error: ... line + exit 1 instead of one identical failed row per module: an offline / disconnected / unauthorized device (reconnect, accept the USB-debugging prompt, and check adb devices), no usable root (su missing or denied root), or root without a usable magisk binary. Connectivity is decided by re-running adb devices for the serial, never by matching the probe's error text, so untrusted module stderr can't be mistaken for a disconnect. A device that genuinely drops offline mid-batch aborts the remaining modules with the same offline diagnosis (which names how many were skipped; rows completed before the abort are still reported). Host-side validation failures (missing/non-zip path, sha256 mismatch) always stay per-module failed: rows and never abort the batch. Redroid instances don't support the flag and exit 2. See the modules guide for examples.


build

Build the redroid base image and Beetroot layer for a chosen GApps intent. One-time bootstrap; re-run when you want a fresh image.

beetroot build [<gapps>] [--gapps-vendor <vendor>] [--vm-kernel] [--from-source] [--check] [--android-version <N>]
Argument / Flag Type Description
gapps positional, optional GApps intent to bake into the base image. One of none, minimal (default), full.
--gapps-vendor enum, optional Pin a specific GApps vendor for app compatibility instead of letting the intent pick: litegapps, opengapps, mindthegapps. Cannot be combined with the none intent.
--vm-kernel flag Build the binder: vm micro-VM guest kernel + rootfs instead of the redroid base image (for hosts with no kernel binder). Prints the resulting vm.kernel / vm.rootfs paths.
--from-source flag With --vm-kernel: build both guest artifacts locally — compile the kernel from source (~7 min) instead of fetching the matching prebuilt bzImage, and bake the rootfs locally (needs the Docker daemon + busybox/socat/iptables toolchain) instead of fetching the matching prebuilt image.
--check flag With --vm-kernel: only run the host-prerequisite preflight (the full superset both paths could need) and report what's missing — don't build. Exits 0 when the host is ready, 1 otherwise.
--build-context path, optional Path to a source checkout whose docker/ tree supplies the build assets. Overrides BEETROOT_BUILD_CONTEXT. Defaults to the assets bundled in the installed wheel.
--android-version int, optional With --vm-kernel: Android major version to bake into the guest rootfs (must match the instance's android.version). Defaults to 14 — the same default a fresh beetroot create uses, so an unflagged build and a default instance agree. Bake a different version (e.g. --android-version 11) when your instance pins one.

--vm-kernel is fetch-first, so the default path needs almost nothing on the host. A real build first checks only the lightweight fetch prerequisites (curl + tar, which the kernel source-compile fallback would need) and aborts early if those are missing. The heavyweight bake-only toolchain — the static busybox/socat/iptables-legacy binaries, ldd/mke2fs, and a responsive Docker daemon — is enforced only when a local rootfs bake actually runs (a prebuilt miss, --from-source, or a bake-override env var), not on the default fetch path. So a fresh, dockerless, busybox-less host that the prebuilt targets builds fine: it never touches the bake toolchain. When a bake is required and the host is missing those prerequisites, the build reports everything missing in one pass, each with the apt package (or command) that fixes it, instead of failing one tool at a time. Run beetroot build --vm-kernel --check to see the report for the full superset both paths could need, without starting a build. Setting any of REDROID_TAR, REDROID_IMAGE, IMAGE_SIZE_MB, or DOCKER_URL forces a local bake (those change the baked bytes but are not part of the prebuilt fingerprint, so the prebuilt would silently ignore them); on that bake path, if the redroid pull hits a Docker Hub rate limit, point REDROID_TAR at a docker saved image tarball or use a registry mirror (the daemon check is then skipped, since the bake loads from the tarball instead of pulling).

With --vm-kernel, the guest kernel is fetched prebuilt by default: a ~12 MiB bzImage is downloaded from the repo's vm-kernel GitHub release, matched to the pinned kernel version and a fingerprint of the bundled kernel.config (shipped as package data at src/beetroot/templates/vm/kernel.config), and verified against its .sha256. If no asset matches (you edited the config, bumped the version, the release isn't published yet, or the network is blocked), it falls back to compiling from source. So a fresh host skips the ~7-minute compile, and the vendored config stays authoritative — you can never boot a stale prebuilt kernel. The rootfs is also fetched prebuilt by default (issue #79): a zstd-compressed ext4 image is downloaded from the repo's vm-rootfs GitHub release, matched per Android version and a composite fingerprint over the three rootfs-determining inputs (Android version, Docker static-bundle version, guest-init.sh), verified against its .sha256, and decompressed locally — so a fresh host skips the ~2 GB redroid pull + local bake and needs no Docker daemon. If no asset matches (any input changed, the release isn't published yet, or the network is blocked), it falls back to baking the rootfs locally. --from-source forces a local bake of the rootfs too.

The source-compile fallback is self-contained: it fetches the pinned linux-<version>.tar.xz from cdn.kernel.org, extracts it into a throwaway scratch tree, and compiles there (merging make defconfig with the vendored kernel.config). You don't need to be sitting inside an extracted kernel tree for it to work — so a prebuilt miss on a fresh host degrades to a slow-but-working compile rather than a hard No rule to make target 'defconfig' error.

The kernel.config, guest-init.sh and adbprobe.c build assets ship inside the wheel, so --vm-kernel works from a plain uv tool install with no source checkout. To build against a working tree's assets instead, pass --build-context <path-to-checkout> (or export BEETROOT_BUILD_CONTEXT=<path-to-checkout>); Beetroot then reads them from <context>/docker/vm.

The guest rootfs bakes the plain upstream redroid image for the chosen Android version (e.g. redroid/redroid:14.0.0-latest) and records that version in a marker beside the image. If an instance's android.version later disagrees with the baked rootfs, beetroot up / beetroot apply print a one-line warning (the guest boots the baked version regardless) telling you to rebuild with a matching --android-version or change android.version. A pre-existing rootfs without the marker is treated as a match (no warning) for backward compatibility.

The verb:

  1. Clones ayasa520/redroid-script into ~/.cache/beetroot/redroid-script (the per-user cache dir, respecting $XDG_CACHE_HOME).
  2. Runs the patcher to produce a local Docker image (e.g. redroid/redroid:14.0.0_litegapps_houdini_magisk).
  3. Runs docker compose build to layer entrypoint.sh and stealth.rc on top.

beetroot up no longer accepts a --build flag — to rebuild before starting, run beetroot build explicitly first.


snapshot

Pack an instance's host-side state into a .tar.zst archive.

beetroot snapshot <name> [-o <archive>]
Argument Type Description
name positional Instance name to snapshot.
-o, --output path Archive path (default: ./<name>.tar.zst). The .tar.zst extension is appended automatically if you omit it.

Stop the instance first (beetroot down <name>) — tar-ing live data/ produces an inconsistent archive. The archive excludes .env (it's regenerated on the next apply). See Snapshots for the round-trip workflow and the path_layout forward-compat story.


restore

Unpack a snapshot archive into a new instance and register it.

beetroot restore <archive> [--name <name>] [--path <dir>] [--force]
Argument Type Description
archive positional Path to a .tar.zst snapshot archive.
--name string Registry name for the restored instance (default: the name recorded in the manifest).
--path path Directory to extract into (default: ./<name>).
--force flag Wipe a non-empty destination directory before extracting.

A fresh port index is allocated — the source's index is never reused, so the original and the restored instance can run concurrently if both directories still exist. After restore, run beetroot apply <new-name> to regenerate .env, then beetroot up <new-name>.