# Horizon Browser

> Control, inspect, or audit a live browser panel inside Horizon through the Horizon browser MCP tools.

- Skill: `peters/horizon-browser` (Agent Skill)
- Install (CLI): `npx skillmds@latest add peters/horizon-browser`
- Raw SKILL.md: https://api.skillmd.com/api/skills/peters/horizon-browser/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: peters (https://skillmd.com/u/peters)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/peters/horizon-browser

---


# Horizon browser control

Use the `browser_*` MCP tools as the only agent-facing browser contract. Do
not inspect Horizon runtime files, connect to raw CDP/BiDi/WebDriver endpoints,
or invoke a browser-control CLI. If the MCP tools are unavailable, report that
the Horizon browser MCP server is not connected.

Start with `browser_list` when the panel id is unknown. If it returns no panels,
call `browser_create`; this opens a panel in the current agent's Horizon
workspace and returns its ready panel id once the backend is ready and, when
you passed a `url`, once that page committed (`navigation: committed`). A
`navigation: pending` result means the panel is controllable but the first
page had not committed within the bounded startup wait, so use `browser_wait`
or `browser_panel` before reading it; `navigation: failed` means that page
failed to load (`navigation_error` says why) and you must navigate again or
fix the URL; `navigation: superseded` means the user navigated the panel
first, so read `panel.url` before acting. If `browser_list` returns a usable panel, reuse
that panel for iframe, popup, dialog, and consent interactions. Never create or
reveal a helper panel as a workaround. Only when the user explicitly requests
another independent browser session may you call `browser_create` with
`allow_additional: true`. Omit `backend` to use Horizon's
configured browser, or select `chromium`, `firefox`, or `safari` when the
platform supports it. To run at a configured remote target instead of a
local browser, pass `target` with its name and omit `backend`; Horizon
resolves the provider and credentials from its configuration, and a
refusal carries a typed code and at most the target, provider or credential
reference name, never a credential value. Such a panel reports
`remote_target`, classic WebDriver and no network capture; whether the
target is physical hardware is its configuration, not something the call
verifies, so do not report it as real-device evidence on its own. Set `visible: false` for background automation; use
`browser_visibility` to show or hide the live panel later without stopping its
session, capture, ownership, or MCP control. Call `browser_close` on a panel
you own when the user is done with it or a remote device session must be
released now; it stops the session, releases any remote allocation, and the
panel leaves `browser_list`. Read anything you still need from
`browser_audit` before closing: it answers only for a live panel. An optional bare-host `url`
defaults to HTTPS while explicit HTTP remains available. Use `browser_panel`
for a known panel. Discovery and control are scoped to the workspace that
contains your agent panel: `browser_list` never shows panels from other
workspaces, every other tool rejects their ids, and a panel's `visible` field
is host presentation state, not proof that the panel is in your workspace. If
nothing usable is listed, create a panel rather than guessing an id. Before
interacting, call `browser_snapshot` or `browser_query` and prefer its
short-lived `ref` in `browser_act`. Navigation, another snapshot or query, and
`browser_wait` can invalidate earlier refs, so reacquire a ref immediately
before an action when the page may have changed.

Snapshots expose iframe boundaries as `iframe` nodes. If the current top-level
semantic tools cannot reach the embedded frame content, call `browser_handoff`
on the original panel so the user can complete the interaction; do not open a
separate panel for the frame.

`browser_navigate` returns a typed outcome: by default it waits until the
document committed and reports `committed_url`, `title` when known, `loading`,
`redirected`, and `state`. Check `completed`; a `timed_out` state carries the
latest page state so you can inspect or retry, and `wait: dom_content_loaded`
or `wait: dispatched` (handed to the backend, browser acceptance not awaited)
change how long it waits; `timeout_millis` is raised to
at least 1000 ms, and on Safari every wait returns once the page loaded or the
bound elapsed. After navigation or
interaction, verify the visible outcome with `browser_wait`, `browser_query`,
or a new snapshot. `browser_wait` is one audited engine-side action that
observes the page itself: it returns the matched nodes and `elapsed_millis`,
and fails with a typed code (`wait_timeout`, `wait_navigation_invalidated`,
`wait_ownership_lost`, `wait_handoff_pending`, `wait_superseded`,
`browser_unavailable` when the backend stops) instead of looping on queries,
so do not poll it in a tight loop; pick a `timeout_millis` that covers the
expected change. Use `browser_evaluate` only when the semantic tools cannot
answer the question.

For HTTP or WebSocket observation, first inspect the panel's
`network_capture` field from `browser_list` or `browser_panel`. When supported,
call `browser_network` with `operation: start` **before navigation** so open,
frames, errors, and close are all observed. Use URL filters and payload/file
limits for busy streams. To capture HTTP response content, set both
`include_http: true` and `include_http_bodies: true`, and check
`http_response_body_transport` first. Bodies appear as bounded
`http_response_body` records; they may contain sensitive page data and never
belong in the action audit. The result returns live connection counters and one
private NDJSON export path. Prefer `browser_network_watch` for event-driven
monitoring: filter by URL and event kind, leave payloads excluded unless needed,
then pass the returned `capture_id` and `next_sequence` into the next call. It
reports timeout, capture stop/replacement, gaps, drops, truncation, file limits,
and writer failure explicitly. For sustained local analysis, it is also safe to
inspect the exact path returned by `browser_network` with read-only tools such
as `tail -f`, `jq`, or `rg`; never infer or inspect another Horizon runtime
path. Call `operation: stop` to flush the capture.

For page-pixel recording, inspect `video_capture` then call `browser_video`
with `operation: start`. Optional start-only knobs: `quality` (1-100),
`compression_level` (0-10, higher is slower/smaller), `fps` (1-30),
`max_width` (320-1920), `max_file_bytes`. Pause skips time in the file;
resume continues the same WebM; stop finalizes a private `.webm` path.
Page pixels never enter the action audit. The recording samples the existing
decoded frame slot on Chromium, Firefox, and Safari.

Chromium HTTP bodies and WebSocket frames are protocol-native, but CDP cannot
return a `fetch()` body the page drained with `response.blob()`; that
`http_response_body` record carries an `error` and no `payload`, so when the
bytes matter, read `text()` or `arrayBuffer()` or leave the body unread. A
top-level navigation to a PDF is different: it captures the viewer's HTML
shell as a normal successful body, never the PDF bytes. Firefox HTTP
bodies are native WebDriver BiDi, while WebSocket frames use page
instrumentation because standard BiDi does not expose them; the panel
advertises both distinctions. Safari network capture is currently unsupported.
Do not describe Firefox WebSocket instrumentation as undetectable.

When the user must steer, call `browser_handoff` with a concise reason and stop
issuing actions. Poll `browser_list` until `handoff_pending` becomes false,
then take a fresh snapshot before continuing. Use `browser_audit` to review the
redacted ordered action history or to verify a specific action id. The default
page is the newest matching records (`limit` 1-500, default 100). To iterate
every retained record, call with `from_start: true` and reuse `next_event_id`
as `after_event_id` until `has_more` is false. Treat `cursor_lost`,
`malformed_records`, and `older_records_dropped` as explicit loss.

