Named devices & the daemon

Provision named devices and let the background daemon keep them running across reboots.

Provision named devices and let a background daemon keep them running across reboots — no folder required, no terminal tied up.

Two ways to run devices

The folder model is great for a quick capture: cd somewhere, run bmc start, Ctrl-C when done. But for devices you want to keep around — named, supervised, restarting after a reboot — there's the daemon.

A named device lives in a per-machine registry, maps to a Chrome binary, and is launched and supervised by a background daemon. You manage them by name from anywhere, without cd-ing into a folder.

Bring the daemon up

Run bmc with no arguments:

bash
bmc

This starts the daemon in the background (releasing your terminal) and lists this machine's named devices. Run it again any time — it never starts a second daemon, so it's safe to repeat.

To stop the daemon (and every running device under it):

bash
bmc stop

Provision a named device

The interactive way is bmc setup — a small terminal UI to add, edit, rename, and remove named devices, mapping each to a Chrome binary (it auto-detects your installed browsers):

bash
bmc setup

Or do it directly:

bash
bmc create "Staging"                         # auto port + profile
bmc create "Staging EU" --from "Staging"     # clone binary/flags onto a fresh port + profile
bmc create "Canary" --chrome /path/to/chrome # pin a specific binary

bmc create provisions the device (pairs its device JWT, allocates a port and profile) but does not start it.

Start, stop, list

bash
bmc start "Staging"     # launch the named device's Chrome via the daemon
bmc stop "Staging"      # stop just that device
bmc ps                  # list named devices + which are running
bmc ps --json           # machine-readable

bmc start "<name>" is the same command you'd use for a folder device — when <name> matches a registered named device (and the daemon is up), it's routed to the daemon instead of creating a folder device. bmc ps works even when the daemon is down: it shows the registry, all marked stopped.

[bmc] daemon: running
  NAME       STATE     PORT   UUID
  Staging    running   9301   8f3c…
  Canary     stopped   9302   1a7e…

Start automatically at login

Wire the daemon to launch when you log in, and start it now:

bash
bmc autostart              # install the supervisor + start the daemon under it
bmc autostart --takeover   # same, adopting a daemon that is already running (its browsers stay up)
bmc autostart --linger     # also keep it running after logout (where supported)
bmc autostart off          # remove the supervisor

The mechanism is per-OS:

  • macOS — a launchd LaunchAgent in ~/Library/LaunchAgents, tied to your GUI login session. --linger is a no-op here (surviving logout would need a root LaunchDaemon), so a machine that must come back on its own after a reboot should auto-login that user.
  • Linux — a systemd --user unit. --linger runs loginctl enable-linger so the daemon survives logout.
  • Windows — a Scheduled Task that runs at logon. --linger is a no-op.

Note: bmc autostart only wires up the daemon; bmc autostart off removes that. To bring the daemon up or down by hand, use bmc and bmc stop. Re-running bmc autostart on an installed unit is safe — it is idempotent and never restarts your browsers.

The supervisor: crashes, updates, reboots

From bmc 85 the login agent is a real supervisor (KeepAlive on macOS / systemd), not just a launcher. What that buys you:

  • The daemon comes back on its own. Whatever ends it — a crash, a kill -9, the hot recycle a bmc update performs, a manual unload — the supervisor relaunches it within ~10 s. The relaunched daemon re-adopts every browser that is still running instead of restarting them.
  • A daemon exit never takes your browsers with it. Every device's Chrome runs in its own process group, so a supervised daemon going away is invisible to the pages open in your devices. A hard-killed device child that left an orphan Chrome behind is reaped before its device is relaunched, so two devices can never end up attached to the same Chrome.
  • Devices are supervised per flag. A device with autostart on is boot-started and crash-relaunched with backoff (2 s → 60 s, reset after 5 min of stable uptime); a device with autostart off is left exactly as you left it.
  • bmc daemon --foreground runs the same supervisor loop in your terminal — what the autostart unit runs — for a one-off session or for debugging.

