Step 1: handshake with capabilities
Ask the engine what it speaks before you assume anything:value is:
envelopeSchema and readySchemaVersion against what your parser expects. outputJsonCommands is read off the live registrations, so it never drifts from the release that you run.
Step 2: start a session with machine output
--allow-control unlocks the bounded act verbs (storage, reload, open, inspect). Add --allow-eval only when you need extension eval, it runs arbitrary code and writes a 0600 session token.
Under --output json the command prints one started envelope, and failures arrive as envelopes too. Human copy moves to stderr, so stdout stays parseable.
To follow the whole session frame by frame, set EXTENSION_OUTPUT=ndjson in the environment and read the lifecycle stream.
Step 3: gate on runtime attached
Readiness has two phases, and acting too early is the most common agent failure:ready.jsonreportsstatus: "ready". This means compiled, nothing more.- The contract carries
runtime: "attached"andexecutorAttachedAt. Now the service worker is connected and can be driven.
dist/extension-js/chromium/ready.json until runtime equals "attached" before you run any act verb. See ready.json for the freshness and pid-liveness rules that keep you off stale contracts.
Step 4: act and read envelopes
Every act verb takes--output json and answers with one schema-1 result envelope:
navigate points a tab at a url through the extension itself, as a static
tabs call rather than an eval, so it works on every engine including a
Safari MV3 background that refuses eval. It reuses the active tab, takes
--tab <id> for a specific one, and --new-tab (with --background to
leave it unfocused) to open another. Firefox refuses privileged pages such
as about:newtab and about:config from an extension’s tabs API, so
navigate answers E_ARGS for them there with a hint naming the rule, while
chrome://newtab/ opens on Chromium. Chromium refuses javascript: urls the
same way, and the hint points at eval instead. Web urls and about:blank
work on every engine. A --tab id no open tab carries answers
E_TARGET_NOT_FOUND on every verb, with inspect --list-tabs as the way to
find one.
Branch on ok and error.code, never on error.message. The error code table is stable across releases.
Discover the CLI with —ai-help
extension --ai-help --output json prints a machine-readable self-description. Two mechanics matter to a caller:
--ai-helpbypasses the argument parser. It runs before any subcommand parsing, so it works with otherwise-invalid invocations.- The payload can outgrow one pipe buffer, so the process drains stdout before it exits. Read until EOF.
The
capabilities.readyContract block names the contract paths, statuses, fields, and event types, so an agent can self-configure without this docs site.
Rules that keep agents honest
- Never parse pretty terminal output. Every fact that you need has a machine surface.
- Verify
pidliveness and contract freshness before trustingready.json. - Join
ready.json,events.ndjson, andlogs.ndjsononrunId. - Use
browserPidfromready.jsonto tear the browser down, never process-name matching. - Treat unknown envelope fields as additive. The schema grows, it does not break.
Next steps
- Keep the contract details open: ready.json and Result envelope.
- Wire the same loop into CI with CI templates.

