Surf CLI Browser Automation
Overview
Use this skill when browser work should be done through surf commands instead of ad-hoc manual browsing.
Core principle: read page state first, then act, then verify.
- Repo:
nicobailon/surf-cli - Tool style: local CLI + Chrome extension + native host (Unix socket / named pipe)
- Strength: fast deterministic workflows (
surf do ...), robust page interaction via refs/semantic locators, and durable per-agent browser sessions
When to Use
Use this skill when the user asks for:
- Browser automation from terminal/agent
- Clicking/filling/submitting web forms
- Screenshots, page text extraction, state checks
- Network or console diagnostics
- AI queries via browser sessions (ChatGPT/Gemini/Perplexity/Grok/AI Studio/Kimi)
- Multiple agents driving the same or different browser tabs concurrently
Do not use when:
- User only needs static HTTP scraping (use curl/wget first)
- Browser extension/native host is unavailable and cannot be installed
Quick Start
# 1) Install surf-cli
npm install -g surf-cli
# 2) Inspect command surface
surf --help
surf --help-full
surf --llm-context # compact reference built for agents
# 3) Diagnose the native host connection before first use
surf doctor
# 4) Smoke test connection
surf tab.list
If doctor or tab.list fails, load references/setup-and-troubleshooting.md.
Sessions: Required for Concurrent Agents
Every independent agent shell MUST claim a named session before its first browser command. Sessions stop Chrome focus changes from retargeting another agent's commands.
export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
surf session.ensure "$SURF_SESSION" about:blank
# All later tab-scoped commands use that session automatically
surf go "https://example.com"
surf read
session.ensure is idempotent: it creates a missing session, reuses a live one, and reopens a stale/closed one. Give each agent a distinct session name — never share one across concurrent agents in different worktrees/directories.
Other session commands: session.new, session.list --refresh, session.info <name>, session.close <name>, session.cleanup --idle-after 1h, session.rebind, session.reopen. Load references/advanced-features.md for full session, remote, and playbook detail.
Default Interaction Loop
# Navigate
surf go "https://example.com"
# Read page model (refs and content)
surf read --compact
# Interact
surf click e5
surf type "hello" --selector "#search"
# Verify result
surf wait.load
surf screenshot --output /tmp/surf-result.png
Recommended Command Patterns
1) Reliable element targeting
Prefer in this order:
- refs from
surf read(e1,e2, ...) - semantic locators (
locate.role,locate.label,locate.text) - CSS selectors (fallback)
surf locate.role button --name "Submit" --action click
surf locate.label "Email" --action fill --value "user@example.com"
2) Multi-step automation with do
Use surf do for deterministic flows and less token overhead. Actions that change the page (navigation, click, type, tab switch) auto-wait for completion.
surf do 'go "https://example.com/login" | type "user@example.com" --selector "#email" | type "secret" --selector "#password" | click --selector "button[type=submit]" | wait.load | screenshot --output /tmp/login.png'
Validate before execution when flow is uncertain:
surf do 'go "https://example.com" | click e5' --dry-run
For a reusable flow, save it as a workflow JSON file under the shared-context root and run it by absolute path. You MUST NOT create ./.surf/workflows/ inside the repository.
surf do "$ROOT/RWR-19971/surf-workflows/login.json" --email "user@example.com"
Both surf do <path>.json and surf do --file <path>.json accept any path. A bare name reaches only two hardcoded directories, and a miss fails silently.
Read surf do --json to get the result back: vars carries every as capture. Assert by throwing inside a js step — expect, when, if, retry, continueOnError and until are accepted by the validator and ignored by the runner. See references/advanced-features.md for the storage procedure, the verified step schema, and the --json contract.
3) Debugging workflow
surf doctor # native host / socket health first
surf read --compact
surf page.state
surf console
surf network --limit 20
surf screenshot --output /tmp/debug.png
4) Waiting for real readiness
wait.ready fails fast on a bad page state instead of timing out silently, returning a typed state (ready, empty, login, challenge, not-found, error).
surf wait.ready --selector ".results" --empty-text "No results"
5) AI-through-browser workflow
Use when the user explicitly wants browser-authenticated AI providers (no API key flow).
surf chatgpt "summarize this page" --with-page
surf gemini "explain this" --with-page
surf perplexity "fact-check this" --with-page
surf grok "latest AI agent trends on X" --deep-search
surf aistudio "explain quantum computing" --with-page
surf kimi "summarize" --with-page
For a durable, resumable ChatGPT consult (as opposed to a one-shot query), use surf oracle ask/status/result/follow — see references/advanced-features.md.
If login/model issues occur, load references/setup-and-troubleshooting.md.
High-Value Command Reference
# Navigation & page state
surf go <url> # alias: navigate
surf back
surf forward
surf read --compact # alias: page.read
surf page.text
surf page.state
surf extract <url> --file rows.js # read-only structured extraction into JSON/Markdown
# Interactions
surf click <ref>
surf click --selector "..."
surf type "..." --selector "..."
surf key Enter
surf hover --ref e5
surf select e5 "US" # dropdown option select
# Waiting
surf wait 2
surf wait.element ".ready"
surf wait.network
surf wait.url "/success"
surf wait.load
surf wait.ready --selector ".results"
# Capture
surf tab.switch <tab-id> # required before a screenshot on an unfocused session
surf screenshot --output /tmp/shot.png # alias: snap
surf screenshot --fullpage
surf record --duration 2000 --fps 10 --output /tmp/anim.gif
surf video start ./demo.webm --fps 30
# Diagnostics
surf doctor
surf console
surf network
surf network.get --id <req-id>
surf network.body --id <req-id>
surf network.export --har --output ./trace.har
# Emulation
surf emulate.device "iPhone 14"
surf emulate.viewport --width 375 --height 812
# Workflows & playbooks
surf do 'go "https://example.com" | read | screenshot'
surf do <path>.json --arg value # stored workflow: always by path
surf workflow.info <path>.json
surf workflow.validate <path>.json
surf workflow.list # hardcoded dirs only; misses shared context
surf do <path>.json --json # results + vars (every `as` capture)
surf do <path>.json --on-error continue # do not halt on first failed step
surf playbook list
surf use <site> <op>
Full command surface: surf --help-full. Search by keyword: surf --find <term>.
Aliases
| Alias | Command |
|-------|---------|
| snap | screenshot |
| read | page.read |
| find | search |
| go | navigate |
Common Mistakes
-
Acting before reading state
- Fix: always run
surf readorsurf page.statebefore interactions.
- Fix: always run
-
Skipping session setup with multiple agents
- Fix: run
surf session.ensure "$SURF_SESSION" about:blankbefore any browser command when more than one agent may be driving Chrome.
- Fix: run
-
Using stale refs after navigation/DOM shifts
- Fix: rerun
surf readand use fresh refs.
- Fix: rerun
-
Long flows as many separate commands
- Fix: collapse into
surf dofor deterministic execution, or save a workflow file.
- Fix: collapse into
-
Running a stored workflow by bare name
- A bare name reaches only
./.surf/workflows/and~/.surf/workflows/. A miss does not error: surf parses the name as an inline command and runs one bogus step. - Fix: store workflows under the shared-context root and always pass the absolute path.
- A bare name reaches only
-
Trusting
expect,when,if,retryorcontinueOnErrorin a workflow- The validator accepts all five. The runner reads none of them, with no warning. A step with a failing
expectreportsok; a step withwhenfalse still runs.untilruns every iteration and never exits its loop. - Fix: assert by throwing inside a
jsstep, which does fail the run. Use--on-error continueinstead ofcontinueOnError. See references/advanced-features.md.
- The validator accepts all five. The runner reads none of them, with no warning. A step with a failing
-
Using
semantic.stepwithop: assertto verify an outcome- That op sends the page to a model and returns a confidence judgement, not a fact. It also needs
--allow-semantic. - Fix: use it to find an element if you must, then assert deterministically by throwing.
- That op sends the page to a model and returns a confidence judgement, not a fact. It also needs
-
Screenshotting a session tab that is not visible
session.newopens an unfocused window, so a fresh session fails withscreenshot_target_not_visible. No file is written, and the step fails after the earlier actions already ran.- Fix:
surf tab.switch <tab-id>before the first screenshot. For unattended capture useplaywright-cliinstead — surf drives a visible Chrome.
-
Saving a playbook with
--project--projectis the only thing that writes./.surf/playbooks/into the repository.surf pb saveandsurf playbook importalready default to user scope.- Fix: drop the flag. Keep the canonical copy in shared context and
surf playbook export/importto move it.
-
Ignoring timeouts on slow AI models/pages
- Fix: increase timeout (
--timeout 600) and add explicit waits.
- Fix: increase timeout (
-
Assuming AI tools work without browser login
- Fix: ensure active login session in Chrome for each provider.
Decision Map
[Need browser task]
|
v
[Can use surf?] --no--> [Use alternative tool/path]
|
yes
v
[Multiple concurrent agents?] --yes--> [session.ensure "$SURF_SESSION" first]
|
no
v
[Single action?] --no--> [Use surf do / workflow JSON]
| |
yes v
v [dry-run optional]
[read -> act -> verify] |
| v
+----------------------> [execute + screenshot/log validation]
Adjacent Skills
shared-context: resolves the root that stored workflow JSON is written underagent-browser: alternate browser automation CLIchrome-debug: CDP-first debugging workflowslynx-web-search: research without browser automation