| name | surf |
| description | Control Chrome browser via CLI for testing, automation, and debugging. Use when the user needs browser automation, screenshots, form filling, page inspection, network/CPU emulation, DevTools streaming, or AI queries via ChatGPT/Gemini/Perplexity/Grok/AI Studio. |
Surf Browser Automation
Control Chrome browser via CLI or Unix socket.
Ordinary socket-backed CLI commands report top-level host tool-response errors on stderr with a supplied [code] on the first line and exit 1. --json additionally writes {error:{code,message,details?}} on stdout (missing code becomes "error"). --soft-fail instead keeps the original stderr warning, empty stdout and exit 0, even with --json. This does not cover local validation, transport/parser failures or compound-command errors: do not assume every failure produces JSON. Connection failures remain stderr-only and exit 1, including with --soft-fail.
Native Host / Socket Notes
For WSL2 with Windows Chrome, run surf install <extension-id> inside WSL2. Surf detects WSL2 and writes the Windows-side native messaging manifest plus a wrapper that launches the WSL host. Use surf install <extension-id> --target linux only for Linux browsers running inside WSLg.
On macOS, Chrome reads the native messaging manifest at ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/surf.browser.host.json. If native messaging fails, confirm that file exists, its allowed_origins extension ID matches chrome://extensions, then rerun surf install <extension-id>, restart Chrome, reload the extension, and inspect the extension service-worker console.
If a command reports Socket connect failed, run surf doctor first, then check the Attempted socket: line. Default sockets are /tmp/surf.sock on macOS/Linux/WSL2 and //./pipe/surf on Windows. If SURF_SOCKET is set, the browser-launched host and the shell running surf must use the same value.
For opt-in POSIX group sharing, install with surf install <extension-id> --socket-mode 660 --socket-group <group>. The default remains 0600; mode 660 grants every member of that group full Surf authority, so use a dedicated narrow group. Re-run surf install without those flags to clear the wrapper settings. Remote Surf credentials remain the revocable per-client alternative.
Remote Surf
Remote clients require a per-client credential; Tailnet reachability alone is not authorization. On the POSIX browser host, authorize the client before installing the listener:
surf remote authorize agent-macbook --output ~/agent-macbook.surf-credential.json
surf install <extension-id> --listen 100.101.102.103:4321
surf remote list
Move the mode-0600 credential to the client through a secure channel. It grants full trusted Surf authority. Use it explicitly or through SURF_REMOTE and SURF_REMOTE_CREDENTIAL:
surf --remote 100.101.102.103:4321 \
--remote-credential ~/.config/surf/agent-macbook.json \
page.read
surf --remote surf.example.com:443 --remote-tls \
--remote-tls-ca ~/.config/surf/private-ca.pem \
--remote-credential ~/.config/surf/agent-macbook.json page.read
surf remote revoke agent-macbook
Environment equivalents are SURF_REMOTE_TLS=1, SURF_REMOTE_TLS_CA, and
SURF_REMOTE_TLS_SERVER_NAME. A custom CA replaces system roots; Ed25519 credentials remain
mandatory after TLS validation.
Remote paths are client-local by default. local:./file is explicit client-local syntax; only remote:/absolute/path accesses the browser host directly. Remote transfer supports one upload or ChatGPT/Gemini input and one screenshot, network-export, or Gemini image output. Limits are 256 MiB per file, 512 MiB and 32 files per connection, and 256 KiB decoded chunks. record, aistudio.build, smoke screenshot directories, directories, and multi-file inputs are not supported remotely. Successful action screenshots and failure --auto-capture diagnostics are transferred back to client-local paths.
CLI Quick Reference
surf --help
surf <group>
surf --help-full
surf --find <term>
surf --help-topic <topic>
First Command for Independent Agents
Before the first browser command in each independent agent shell, choose a unique valid session name and ensure its target exists:
export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
surf session.ensure "$SURF_SESSION" about:blank
session.ensure is idempotent. It creates a missing session, reuses a live binding, and reopens a stale or closed tab. Keep SURF_SESSION set for every later tab-scoped command in that shell. Use a distinct worktree/directory name per agent; when agents share one directory, append a stable agent identifier. Use surf session.info "$SURF_SESSION" to inspect the target and queue state.
Core Workflow
surf navigate "https://example.com"
surf page.read
surf click --ref "e1"
surf click --x 100 --y 200
surf type --text "hello"
surf screenshot --full-page --output /tmp/shot.png
surf animate-audit --selector ".thing" --duration 2000 --fps 10
AI Assistants (No API Keys)
Query AI models using your browser's logged-in session. Must be logged into the respective service in Chrome.
ChatGPT
surf chatgpt "explain this code"
surf chatgpt "summarize" --with-page
surf chatgpt "review" --model gpt-5.5
surf chatgpt "analyze" --file document.pdf
Oracle
Use surf chatgpt for quick one-shot questions. Use surf oracle for long-running or Pro coding consults that need a durable job, explicit model and effort selection, file context, a direct local attachment, recovery, or follow-up turns. Oracle is local-only.
For agent workflows, detach after dispatch and keep the returned .id:
surf oracle ask "Review this change and identify release risks" \
--files "src/**/*.ts" --files "package.json" \
--model gpt-5.5 --effort pro --file ./design.md --github --detach --json
surf oracle status <job-id> --json
surf oracle result <job-id> --json
surf oracle result <job-id> --wait --json
status reads persisted state without touching Chrome. result attempts to harvest the answer and returns the job object with response once its state is captured. A Ctrl-C during waiting exits with status 130 and prints Recover with: surf oracle result <id>. Once the job is awaiting, the persisted ChatGPT conversation URL is its durable key, so surf oracle result <id> can recover after CLI exit, native-host restart, or Chrome restart by reopening that conversation.
Treat Pro quota as scarce. Oracle never selects Pro effort implicitly; request it with --effort pro. ChatGPT model aliases include gpt-6-astra, latest, gpt-5.6-sol, and gpt-5.5; latest is an explicit floating choice, while gpt-6-astra must read back as model 6 before submission. Accepted --effort values are instant, medium, high, xhigh/extra-high, and pro. Use --model gpt-6-astra --effort pro for GPT-6 Astra with Pro effort. Requested model and effort selections are read back before submission, and an unverifiable selection fails with model_verification_failed instead of silently continuing. Capacity is one non-terminal oracle job. A capacity error includes the in-flight job ID; poll that job or wait for it to finish rather than submitting the same consult again.
ChatGPT can hide the model version at lower effort settings. Use --model latest if floating model selection is intended; do not retry an unverifiable gpt-6-astra request as latest without the user's approval.
When loaded as a Pi extension, Surf also registers a surf-oracle external-job provider when the runtime exposes that bridge. The provider maps start, status, result, and reattach to durable Surf Oracle jobs and returns pi-subagents' external-job contract shape: providerJobId, a contract state (queued, running, completed, failed), the conversation URL, the captured result text as output, and failure code and message. It honors options.model, options.effort, options.file, and options.github for starts and follow-ups, so model: gpt-6-astra plus effort: pro selects ChatGPT GPT-6 Astra with Pro effort through the browser, while github: true requires Chat mode and the connected GitHub tool. reattach only harvests an existing job by ID; it never submits the prompt again.
When Surf is installed as a Pi package, it exposes an optional gpt-pro package agent for pi-subagents. That profile uses runner.type: external-job, provider surf-oracle, options.model: gpt-6-astra, and options.effort: pro. Surf remains useful without Pi or pi-subagents.
Context comes from repeatable --files globs. Use --file <path> for one additional local attachment; --github requires Chat mode and a connected GitHub tool. Surf fails closed when a glob matches nothing or a matched file is unreadable, binary, or invalid UTF-8. It also blocks gitignored files and basenames matching .env*, *.pem, *.key, id_rsa*, id_ed25519*, *.p12, *.pfx, credentials*, or secrets*. Use --allow-sensitive only after intentionally reviewing those files; it overrides the block rather than redacting content. Context up to 60,000 evidence characters is inserted inline, while larger context becomes one private text attachment. The assembly manifest records each path, byte count, SHA-256, inline or bundle disposition, and deny-list outcome.
Continue a captured consult with follow. Use the ID returned by each turn for the next turn:
surf oracle follow <job-id> "Challenge your recommendation. What could invalidate it?" --file ./follow-up.md --github --detach --json
surf oracle result <follow-job-id> --wait --json
surf oracle follow <follow-job-id> "Give the final decision and concrete next steps." --detach --json
Gemini
surf gemini "explain quantum computing"
surf gemini "summarize" --with-page
surf gemini "analyze" --file data.csv
surf gemini "a robot surfing" --generate-image /tmp/robot.png
surf gemini "add sunglasses" --edit-image photo.jpg --output out.jpg
surf gemini "summarize" --youtube "https://youtube.com/..."
surf gemini "hello" --model gemini-3.5-flash
surf gemini "wide banner" --generate-image /tmp/banner.png --aspect-ratio 16:9
Perplexity
surf perplexity "what is quantum computing"
surf perplexity "explain this page" --with-page
surf perplexity "deep dive" --mode research
surf perplexity "latest news" --model sonar
Grok (via x.com - requires X.com login in Chrome)
surf grok "what are the latest AI trends on X"
surf grok "analyze @username recent activity"
surf grok "summarize this page" --with-page
surf grok "find viral AI posts" --deep-search
surf grok "quick question" --model fast
For exhaustive, multi-angle X research with categorized findings and full post-URL traceability, use the deep-x-research skill (skills/deep-x-research/) instead of a single Grok query.
Grok Validation & Troubleshooting:
surf grok --validate
surf grok --validate --save-models
AI Studio (via aistudio.google.com - requires Google login in Chrome)
surf aistudio "explain quantum computing"
surf aistudio "redteam this" --with-page
surf aistudio "quick answer" --model gemini-3-flash-preview
surf aistudio "analyze" --timeout 600
Why AI Studio over Gemini? AI Studio gives access to less restricted Gemini models. For Gemini 3 Pro the difference can be significant with certain prompts. Downside: aggressive per-day rate limits on Pro and Flash models.
Model selection is best-effort: Pass any AI Studio model id (e.g. gemini-3.1-pro-preview, gemini-3-flash-preview, gemini-flash-lite-latest). If the model isn't found, AI Studio uses whatever model was last selected in the UI.
AI Studio App Builder
surf aistudio.build "build a portfolio site"
surf aistudio.build "todo app" --model gemini-3.1-pro-preview
surf aistudio.build "crm dashboard" --output ./out
surf aistudio.build "game" --keep-open --timeout 600
Automates AI Studio's App Builder at aistudio.google.com/apps. Types your prompt, clicks Build, waits for completion, downloads the generated zip, and optionally extracts it.
--output <dir> extracts the zip to a directory
--model <id> overrides the model in Advanced Settings
--timeout <seconds> build timeout (default: 600s)
--keep-open leaves the AI Studio tab open after completion
Returns zipPath, extractedPath, model, buildDuration, and tookMs.
AI Tool Troubleshooting
When AI queries fail, check these common issues:
- Not logged in: The error "login required" means you need to log into the service in Chrome (chatgpt.com, gemini.google.com, perplexity.ai, x.com, or aistudio.google.com)
- Model selection failed: The UI may have changed. Run
surf grok --validate to check
- Response timeout: Reasoning-heavy models (ChatGPT o1, Grok Expert) can take 45+ seconds. AI Studio builds can take several minutes.
- Element not found: The service's UI changed. Check for surf-cli updates
Debugging workflow for agents:
surf grok --validate
surf grok --validate --save-models
surf grok "query" --model <model-from-validation>
surf grok "query" --timeout 600
Tab Management
surf tab.list
surf tab.new "https://google.com"
surf tab.switch 12345
surf tab.close 12345
surf tab.move 12345 --to-window 67890
surf tab.reload
surf tab.name myapp
surf tab.switch myapp
surf tab.named
surf tab.unname myapp
surf tab.group
surf tab.ungroup
surf tab.groups
Window Management
surf window.list
surf resize 1280 720
surf resize 1280
surf window.list --tabs
surf window.new
surf window.new --url "https://example.com"
surf window.new --incognito
surf window.new --unfocused
surf window.focus 12345
surf window.close 12345
surf window.resize --id 123 --width 1920 --height 1080
surf window.resize --id 123 --state maximized
Concurrent agent sessions:
export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
surf session.ensure "$SURF_SESSION" about:blank
surf --session research go "https://example.com"
surf --session research read
surf session.list --refresh
surf session.info research --refresh
Each session owns one explicit tab and defaults to a separate unfocused window. Commands for the same tab are FIFO; different session tabs may run concurrently. Browser-wide writers wait for tab lanes to drain. --no-wait returns tab_busy or browser_busy immediately. On tab_gone or session_epoch_stale, run the exact command printed after Recovery:—normally surf session.reopen <name>.
Browser-login provider commands (chatgpt, gemini, perplexity, grok, kimi, aistudio, and oracle ask) take exclusive browser access and print a warning before dispatch. Do not assume Surf is hung while that warning is visible; inspect surf session.info <name> from another shell to see the active writer.