surf
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
SKILL.mdengineering/surf/SKILL.md
GitHubSurf Browser Automation
Control Chrome browser via CLI or Unix socket. Surf drives the user's real Chrome — sessions share the profile's cookies, logins, storage, downloads, history, and bookmarks.
Reference files — read when the branch applies
- Command fails with
Socket connect failedor native-messaging errors, or fresh install/WSL2 setup → read references/troubleshooting.md before retrying. - Controlling a browser on another machine (credentials, Tailnet, transfer limits) → read references/remote.md.
- Querying AI assistants through the browser (ChatGPT, Oracle jobs, Gemini, Perplexity, Grok, AI Studio) → read references/ai-assistants.md first.
- Device/network emulation, network capture & HAR export, console/perf tracing, cookies, history, smoke tests, named workflows & playbooks, socket API → read references/advanced.md.
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.
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>. Use separate browser/profile instances and SURF_SOCKET values only when hard isolation is required.
# Explicit form when an environment variable is inconvenient
surf --session research go "https://example.com"
surf --session research read
# Inspect bindings and scheduler state
surf session.list --refresh
surf session.info research --refresh
CLI Quick Reference
surf --help # Basic help
surf <group> # Group help (tab, scroll, page, wait, dialog, emulate, form, perf, ai)
surf --help-full # All commands
surf --find <term> # Search tools
surf --help-topic <topic> # Topic guide (refs, semantic, frames, devices, windows)
Core Workflow
# 1. Navigate to page
surf navigate "https://example.com"
# 2. Read page to get element refs
surf page.read
# 3. Click by ref or coordinates
surf click --ref "e1"
surf click --x 100 --y 200
# 4. Type text
surf type --text "hello"
# 5. Full-page screenshot
surf screenshot --full-page --output /tmp/shot.png
# Inspect animation/style changes as JSON
surf animate-audit --selector ".thing" --duration 2000 --fps 10
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 # Reload current tab
# Named tabs (aliases)
surf tab.name myapp # Name current tab
surf tab.switch myapp # Switch by name
surf tab.named # List named tabs
surf tab.unname myapp # Remove name
# Tab groups
surf tab.group # Create/add to tab group
surf tab.ungroup # Remove from group
surf tab.groups # List all tab groups
Window Management
surf window.list # List all windows
surf resize 1280 720 # Resize current browser window
surf resize 1280 # Set current window width only
surf window.list --tabs # Include tab details
surf window.new # New window
surf window.new --url "https://example.com" # New window with URL
surf window.new --incognito # New incognito window
surf window.new --unfocused # Don't focus new window
surf window.focus 12345 # Focus window by ID
surf window.close 12345 # Close window
surf window.resize --id 123 --width 1920 --height 1080
surf window.resize --id 123 --state maximized # States: normal, minimized, maximized, fullscreen
Input Methods
# CDP method (real events) types at the current focus
surf type --text "hello"
surf click --x 100 --y 200
# Selector/ref targets use frame-aware DOM input
surf type "hello" --into "#input"
surf type "hello" --ref e5
# Keys
surf key Enter
surf key "cmd+a"
surf key.repeat --key Tab --count 5 # Repeat key presses
# Hover and drag
surf hover --ref e5
surf drag --from-x 100 --from-y 100 --to-x 200 --to-y 200
Page Inspection
surf page.read # Accessibility tree with refs + page text
surf page.read --no-text # Interactive elements only (no text content)
surf page.read --ref e5 # Get specific element details
surf page.read --depth 3 # Limit tree depth
surf page.read --compact # Minimal output for LLM efficiency
surf page.read --max-bytes 2000 # Cap visible text at a UTF-8 byte boundary
surf page.text # Plain text content only
surf page.html --strip-scripts # Rendered HTML without scripts
surf page.save --selector "#artifact" --strip-scripts --output page.html # Save one static element
surf page.state # Modals, loading state, scroll info
surf animate-audit --selector ".thing" --duration 2000 --fps 10 # JSON animation timeline
Export Rendered HTML
Use page.html when the user wants a static copy of the current rendered DOM (works for Claude artifact pages and ordinary web pages):
surf page.save --output page.html # Save the active page as HTML
surf wait.dom --stable 500
surf page.html --selector "#artifact" --strip-scripts > artifact.html
--selector <css> exports its matching element only (a miss fails with an error). --strip-scripts removes scripts from exported markup without changing the page. Without --selector, page.html exports the whole document with its doctype, and it exports the selected frame when frame.switch is active. Use page.read first when you need refs or visible text.
Semantic Element Location
Find and act on elements by role, text, or label instead of refs:
surf locate.role button --name "Submit" --action click
surf locate.role textbox --name "Email" --action fill --value "test@example.com"
surf locate.role link --all # Return all matches
surf locate.text "Sign In" --action click
surf locate.text "Accept" --exact --action click
surf locate.label "Username" --action fill --value "john"
Actions: click, fill, hover, text (get text content)
Text Search
surf search "login" # Find text in page
surf search "Error" --case-sensitive # Case-sensitive
surf search "button" --limit 5 # Limit results
surf find "login" # Alias for search
Element Inspection
surf element.styles e5 # Get computed styles by ref
surf element.styles ".card" # Or by CSS selector
# Returns: font, color, background, border, padding, bounding box
Scrolling
surf scroll down 800 # Scroll down 800px
surf scroll up 400 # Scroll up 400px
surf scroll bottom # Scroll to bottom (dot form scroll.bottom also works)
surf scroll top # Scroll to top
surf scroll.to --ref e5 # Scroll element into view
surf scroll.info # Get scroll position
Waiting
surf wait 2 # Wait 2 seconds
surf wait.element ".loaded" # Wait for element
surf wait.network # Wait for network idle
surf wait.url "/success" # Wait for URL pattern
surf wait.dom --stable 100 # Wait for DOM stability
surf wait.load # Wait for page load complete
Dialog Handling
surf dialog.info # Get current dialog type/message
surf dialog.accept # Accept (OK)
surf dialog.accept --text "response" # Accept prompt with text
surf dialog.dismiss # Dismiss (Cancel)
Form Automation
surf page.read # Get element refs first
# Fill by ref
surf form.fill --data '[{"ref":"e1","value":"John"},{"ref":"e2","value":"john@example.com"}]'
# Checkboxes: true/false
surf form.fill --data '[{"ref":"e7","value":true}]'
# Dropdown selection
surf select e5 "Option A" # By value (default)
surf select e5 "Option A" "Option B" # Multi-select
surf select e5 --by label "Display Text" # By visible label
surf select e5 --by index 2 # By index (0-based)
File Upload
surf upload --ref e5 --files "/path/to/file.txt"
surf upload --ref e5 --files "/path/file1.txt,/path/file2.txt"
Iframe Handling
surf frame.list # List frames with IDs
surf frame.switch --selector "#payment-iframe"
surf frame.switch --name "checkout"
surf frame.switch --index 0 # First iframe
surf frame.main # Return to main frame
surf frame.js "return document.title" --id "FRAME_ID"
# After frame.switch, subsequent commands target that frame:
surf frame.switch --selector "#payment-iframe"
surf page.read # Reads iframe content
surf click --selector "#pay" # Clicks in iframe
surf frame.main # Back to main page
JavaScript Execution
surf js "return document.title"
surf js "document.querySelector('.btn').click()"
Screenshots
surf screenshot # Auto-saves to /tmp/surf-snap-*.png
surf screenshot --output /tmp/shot.png # Save to specific file
surf screenshot --selector ".card" # Element only
surf screenshot --full-page # Full page scroll capture
surf screenshot --full-page /tmp/full.png # Full page saved to path
surf screenshot --no-save # Return base64 only, don't save file
Multi-Step: surf do
Execute multi-step automation as one command with smart auto-waits — instead of 6-8 separate CLI calls with LLM orchestration between each, a workflow executes deterministically. Faster, cheaper, more reliable.
surf do 'go "https://example.com" | click e5 | screenshot'
surf do 'go "https://example.com/login" | type "user@example.com" --selector "#email" | type "pass" --selector "#password" | click --selector "button[type=submit]"'
surf batch --actions '[{"type":"frame.switch","index":0},{"type":"click","selector":"#pay"}]'
surf do 'go "url" | click e5' --dry-run # Validate without executing
For reusable named workflows (JSON files with args, loops, step outputs) and site playbooks, read references/advanced.md.
Error Diagnostics
# Auto-capture screenshot + console on failure
surf wait.element ".missing" --auto-capture --timeout 2000
# Saves to /tmp/surf-error-*.png
Common Options
--session <name> # Target a durable named session (or set SURF_SESSION)
--tab-id <id> # Target a specific tab
--window-id <id> # Target a specific window
--no-wait # Return tab_busy/browser_busy instead of queueing
--json # Raw JSON including target metadata
--auto-capture # Screenshot + console on error
--timeout <ms> # Override default timeout
Tips
- First CDP operation is slow (~5-8s) - debugger attachment overhead, subsequent calls fast
- Use refs from page.read for reliable element targeting over CSS selectors
- JS method for contenteditable - Modern editors (ChatGPT, Claude, Notion) need
--method js - Named tabs for workflows -
tab.name appthentab.switch app - Auto-capture for debugging -
--auto-capturesaves diagnostics on failure - Use
surf dofor multi-step tasks - Reduces token overhead and improves reliability;--dry-runvalidates first - Session first - Set a unique
SURF_SESSIONand runsession.ensurebefore the first browser command in every independent agent shell - Queue diagnostics -
session.infodistinguishes the session's own tab queue, other active tabs, and browser-wide writers; use--no-waitfor immediate busy errors - Native host diagnostics - On socket/native-host errors, read references/troubleshooting.md and run
surf doctorbefore guessing at reinstall steps - HTML export - Use
surf page.html > artifact.htmlto save Claude artifacts or any rendered page as static HTML - Animation capture - Use
surf record --duration 2000 --fps 10 --output /tmp/anim.gifwhen the agent needs to see motion; useanimate-auditfor numeric timelines andperf-auditfor jank/layout-shift snapshots - Semantic locators -
locate.role,locate.text,locate.labelfor more robust element finding - Frame context - Use
frame.switchbefore interacting with iframe content