After a reboot there is nothing to do: at login the supervisor starts the daemon, the daemon pairs its host record and boot-starts every autostart device (seven devices come up in well under ten seconds). Devices without autostart stay stopped until you bmc start "<name>" — or flip them with bmc edit "<name>" --autostart on so the next reboot brings them too. Verify with bmc ls: the FLAGS column shows auto for autostart devices and ctl-only for capture-off ones; every autostart device should read running. A device that shows stopped with auto means the supervisor's crash ladder gave up — an operator bmc restart "<name>" lifts the hold. Because both flags live on the device's settings row, the dashboard's Automation tab and the MCP tools set_device_autostart / set_device_capture_enabled show and flip the same state, and bmc ls reflects a remote flip at once.

Window mode: front, minimize, and running out of sight

A device's Chrome is a real, fully-windowed browser — its window's on-screen state is what windowMode controls (settings_device.data.windowMode: "visible" or "minimized"). Nothing about the page changes; a site can't tell the difference. This is deliberately not headless mode — see why below.

Class default. A row that says nothing resolves by device class: usage-probe devices and any capture-off (control-only) device start minimized (off-screen at launch, then moved out of the way once Chrome attaches) — nobody is meant to be watching them, so they never fly across your screen. Every other device — your own remote-control browser — starts visible, unchanged from before windowMode existed.

Bring one to the front, or send it back:

bash
bmc front "Staging"      # raise the window, front the tab, title it "[Staging] …"
bmc minimize "Staging"    # send it back off-screen

bmc front opens a short-lived CDP connection straight to the device's own local port, raises the window (Browser.setWindowBounds), brings its tab to front, and titles the tab [<name>] … so it's identifiable among every other bmc window on screen — then closes that connection. The device's own long-lived capture session is never disturbed. The Ink device list (the live bmc console) offers the same as hotkeys on the selected running device: f (front) / m (minimize).

Reachable remotely. front/minimize dispatch over the same bmc.daemon.* host-command wire as start/stop/createthe same wire proven reachable below — gated by the same Browser Control opt-in as every other remote command. That makes two more surfaces possible on top of the identical wire:

  • The dashboard Devices panel offers Bring to front / Minimize in a bmc device's row action menu, plus a Window mode toggle in its settings (persists the row — same effect as bmc edit, without a CLI).
  • MCP exposes both halves as separate tools: focus_device_window raises or minimizes a running device's window right now ({ device_uuid?, deviceName?, device, action: "front"|"minimize", confirm: true }device_uuid/deviceName select the host machine, device names the target device on it) without touching the row, and set_device_window_mode persists the setting on a named device ({ device_uuid?, deviceName?, windowMode: "minimized"|"visible", confirm: true }) — the same row bmc edit and the dashboard toggle write. Full argument reference: MCP tools → Named CDP device daemon. get_device_status reports the persisted setting back as windowMode (best-effort; null means the class default applies), and list_cdp_instances reports each running device's live on-screen state as windowState: "normal"|"minimized".

Headless: one launch vs. persistent

There is a second, unrelated way to hide a bmc browser: real Chrome headless mode (--headless=new) — no window exists at all, not even off-screen, which also removes the whole compositor/GPU/audio pipeline. bmc deliberately does not default any device class to it: some origins (claude.ai among them) detect headless Chrome and serve a Cloudflare interstitial instead of the real page, so a device that must sign in and stay signed in is safer minimized (a genuine window, above) than headless. Headless stays available, opt-in, per device — settings_device.data.headless: true — for a device whose origin doesn't gate on it.

That persistent row is a different thing from the one-shot override on bmc start:

bash
bmc start "cu-claude6" --headless off   # THIS launch only, visibly — e.g. to re-sign in

--headless on|off (same on/off spelling as --capture/--autostart) overrides a single launch — it is never written to the row or the local mirror, so the very next natural start or restart reverts to whatever the row/mirror/class-default already said. The MCP mirror is start_device's optional headless argument, forwarded the same way. Use the one-shot flag to sign an expired account back in with eyes on the page, then let the device revert to its normal setting on its own — never bmc edit a row just to undo it afterwards.

