Browser
August 28, 2026 · View on GitHub
Drive Chromium-family browsers from AI agents via the Chrome DevTools Protocol.
Overview
agents browser gives agents a real browser — the same Chrome, Brave, or Edge you use manually, with your existing cookies, fingerprint, and IP. There is no Playwright subprocess, no automation flags, no relay extension. Sites that block Puppeteer and Playwright let it through because there is nothing to detect.
The CLI manages browser processes, tab lifetimes, and network capture through a background daemon (agents browser IPC server). Each agent creates a named task. Multiple agents run tasks in parallel without sharing state: profile A has its own chrome-data/, profile B has its own — no cookie bleed, no race on focus.
Intended users: LLM agents that need to log in to real web apps, scrape authenticated pages, fill forms, upload files, or capture screenshots to feed back into a reasoning loop.
Architecture
agent process
│
├─ agents browser <subcommand> (one request, then exit)
│
└─ agents browser stream (long-lived NDJSON; process + socket stay warm)
(both resolve $AGENTS_BROWSER_TASK or --task)
▼
CLI (browser.ts)
│
│ JSON-RPC over UNIX socket
│ ~/.agents/.cache/helpers/browser.sock
▼
Browser Daemon (ipc.ts / service.ts)
│
│ Chrome DevTools Protocol
│ ws://127.0.0.1:<port>/json
▼
Browser process
(Chrome / Brave / Edge / Chromium / Comet)
│
├── Profile A chrome-data/A/ → Task swift-crab-a1b2
└── Profile B chrome-data/B/ → Task bold-phoenix-c3d4
Remote variant (ssh:// endpoint):
CLI → SSH tunnel → CDP on remote host → remote Chrome
The daemon auto-starts on the first command that needs it. Commands that only
inspect local state (ps, profiles list) do not start it.
Liveness is a reply, not just an accept. The daemon shares one event loop with
the rest of agents __daemon-run, and a unix-socket connect succeeds at the
kernel level even when that loop is blocked — so a busy daemon accepts on
browser.sock while never servicing the request. Before running a verb the CLI
requires the daemon to actually answer a version probe; a reachable-but-wedged
daemon fails loud (Browser daemon is running but unresponsive — its event loop is blocked…) with the daemon log path and the recovery verb, instead of the old
confusing Timeout waiting for browser daemon socket or an indefinite hang. Reset
it with agents browser stop --daemon — the next browser command restarts it
clean (PHNX-3411).
Tasks are addressed by a short machine id (8 hex chars). browser status
shows a human label (--title if given, else the first navigated host,
else untitled). In the common case you never type a handle: the CLI stamps
the caller's session/launch identity and the daemon resolves the single live
task for that caller. Pass --task only when deliberately running two tasks
at once. $AGENTS_BROWSER_TASK is still accepted as an explicit override.
Page verbs (navigate, screenshot, …) create a task when none resolves;
done/stop never create.
Setup
1. Create a profile
A profile names a browser + CDP endpoint pair. A new profile is machine-local
by default (RUSH-2716): it is written to this machine's own
~/.agents/devices/<machine>/agents.yaml under browser:, and no other machine
sees it. That is the right default because a profile pins an OS-specific
binary: path and a locally chosen CDP port, so a synced copy is wrong on every
other box — and because agents create throwaway profiles freely, syncing each one
filled the shared file with junk.
A name declared by exactly one device is identity-bearing (the daemon tunnels
to that device). A name declared by several devices is fungible (use the local
one). Leftover central browser: entries are claimed with
agents browser profiles claim on the machine that hosts the browser.
Runtime state — the Chrome chrome-data cookie jar — lives separately under
~/.agents/.cache/browser/<profile>@<device>/, which is gitignored and
per-machine, so each machine logs in once.
# Minimal: let agents pick a free port and auto-detect the binary (this device)
agents browser profiles create work --browser chrome
# Pin an endpoint explicitly
agents browser profiles create work --browser chrome --endpoint "cdp://127.0.0.1:9222"
# Remote host via SSH — declare it on the machine that should own the name
agents browser profiles create staging --browser chrome \
--endpoint "ssh://deploy@staging.example.com?port=9222"
agents browser profiles list shows a WHERE column of declaring devices.
If you skip --profile, or pass the reserved name --profile default, the
profile is resolved in this order — the same order in every command
(start, stop, status, navigate, tab add), not just start:
-
A profile that literally bears the name you typed. A profile you named
defaultyourself resolves to itself and is never redirected. -
Your configured default — the profile set via
agents browser use <name>on THIS machine, when it can launch here. If its browser/binary isn't installed on this machine,startwarns and falls through to auto-detect. -
The auto-detected profile,
auto-chrome— or thedefault-named predecessor an older agents-cli wrote on this machine, which keeps working. -
Auto-detect (
startonly) — the first installed Chromium-family browser, saved as theauto-chromeprofile. Detection priority:- macOS: Chrome > Brave > Edge > Chromium > Comet
- Linux: Chrome > Chromium > Brave > Edge
- Windows: Edge > Chrome > Brave > Comet
An auto-detected profile that came from another OS whose binary is missing here — a
/Applications/...Chrome path resolved on Linux, say — is regenerated for this machine rather than failing with "Custom binary not found". Remote (ssh://) defaults skip this check: their browser lives on the far host.
default is an alias, not a profile: it means "whatever profile this machine
is configured to use". The auto-detected profile is called auto-chrome so the
two cannot be confused (before RUSH-2709, default meant both, and only start
honored the alias — so --profile default reached a different profile in
navigate than in start).
Profile names vs runtime keys
agents browser profiles list, status, and --profile all use the bare
profile name (comet-local). A running browser is stored under a runtime key
that also names the device it resolved to (comet-local@zion), so the same
name on two machines does not collide on disk. That key is an implementation
detail you never have to type or read. Leftover @endpoint-N dirs from older
builds are renamed onto the device key. status renders the two separately:
comet-local (device: zion, port 9222, pid 4183)
and status --json carries them as separate profile-side fields:
{ "name": "comet-local", "device": "zion", "key": "comet-local@zion" }
Passing a runtime key where a name is expected still works, so a key copied out of an older listing resolves to its profile.
The configured default is a per-device setting: it lives in this machine's
browser.profile config key (devices/<machine>/agents.yaml config:), so
each machine keeps its own choice — the profile it points at may hold
machine-local logins. It is machine-local: only this box can set it. Set it
once per machine.
Safari and Firefox are not supported. They do not implement the Chrome DevTools Protocol.
2. First-run onboarding
On the first start, Chrome opens to a new user-data directory with no
cookies or saved state. Complete any first-run screens (agree to terms,
sign in) before automating. Run agents browser profiles doctor <name> to
check if onboarding is complete.
3. Drive the page (no export required)
# Implicit task create + navigate — identity tracks the rest of the session
agents browser navigate https://example.com
agents browser screenshot
# Explicit start still useful for --profile / --url / --record / --title
agents browser start --profile work --title "login check"
agents browser screenshot # resolves from caller identity
Pass --task <id> only when running two tasks at once. $AGENTS_BROWSER_TASK
remains a valid explicit override.
Keep the action loop warm
Normal agents browser <command> calls are convenient for individual actions,
but each shell invocation starts a new Node process and opens a new daemon IPC
connection. For an observe-and-act loop, start browser stream once and send one
IPC request object per line. It keeps both the narrow browser CLI process and the
existing browser-daemon socket open until stdin closes; the daemon continues to
reuse its existing CDP connection.
printf '%s\n' \
'{"action":"screenshot","path":"/tmp/page.jpg"}' \
'{"action":"click","atX":320,"atY":540}' \
| agents browser stream --task "$AGENTS_BROWSER_TASK"
The response is one compact JSON object per non-empty input line, in the same
order. A long-lived caller can leave stdin open and write later requests to the
same process. --task supplies the default task field; a task returned by a
start request becomes the default for subsequent lines. Malformed JSON returns
an error response without closing the stream. A fleet-remote start line obeys
the same device-local browser remote-control consent gate as the ordinary
browser start command.
Command Reference
Profile management
| Command | Description |
|---|---|
agents browser use [name] | Pick this machine's default profile. No name opens a picker on a TTY or prints the current default headlessly; --unset or auto restores auto-detect. |
agents browser profiles list | List all configured profiles and the devices declaring each one (WHERE). A * marks this machine's configured default — which is NOT the same thing as the profile named default. --json adds devices + kind (identity | fungible) + isConfiguredDefault |
agents browser profiles create <name> | Create a new profile on this device (add is an alias). Prints Added "<name>" on <device> (port N). |
agents browser profiles add <name> | Alias of create |
agents browser profiles seed | Create a machine-local profile for each installed browser (named <browser>-local), so you can browser use one instead of hand-crafting each. Idempotent — existing profiles are left untouched |
agents browser profiles prune | Remove dead profiles this device declares — browser not installed here, or never started (see below) |
agents browser profiles edit <name> | Edit an existing profile in place — description, endpoints, secrets, viewport, binary. Stays in the store it already lives in. The browser type and the name are NOT editable: both key the on-disk profile cache (and its logins), so changing either orphans it — delete and recreate instead |
agents browser profiles rename <from> <to> | Rename a profile and move its browser data with it, so logins survive. Refuses while the profile is in use. The one safe way to change a name: edit refuses it, and delete-and-recreate abandons the --user-data-dir |
agents browser profiles claim [name] | Move leftover central browser: entries into this device's declaration file. Only profiles this machine can host are claimed. Run on the machine that actually has the browser. |
agents browser profiles show <name> | Show profile details |
agents browser profiles use <name> | Compatibility spelling for agents browser use <name> |
agents browser profiles logins | Per profile: SERVICE | ACCOUNT | CREDS — live session, the signed-in account (plaintext username, never decrypts), and whether login creds are in the profile's secrets bundle |
agents browser profiles remove <name> (alias delete) | Remove profile config and chrome-data cache |
agents browser profiles doctor <name> | Diagnose where it is declared, binary, port, user-data-dir, onboarding state. Fails where when an identity-bearing name (exactly one declaring device) is a loopback endpoint on a box that is not the declaring device — the original comet-local bug. |
profiles create flags:
| Flag | Description |
|---|---|
-b, --browser <type> | Required. One of: chrome, comet, chromium, brave, edge, arc, custom. arc is recognized but NOT drivable — Arc exposes no CDP page targets and crashes on tab creation, so agents browser refuses it with a clear error; pick a Chromium-family browser to automate |
-e, --endpoint <url> | CDP endpoint URL (repeatable). Auto-assigned if omitted |
-s, --secrets <bundle> | Secrets bundle for this profile: injected as env vars at launch, AND the credential store for browser type --secret (keys <PREFIX>_USERNAME/<PREFIX>_PASSWORD). Warns if the bundle doesn't exist yet |
-d, --description <text> | Human-readable description |
--headless | Run in headless mode |
--window <WxH> | Window size in CSS pixels (default: 1512x982, MacBook Pro 14") |
--position <X,Y> | Window position on screen |
--binary <path> | Absolute path to browser binary (required for --browser custom) |
--electron | Treat as an Electron desktop app; never creates new targets |
--target-filter <expr> | Pick the visible CDP page target. Format: url:<substring> or title:<substring>. Requires --electron |
Which browser shows YOU a page (browser.viewer)
Two browsers exist on a machine like this, and they are not interchangeable:
- the OS default handler — whatever
open/xdg-openresolves to - the configured profile — what
agents browserdrives, and where the fleet's logins accumulate (agents browser profiles loginslists them)
Anything the CLI shows you — a rendered artifact, agents feedback, a login
dashboard, agents sessions trace --open — goes through one seam that resolves
the viewer once:
browser.viewer | Result |
|---|---|
| unset | follows browser.profile, i.e. the profile agents drive |
| a profile name | that profile |
os | the OS default handler |
Set it with agents config set browser.viewer <name>, or agents config set browser.viewer os to keep the OS default handler.
External tools reach the same seam through agents browser show <url|file>,
which is the entry point to use instead of navigate for anything a person is
going to read — navigate binds a task, and the abandoned-task reaper closes a
task's tabs. --os-browser forces the OS handler for one call; --json reports
where it landed (profile, os, or none).
Two deliberate carve-outs. Screenshots, PDFs and recordings go to the OS default
app regardless — Preview and QuickTime are the better viewer and a CDP tab is
a downgrade. And the viewer tab is bound to no task, so the abandoned-task
reaper never closes a page you are reading; stop/done leave it alone too,
because it is your tab now.
If the viewer cannot be reached — profile missing, Arc (which exposes no CDP page targets), not launchable here — the call falls back to the OS handler and prints one line saying why. It never silently ignores your configuration.
Identity-bearing names vs loopback endpoints
A name declared by exactly one device is identity-bearing. The daemon resolves it without the caller naming a machine:
- this device declares it → connect locally
- only other devices declare it → rewrite
cdp://localhost:Ntossh://<device>?port=Nand tunnel; unreachable declaring devices fail loud (never a local logged-out fallback) - nobody declares it → loud error. If the name still lives in the central
browser:map, the error tells you toprofiles claimit on the machine that hosts the browser; otherwise it lists similar names and who declares them
A name declared by several devices is fungible: each box uses its own.
cdp://localhost:9333 on an identity-bearing name is the original
comet-local bug when a worker evaluates it locally — five real logins on
zion, a bare logged-out chromium on the worker, same name. The daemon now
tunnels that shape. profiles doctor still flags it (FAIL where) so a
worker does not report a green local binary/port for someone else's browser,
and profiles prune notes it in the kept reason. Diagnose the real browser
on the declaring device.
Leftover central browser: entries (the pre-registry comet-local sitting in
~/.agents/agents.yaml) are not claimed on upgrade. On the machine that owns
the browser:
agents browser profiles claim comet-local
Nothing claims implicitly — an implicit claim would race across devices.
--device is only valid on agents browser start. Later verbs (navigate,
screenshot, type, stop --profile, …) resolve the device from the task
and reject --device. --device all is rejected: a task lives on one device.
agents browser start --task post --device zion --url https://x.com/
agents browser type --task post --ref @e3 "hello"
agents browser screenshot --task post
Cleaning up dead profiles (prune)
Profiles accumulate — an agent mints one for a task and never removes it.
agents browser profiles prune removes the ones that are provably dead:
| Reason | Meaning |
|---|---|
binary-missing | The profile's browser/binary is not installed on this machine, so it cannot launch here at all |
never-used | No runtime dir has ever been created for it (~/.agents/.cache/browser/<name>* is absent) |
agents browser profiles prune --dry-run # preview; changes nothing
agents browser profiles prune # apply
agents browser profiles prune --json # machine-readable plan
Four guards, each because removing that profile would be wrong rather than untidy:
- In use — a live browser, an SSH tunnel, or an open task on any of its
runtime dirs, runtime-key (
<name>@<endpoint>) dirs included. - This machine's configured default — a bare
agents browser startresolves to it. - The auto-detected profile (
auto-chrome, or a legacydefault) — regenerated on demand, so pruning it is pure churn.
prune only considers profiles this device declares. A name declared only on
another device is left alone (and reported if it is misfiled).
Removing a profile drops its config entry and wipes its cache dirs, exactly like
profiles remove.
A profile config records no creation time, so one you created seconds ago and have not started yet is indistinguishable from an abandoned one and reports
never-used. Preview with--dry-runfirst.
Session lifecycle
| Command | Description |
|---|---|
agents browser start | Start a browser task; prints task name to stdout |
agents browser done | Complete the task and close its tabs |
agents browser stop | Stop a task (or --profile <name> to detach whole profile) |
agents browser status | Show running tasks (interactive picker in TTY) |
agents browser tasks | List all tasks in non-interactive table form |
agents browser ps | List all tracked browser/electron/tunnel processes, alive or stale |
agents browser history | Recent task history |
agents browser stream [--task <name>] | Keep one CLI process and daemon IPC socket open; NDJSON requests in, NDJSON responses out |
agents browser prune [--dry-run] | Run the abandoned-task reaper now instead of waiting for the daemon's next tick (alias: gc) |
start flags:
| Flag | Description |
|---|---|
-p, --profile <name> | Profile to use (auto-picks if omitted) |
--task <name> | Override auto-generated task name |
-e, --endpoint <name> | Endpoint preset within the profile |
-u, --url <url> | Open URL in first tab. If an abandoned task on this profile already holds a tab showing that exact URL, the tab is reclaimed instead of a duplicate being opened (RUSH-2622) — a tab held by a live task, or one you opened yourself, is never taken |
--fresh | Always open a new tab, skipping the reclaim above |
--no-skills | Skip domain-skill auto-discovery |
--record | Start recording immediately after tab opens |
--fps <n> | Recording frames per second (1–30, default 5) |
--duration <sec> | Recording duration cap (default 60s) |
--max-mb <mb> | Recording size cap (default 25 MB) |
Tab hygiene — automatic reaping (RUSH-2622)
agents browser done / stop close a task's tabs, but agents routinely never
call them — the run ends, the process exits, and the tabs stay open in the
profile window forever. The daemon runs a periodic reaper so leftover tabs
don't pile up, on the same 5-minute cadence as its other housekeeping ticks:
-
Session-end reap. When the agent session (or run) that started a task is no longer alive on this host, the daemon closes that task's tabs and marks it done — the same code path
doneuses. This always runs; there is no way to turn it off. -
Idle reap. A task with no IPC action (navigate, click, type, screenshot, evaluate, …) for
browser.task-idle-minutes(default 30) is closed the same way. Set it to0to disable idle reaping only — a task with no recorded identity then never gets closed by this path, though session-end reaping still catches every task that does carry one:agents devices config <this-machine> browser.task-idle-minutes 15 agents devices config <this-machine> browser.task-idle-minutes 0 # idle reap offMachine-local, like
browser.profileandbrowser.remote-control— it lives in this box's own config and cannot be set for a peer.
Both reasons are conservative: only tabs in task.tabs are ever closed (a tab
you opened yourself is never touched), a task mid-recording is always left
alone, and the shared profile window itself is never closed or killed.
Run the same pass on demand instead of waiting for the next tick:
agents browser prune --dry-run # list what would be closed, close nothing
agents browser prune # actually close it (alias: agents browser gc)
agents browser prune --idle-minutes 5 # override the idle window for this run
Driving another machine's browser (--device) and consent
--device is only valid on agents browser start. It binds the task to that
device; later verbs resolve the device from --task (or caller identity) and
reject --device. Identity-bearing profiles do not need --device at all —
the daemon tunnels to the declaring device from the name.
--device all is rejected because a task lives on one device. A verb that
names an unknown task lists the open tasks and exits non-zero.
Because start-with---device lets one machine open a browser on another, the
target decides whether it allows it:
| Command | Description |
|---|---|
agents browser remote-control | Print whether this machine accepts remote drives |
agents browser remote-control on | Allow other fleet machines to drive this browser |
agents browser remote-control off | Refuse remote drives (the default) |
Consent is a per-device setting (the browser.remote-control config key,
in this machine's devices/<machine>/agents.yaml config:) and off by
default: a browser --device <this-machine> request that would open a
browser is refused with a message naming how to enable it, until the owner runs
agents browser remote-control on here. The key is machine-local — only this
box can set it. Local drives (no --device) are never gated.
The gate lives in the browser daemon, at the top of resolveOrCreateTask — the
one chokepoint every task-scoped verb resolves through — plus BrowserService.start
for the task-less browser start command. It has to live in the daemon, not on
the browser start command: navigate, click, screenshot, tab-add and the
other page verbs launch or attach to a browser implicitly, so gating the one
command left every one of them ungated. Daemon/profile queries that resolve no
task — status, profiles list — are not gated, so a peer can still discover
what is running without consent.
The gate covers both launching and attaching. Whether a fleet-remote
request opens a new browser or resolves to a task that already exists —
--task <name>, or the single-match-by-caller-identity path — it is refused with
consent off (RUSH-3064). Earlier the two attach early-returns and tabAdd
bypassed a create-only gate, so a remote tab-add --device <box> --task <name>
could drive the owner's authenticated profile with consent off (status is
ungated by design, so task names are discoverable). That bypass is closed: with
remote-control off, any fleet-remote verb that resolves a task — drive
(navigate/click/tab-add), close (done/stop <task>), or observe
(console/tab-list) — is refused whether or not the task already exists,
because they all flow through resolveOrCreateTask. The one task-less mutation
that does not — stop --profile <name> (terminate a whole profile's browser) —
is a separate, pre-existing gap tracked in RUSH-3179. Local drives (no
--device) remain ungated.
The marker travels on the request, not in the daemon's environment. The
daemon is shared and long-lived, and one auto-started by a fleet-remote CLI
inherits AGENTS_FLEET_REMOTE=1 for its whole life — a daemon that read its own
environment would refuse every later local drive on the machine.
Navigation
| Command | Description |
|---|---|
agents browser navigate [url] | Navigate current tab (positional or --url; alias goto). Creates a task when none resolves for this caller |
agents browser tabs | List open tabs |
agents browser tab add --url <url> | Open URL in a new tab |
agents browser tab focus <tabId> | Switch to tab by ID, prefix, or URL substring |
agents browser tab close [tabId] | Close a tab; omit to close all |
Interaction
| Command | Description |
|---|---|
agents browser refs | Get numbered refs for interactive DOM elements |
agents browser click <ref> | Click element by ref |
agents browser type <ref> --text <text> | Type text into element; --clear to empty first |
agents browser type <ref> --secret <bundle>/<KEY> | Type a credential resolved in-process from a secrets bundle — the value never crosses stdout or the transcript (leak-free login) |
agents browser press <key> | Press a key (Enter, Tab, Escape, etc.) |
agents browser hover <ref> | Hover over element by ref |
agents browser scroll | Scroll by pixels; --dx horizontal, --dy vertical, --at-x/--at-y origin |
agents browser upload | Upload files; supports hidden inputs, drag-drop, OS chooser interception |
agents browser set viewport <W> <H> | Set viewport size; --mobile, --scale |
agents browser set device <name> | Emulate a device preset (iPhone 14, iPad, MacBook Pro) |
agents browser devices | List available device presets |
agents browser download --path <dir> | Set download directory for a task |
agents browser waitdownload | Wait for a download to complete |
upload flags:
| Flag | Description |
|---|---|
-r, --ref <n> | Ref of the upload target (file input or drop zone) |
--trigger <n> | Ref of a button that opens the OS file chooser |
-f, --file <path...> | Absolute path(s) to file(s) (repeatable) |
--drop | Force drag-drop pattern |
--input | Force file-input pattern |
--timeout <ms> | Timeout for chooser interception |
Observation
| Command | Description |
|---|---|
agents browser screenshot | Capture current tab; path printed to stdout |
agents browser evaluate [expr] | Run JavaScript (positional, -e, or --file; alias eval) |
agents browser console | Read console logs; --level (log/info/warn/error), --clear |
agents browser errors | Read uncaught page errors; --clear |
agents browser requests | Captured network requests; --filter <text> |
agents browser responsebody <url-pattern> | Wait for and read a response body |
agents browser logs | Read app JSONL logs; --task (or identity), --source, --lines, --since, --until, --level, --message, --filter |
screenshot flags:
| Flag | Description |
|---|---|
-t, --tab <tabId> | Tab to capture (defaults to current) |
-o, --output <path> | Specific output path; auto-saves under sessions/ |
-q, --quality <mode> | compressed (JPEG, ~100 KB cap) or raw (PNG pixel-faithful) |
Recording
| Command | Description |
|---|---|
agents browser record start | Start recording; auto-saved under sessions/ |
agents browser record stop | Stop recording; prints output path to stdout |
record start flags: --fps, --duration <sec>, --max-mb.
History and discovery
| Command | Description |
|---|---|
agents browser history | Recent task history; --limit <n> |
agents browser refs --all | Include non-interactive elements; --limit <n> |
agents browser wait | Wait for a condition: --time, --selector, --url, --fn, --state |
agents browser sessions | Browse captured screenshots, PDFs, recordings, and downloads, grouped by task. agents sessions --browser is the same view. |
agents browser sessions flags: --profile <name> (default: every profile with
captures), --open [selector] (open latest or a filename match in the OS
default app), --json, --no-interactive.
On a real terminal (no --json/--open/--no-interactive), sessions opens an
interactive, task-first browser: one row per browser task, newest first —
not one row per screenshot. Each task records the agent session that started it
(AGENT_SESSION_ID, resolved in the calling CLI process — the shared browser
daemon cannot know it), plus owner and launchId. That identity is written
once at task start to a durable browser_sessions row in the local session DB
and is never deleted, so a task still links to its session after
agents browser stop and after a daemon restart. The preview pane then shows
the same digest as agents sessions (prompt, changes, tests, last response),
followed by that task's captures newest-first with filename/age/size.
A row shows unresolved when it recorded an identity this machine cannot index (a rotated or peer-owned session), and unlinked when no identity was recorded at all — captures taken before this shipped, whose identity was discarded at stop and cannot be recovered. Either way the captures are still listed and openable. Downloads sit in their own row, separate from any task.
The DB row is metadata only — task, profile, identity, timing, and the
capture directory path. The screenshots, PDFs and recordings themselves stay on
disk under ~/.agents/.cache/browser/<profile>/sessions/<task>/; nothing copies
them into the database, and the listing counts captures by reading that
directory rather than trusting a stored tally.
Known gap — --device drives record no session. agents browser start --device <device> runs the CLI on the remote box, and the SSH dispatch forwards only
AGENTS_ACTOR* and AGENT_TERMINAL_ID — not AGENT_SESSION_ID. A task started
that way therefore records no session and lists as unlinked. Local drives are
unaffected. Forwarding session identity across the SSH hop is tracked on
RUSH-2549 and is not fixed here. Search matches task name, profile, the
linked session's agent/topic, or an artifact filename; enter opens the
highlighted capture directly (or drills into a capture list first when a task
holds more than one). --no-interactive prints the flat per-artifact table
instead — the stable, scriptable surface --json also uses.
Profile Schema
Profiles are stored in the browser: map of each declaring machine's
~/.agents/devices/<machine>/agents.yaml. The fleet registry is the read-time
union of those files. Use agents browser profiles show <name> --json to
inspect the full config, or agents browser profiles list --json to see which
devices declare each name.
The fields map to:
| Field | Type | Description |
|---|---|---|
name | string | Lowercase alphanumeric with hyphens |
browser | string | chrome, comet, chromium, brave, edge, arc, or custom (arc is recognized but not drivable — see the --browser flag note above) |
endpoints | string[] or map | CDP URLs: cdp://host:port, ssh://host?port=N, or wss://... |
defaultEndpoint | string | Key into endpoints map to use by default |
binary | string | Absolute path; required for browser: custom |
electron | boolean | Suppress Target.createTarget; bind to visible window |
targetFilter | string | url:<substring> or title:<substring> for Electron window selection |
description | string | Human-readable label |
secrets | string | Secrets bundle name to inject at browser start |
chrome.headless | boolean | Run headless |
viewport | {width, height, x?, y?} | Initial window size in CSS pixels |
logDir | string | Local path to source-side JSONL logs |
logHost | string | SSH host where logDir lives |
Recipes
1. Create a profile and log in manually
# Create the profile (auto-assigns a free port)
agents browser profiles create work --browser chrome
# Start a session and open the app
export AGENTS_BROWSER_TASK=$(agents browser start --profile work --url https://app.example.com)
# Complete the login in the browser window that opens.
# Then verify onboarding is done:
agents browser profiles doctor work
# On future runs, cookies are already there.
Logins persist across browser restarts, including sites that issue
memory-only session cookies (expires=-1, e.g. idealista): each launch pins
session.restore_on_startup: 1 in the profile's Preferences, which stops
Chromium purging session cookies at startup, while --no-startup-window
keeps the visible side of session restore from ever happening — no tabs from
a previous task reopen. The only logouts left are server-side session
expiries, which no client can prevent.
2. Screenshot a logged-in page
export AGENTS_BROWSER_TASK=$(agents browser start --profile work --url https://dashboard.example.com)
# Wait for the page to load, then capture:
agents browser wait --state networkidle
P=$(agents browser screenshot)
# P is the path to the saved JPEG; pass it to your vision model.
agents browser done
3. Extract data with evaluate
export AGENTS_BROWSER_TASK=$(agents browser start --profile work --url https://app.example.com/orders)
agents browser wait --selector "table.orders"
agents browser evaluate --expression "
Array.from(document.querySelectorAll('table.orders tr')).map(r =>
Array.from(r.querySelectorAll('td')).map(c => c.innerText)
)
"
agents browser done
4. Drive an Electron app (e.g. Slack)
# Create the profile once
agents browser profiles create slack \
--browser custom \
--binary "/Applications/Slack.app/Contents/MacOS/Slack" \
--electron
# Then use it exactly like a web profile
export AGENTS_BROWSER_TASK=$(agents browser start --profile slack)
agents browser screenshot
agents browser refs
agents browser click 7
agents browser done
5. Attach to a remote Chrome via SSH
# The profile stores the SSH endpoint; the daemon opens the tunnel at start time
agents browser profiles create staging \
--browser chrome \
--endpoint "ssh://deploy@staging.example.com?port=9222"
export AGENTS_BROWSER_TASK=$(agents browser start --profile staging)
agents browser navigate --url https://internal.staging.example.com
agents browser screenshot
agents browser done
Demo
See also
- docs/pty.md — drive REPLs and TUI programs from an agent; part of the automation triad
- docs/computer.md — drive native macOS apps via Accessibility; part of the automation triad
- docs/concepts.md — DotAgents repos, resource resolution model