Automate with browser control
octo drives a real Chrome tab over the DevTools Protocol (CDP) — no headless-browser framework dependency, no separate runtime.
octo browser setupConnecting to Chrome
Section titled “Connecting to Chrome”Three ways the browser tool gets a page, tried in this order:
- A known debug port (
browser.connect_portinconfig.yml) — connects directly, no fallback if it fails. This is whatocto browser setupwires up for you. - Attach via a specific profile (
browser.attach_running: trueplusbrowser.user_data_dir) — reads that profile’sDevToolsActivePortfile and connects to the Chrome that owns it. - Attach to any Chrome already running with remote debugging enabled
(
browser.attach_running: truealone, and also the default when nothing is configured) — discovers a debugging-enabled Chrome and reuses your logins; never hijacks a browser that doesn’t already have debugging turned on.
There is no launch fallback: when no attachable Chrome is found, the tool returns instructions for enabling the remote-debugging toggle rather than silently starting a throwaway instance that would carry none of your logins.
Whichever path connects, octo always opens a brand-new tab rather than reusing one you have
open (including its own Web UI tab) — driving an existing tab requires an explicit pages/
select_page action. The connection is reused across the whole session, so you approve any
debugger-attach prompt once, not on every navigation.
octo browser setup
Section titled “octo browser setup”octo browser setupWalks you through enabling chrome://inspect/#remote-debugging (Chrome 136 stopped honouring
--remote-debugging-port when it points at the default user-data directory, and Chrome 144 added
this in-browser checkbox as the way to debug that profile — which is the one carrying your logins),
then probes port 9222 in a loop — confirming not just that it connects, but that a
real page-level CDP call succeeds (a browser-level connection can succeed while page control still
fails on recent Chrome). On success it saves browser.connect_port to your config; on failure it
prompts you to flip the toggle and retry, or quit and resume later.
Recording
Section titled “Recording”There’s no CLI command for this — recording is driven by the browser tool’s record_start /
record_stop actions, either called by the model or via the Web UI’s Record button.
The key thing to understand: it records what you do, not what the model does. After
record_start, the agent hands the browser to you; you perform the steps yourself, then tell the
agent you’re done and it calls record_stop <name>.
What gets captured, per step: the action (click / type / select / upload / navigate), a
selector anchored at the nearest element with a stable id (not a fragile position-based chain),
the element’s visible text, and the URL — plus a redundant fingerprint of the target (alternate
selectors built with different strategies, its role attribute, and the nearest label-like
neighbor text), which is what lets replay survive wholesale class churn (see Replay below).
The recorder also captures what happens between your actions:
- A click that triggers fetch/XHR activity gets an automatic network wait step; one that opens a modal / date picker / overlay gets an element wait — so replay never races ahead of a page that hasn’t settled.
- A click that starts a browser download is upgraded to a
downloadstep bound to afile[]output, so replay captures the file. - Provably-redundant fumbles are compressed away deterministically before any model sees them: retyping the same field keeps only the final value, and a click-away-click-back detour collapses to the single final click.
A model pass then distills the raw capture — dropping dead-end detours, turning your specific
values into {{param}} placeholders, and writing a description — but it can only reorder or rename
real captured steps, never invent a new target: any refined step whose selector isn’t in the
original capture is rejected in favor of the raw version (fingerprints are re-attached
deterministically after the pass).
record_stop replies with a numbered run-plan — each step with what it does and what its check
is — for you to confirm or correct before the recording is used.
Recordings are saved as plain YAML at ~/.octo/browser-recordings/<name>.yaml — readable, editable by
hand, and diffable in git. (Before the recording rename this was ~/.octo/browser-skills/; an existing
directory is migrated automatically.)
Replay
Section titled “Replay”browser(action: "replay", name: "<recording>", params: { ... })(run_skill remains accepted as a deprecated alias.)
Replay is deterministic — no model call in the common case. Each step waits for its target,
executes, and (if the recording declares one) checks a verify. A few robustness details worth
knowing:
- A step recorded with a fingerprint re-identifies its target by scoring candidates (from the original selector, the alternates, and text/role scans) against the recorded text, role, tag and neighbor text. A page whose CSS classes all changed still resolves; a positional selector that now matches the wrong element is refused outright — an explicit failure into self-heal, never a silent wrong click. Older recordings without fingerprints keep the previous behavior: exact selector first, then an element carrying the same recorded label text.
- Auto-inserted
waitsteps settle the page (network idle, or a specific element appearing) before the next action fires. - A
downloadstep clicks its trigger, waits for the browser download to complete, and binds the saved file’s path to its declared output. - If a step opens a new tab, later steps automatically follow it.
- After typing, an unexpectedly empty field gets one clear-and-retype before the step is marked failed.
The result is a structured object — {recording, steps, outputs, self_healed?} — so a recording’s
declared outputs (an extracted value, a downloaded file path) can feed directly into a
workflow via recording("<name>", params).
Secret params
Section titled “Secret params”A param declared with secret: true is a password-class value that never enters the
conversation — not the transcript, not memory, not the provider’s context. Recording already
guarantees the value never lands in the YAML (password fields are captured empty and declared
without a default); replay completes the guarantee by collecting the value at runtime, out-of-band:
params: - name: password description: secret value (not stored; provide at replay) secret: trueWhen a secret param has no supplied value, replay resolves it in this order:
- An explicit
paramsentry from the caller always wins. - The session cache — a value already provided this session is reused silently (in-memory only, evaporates when the session is deleted; never written to disk).
- Environment —
OCTO_BROWSER_SECRET_<NAME>(param name uppercased, non-alphanumerics as_:password→OCTO_BROWSER_SECRET_PASSWORD,api_token→OCTO_BROWSER_SECRET_API_TOKEN). Set it in the process environment or in~/.octo/serve.env(mode0600, loaded at startup). Same-named params across recordings share one env value. This is the path for cron and other unattended replays. - A masked prompt — the TUI reads with echo off; the Web UI shows a password field.
IM chats (WeChat, Telegram, …) deliberately can’t collect secrets: a value typed there persists
in platform history, which no client-side care can undo. A replay that hits a missing secret on IM
fails with an error pointing at serve.env or the Web UI / TUI instead. Error messages name the
recording and the param — never the value.
Self-heal
Section titled “Self-heal”If a step’s selector no longer matches anything — and only if a model is configured for this purpose — octo takes a text digest of the page’s currently interactable elements (selector + visible text, no screenshot, model-agnostic), and asks the model for a single corrected CSS selector given the intended action, the dead selector, and (when the step carries one) the recorded fingerprint — role, tag and neighbor text — turning the request from an open guess into constrained matching. The fix is retried once and, if it works, written back into the recording’s YAML — so a heal is durable, not a one-off patch you’d have to redo on every future replay. (The write-back re-marshals the YAML, so any hand-written comments in the file are dropped on the next healed replay; field values are kept. Version your recordings in git if you annotate them.)
Permissions and vision
Section titled “Permissions and vision”Browser actions go through the same permission engine as any other tool — a click or a form submission on a logged-in account is treated as a real, high-impact action, not a read-only step. Screenshots only come back as an image block if the active model is vision-capable; a text-only model gets a text note instead of a rejected image.
Next: recordings compose directly into workflow scripts via
recording("<name>").