Is it really answering? The command wire

A daemon can be up and unreachable at the same time, and until bmc 86 nothing could tell you apart. The heartbeat that keeps a machine's row fresh travels over plain HTTP; the remote commands you send from the dashboard, MCP or BusyBro travel over a Realtime channel. Those are two independent paths. On 2026-09-05 the second one broke on a loaded machine and the first one kept reporting health for hours: a fresh heartbeat, serving: true, twelve browsers online — and not one command arriving. serving had only ever meant "the local socket is bound".

So the daemon now proves the wire instead of assuming it. Every so often it sends a real command to itself through its own channel and requires its own result back, which exercises receive, dispatch and send in one round trip. The verdict is reported as commandWire:

StateWhat it means
upA round trip completed inside the freshness window. Commands are reaching this machine.
degradedRecent failures behind a still-recent success — the wire is flapping.
downNo round trip has completed in the window. Commands are not arriving, whatever else looks green.
unknownNo probe has completed yet, or the daemon is older than the check. Not a pass.

unknown is deliberately not a friendly grey: a panel that cannot say whether a machine answers has to say so out loud, which is exactly what the outage taught. The dashboard reads the same four words — see the daemon panel — and a machine reporting serving: true with a down wire is shown as down.

Two things follow from the same fix:

  • Broken channels rejoin themselves. Every channel is watched for the error, timeout and closed states that used to be discarded silently, and a broken one is rebuilt with a bounded, jittered backoff (1 s → 30 s). A healthy resubscribe cancels a pending rejoin, so a working channel never churns.
  • A wedged daemon restarts out of band. Five consecutive failed round trips and the daemon exits; the login supervisor starts a clean one within ~10 s. That works precisely because it depends on neither the wire nor the loop that just failed. An unsupervised daemon logs the escalation and stays up instead — exiting into nothing would only make it worse.

Captured traffic survives a refused upload

Ingest can refuse a batch for reasons that have nothing to do with the batch: a rate limit, or the edge in front of the API answering before the API sees it. Those are now treated as temporary, retried with backoff, and any Retry-After the edge sends is honoured as a floor.

A batch that still cannot land after its retries is parked on disk in the device's state directory rather than dropped, and replayed oldest-first after the next successful upload, on the idle tick, and across a daemon restart. The spool is bounded (200 files / 64 MB) and a corrupt file is quarantined instead of poisoning the queue. A batch the server rejects as genuinely permanent — a payload it will never accept — is still dropped, because replaying a poisoned batch forever is not durability.

This is new in bmc 87. Before it, a refused upload was a silent data loss.

Autostart per device

bmc autostart installs the supervisor for the daemon itself (a launchd LaunchAgent / systemd --user unit with KeepAlive — the daemon is relaunched within ~10 s whenever it exits). Which devices the daemon brings up on its own is a per-device flag, settings_device.data.autostart:

bash
bmc create "probe" --autostart on    # boot-start + crash-relaunch this device
bmc edit "probe" --autostart off     # manual only (a running device is NOT stopped)

With the literal true the daemon starts the device on boot and relaunches it with backoff (2 s → 60 s, reset after 5 min of stable uptime) whenever it exits on its own. The flag is read from the device's settings row at boot and at every restart decision, so a flip from the dashboard (Autostart on the device's Automation tab) or the MCP tool set_device_autostart lands without a daemon restart — turning it off never stops a running device. bmc ls marks such a device auto.

A device that should always be up but never captured combines this with capture off: --capture off --autostart on.

Folder devices vs. named devices

Folder deviceNamed device
Created bybmc start in a folderbmc create / bmc setup
Identity stored in./.bmc.json in the folderper-machine registry
Started withbmc start (foreground, in that folder)bmc start "<name>" (via the daemon)
Survives rebootnoyes, with bmc autostart
Terminalholds the terminal until Ctrl-Cruns in the background

Both stream into the same dashboard feed and show up under Devices — they're the same kind of device, just provisioned and supervised differently.

Next

Ask your mate