Capture a Chrome

Run bmc in a folder to launch (or attach to) Chrome and stream its network traffic.

Run bmc in a folder to launch (or attach to) a Chrome instance and stream its network traffic into your dashboard feed.

A folder is a device

The directory you run bmc in is the device. On the first capture in a folder, bmc creates ./.bmc.json there — it holds the device's UUID and a 365-day device JWT, so capture never needs your user session again. Re-runs in the same folder reuse that device.

Because each folder is independent, you can run many devices on one machine — one per folder, each pointing at its own Chrome instance.

Start capturing

bash
bmc start "My Device"

The first run in a folder asks for a device name (the default is the folder name) or takes the one you pass. After that, ./.bmc.json exists, so the name is remembered. Pass a different name later to rename the device:

bash
bmc start "Renamed Device"

bmc start [name] and the bare bmc [name] form do the same thing:

bash
bmc "My Device"     # identical to: bmc start "My Device"

bmc with no arguments instead brings up the background daemon — so always pass a name (or start) when you mean to capture in the current folder.

Press Ctrl-C to stop. If bmc launched the Chrome itself, it shuts that Chrome down too; a Chrome you launched yourself is left running.

Launch vs. attach

By default, bmc launches its own Chrome — that's the only way it can apply your device's external proxy and any pass-through flags. The model is: one folder = one device = its own independent Chrome instance.

  • No --port (the usual case): bmc starts from this device's saved port, and if that's taken it auto-claims the next free debug port (9222 → 9223 → …). The chosen port is persisted to .bmc.json, so every folder keeps its own. It never attaches to a Chrome it didn't launch — including your everyday Chrome on 9222.
  • Explicit --port <p>: your call. If something is already serving CDP on that port, bmc attaches to it; otherwise it launches Chrome there.
bash
cd ~/work/app-a && bmc start "App A"     # e.g. its own Chrome on 9222
cd ~/work/app-b && bmc start "App B"     # e.g. its own Chrome on 9223

The Chrome profile

When bmc launches Chrome, the profile (--user-data-dir) defaults to the folder you ran bmc in — so it's local to the project and persists across runs. Override it with CDP_USER_DATA_DIR, or by passing -- --user-data-dir=… (see External proxy & custom Chrome).

Attach to your own Chrome

To drive a Chrome you started yourself, use bmc attach. It never launches Chrome — it attaches to an already-running one on the debug port, and if none is up yet, it waits for one to appear:

bash
# start Chrome yourself with remote debugging…
open -na "Google Chrome" --args --remote-debugging-port=9333 --user-data-dir=/tmp/c
# …then attach
bmc attach "Manual" --port 9333

Heads up: When attaching, bmc can't apply the device's external proxy or any -- <chrome-flags> — it didn't launch that Chrome, so those launch-time settings are ignored. You'll see a warning if either is configured.

What gets captured

bmc honors the same capture rules as every other device, all set in the dashboard and pushed live (no restart needed):

  • Only hosts in your SSL Proxying domain list are recorded — unless Inspect all requests is on (set per-device or globally).
  • Hosts in your ignore list are always dropped.

Captured requests stream to your dashboard feed tagged source: "cdp" with a Chrome badge. Everything else works for CDP devices too: breakpoints, HAR export, and the global Pause all requests toggle. Manage capture rules and per-device settings under Devices.

Tag a client

Stamp captured requests with an internal client label (shown as a dashboard badge, never sent upstream):

bash
bmc start "My Device" --client "checkout-service"
bmc start "My Device" --client ""        # clear the stored tag

The tag persists to .bmc.json. You can also set it with the CDP_CLIENT environment variable for a single run.

Control-only devices (capture off)

A bmc device can run Chrome and stay fully remote-controllable without capturing anything. Since bmc 87 this is the state a new device starts in: bmc create defaults to capture off, and capture is opt-in.

bash
bmc create "probe" --autostart on    # control-only — the default since bmc 87
bmc create "shop" --capture on       # a purpose-built capture device: say so once
bmc edit "probe" --capture on        # turn capture on later, live, no restart

Why the default is off: capture is the expensive, surprising behaviour, and control is the cheap one. Three ingest floods in a row all began the same way — an ad-hoc browser was created for a proof run and quietly streamed every page load into the feed because nobody had said not to. A device now captures only when someone asked for it, and bmc create prints the resolved mode either way, so the mode is never something you discover from a bill.

Nothing about an existing device changed. The device's settings row stays the live authority, so a device created before 87 keeps capturing, and bmc edit, the dashboard Capture off toggle on the Automation tab and the MCP tool set_device_capture_enabled all still flip it the same way.

The switch is one key on the device's settings row (settings_device.data.captureEnabled); the literal false means:

  • Chrome launches as usual and every remote-control command (browser_targets, browser_open, browser_eval, browser_screenshot, raw CDP) keeps working;
  • no Network capture and zero ingest — the device never writes an entry to the feed, so a browser that only exists to be driven (a login probe, a usage monitor) costs nothing in storage or proxy-server load;
  • the flip lands live on the running device through the settings push — no Chrome restart, no re-pairing. bmc ls marks such a device ctl-only.

Everything else (autostart, remote-control opt-in, block rules, scripts) is unaffected. Pair it with --autostart on for a device that should always be up but never captured — see the daemon page.

Which store owns the flag

The settings row is the single source of truth. bmc also keeps a local mirror in its device registry so a boot with no network still knows the class, and bmc create / bmc edit --capture write both. The child re-reads the row on every settings push, so a dashboard/MCP flip lands live, and it survives a restart: the boot seed only fills keys the row does not already carry, and never overwrites one it does.

Because the local mirror is a cache, it can lag. Since bmc 89 every listing surface (bmc ls --json, bmc status, the MCP list_cdp_instances) reports the effective value and where it came from:

captureSourcemeaning
cloudread off the settings row this daemon lifetime — by the running child, or by the daemon at boot. Trust it.
localnothing could be read; this is the mirror, and it may lag the row.

So a local value never confirms that a capture flip took effect. Restart the device or the daemon to force a cloud read; the daemon then repairs the mirror and logs "<name>": capture mirror reconciled from the cloud row.

This was #2425: two browsers showed capture: "on" for a day after their rows had been set to false. Their behaviour was right the whole time — the row always won, and a fixture device with the mirror on, the row false and a daemon restart still came up control-only (capture off) — but the surface an operator checks could not confirm it, which is the same blind spot that let the earlier capture floods run.

Next

Ask your mate