Control browsers via the ThinkBrowse CLI — navigate pages, interact with elements, extract content, take screenshots. Use when the user asks to browse, scrape, test, audit, or automate anything in a browser using CLI commands. Do NOT use for simple URL fetching (use WebFetch tool instead).
Control real browsers from Claude Code using the thinkbrowse CLI. Two modes:
# Local mode — list tabs, attach, navigate, observe
thinkbrowse tabs
thinkbrowse attach <tabId>
thinkbrowse navigate "https://example.com"
thinkbrowse snapshot # accessibility tree (best for AI)
thinkbrowse screenshot --output /tmp/page.png
# Cloud mode — create session, navigate, observe, clean up
thinkbrowse cloud start "https://example.com"
thinkbrowse snapshot
thinkbrowse screenshot --output /tmp/page.png
thinkbrowse cloud stop --all
| Signal | Mode |
|---|---|
| Bridge running + extension connected |
| Local |
THINKBROWSE_API_KEY set | Cloud |
| Neither | Error |
Priority: --tab flag > THINKBROWSE_TAB_ID env > working-location.json > API key (cloud).
thinkbrowse navigate "https://example.com"
sleep 2 # Wait for page load + CSRF tokens
thinkbrowse snapshot # Read structure before interacting
thinkbrowse screenshot --output /tmp/page.png
# Then use Read tool on /tmp/page.png to view it
Direct CSS selectors often fail when elements lack unique selectors. Use evaluate:
thinkbrowse evaluate "[...document.querySelectorAll('button,a,[role=button]')].find(el=>el.textContent.trim()==='Sign In')?.click()"
sleep 1
# React inputs need keyboard events — use `type`, NOT `fill`
thinkbrowse click "input[type=email]"
thinkbrowse type "input[type=email]" "[email protected]"
thinkbrowse click "input[type=password]"
thinkbrowse type "input[type=password]" "password123"
thinkbrowse click "button[type=submit]"
sleep 3
thinkbrowse evaluate "[...document.querySelectorAll('button,[role=tab],[role=button]')].map(b=>({label:b.getAttribute('aria-label'),text:b.textContent.trim(),class:b.className.slice(0,50)})).filter(b=>b.text||b.label)"
thinkbrowse navigate "https://app.example.com"
thinkbrowse wait "button[type=submit]" --timeout 10000
thinkbrowse click "button[type=submit]"
For all 10+ patterns, see references/patterns.md.
# BAD — "Timeline & Logs" is not a CSS selector
thinkbrowse click "Timeline & Logs" # ELEMENT_NOT_FOUND
# GOOD — find by text via evaluate
thinkbrowse evaluate "[...document.querySelectorAll('button')].find(b=>b.textContent.includes('Timeline'))?.click()"
fill on React inputs# BAD — fill uses native DOM, React won't detect the change
thinkbrowse fill "#search" "query text"
# GOOD — type uses keyboard events, triggers React onChange
thinkbrowse type "#search" "query text"
# BAD — screenshot fails with "fetch failed"
thinkbrowse attach <tabId>
thinkbrowse screenshot --output /tmp/page.png # ERROR
# GOOD — wait for connection
thinkbrowse attach <tabId>
sleep 2
thinkbrowse screenshot --output /tmp/page.png
# BAD — extract runs before new page loads
thinkbrowse click "a[href='/next-page']"
thinkbrowse extract ".content" # OLD page content!
# GOOD — wait for new page element
thinkbrowse click "a[href='/next-page']"
thinkbrowse wait "h1" --timeout 5000
thinkbrowse extract ".content"
# ALWAYS release/stop when done
thinkbrowse release # local mode
thinkbrowse cloud stop --all # cloud mode
All commands return JSON:
{"success": true, "command": "navigate", "durationMs": 1234, "data": {...}}
{"success": false, "error": "...", "code": "ELEMENT_NOT_FOUND", "hint": "...", "retryable": true}
Key gotchas:
thinkbrowse url returns {"data": "https://..."} — plain string, NOT nested objectthinkbrowse screenshot --output /tmp/x.png returns {"data": {"path": "/tmp/x.png"}} — use Read tool on that path to view itevaluate auto-wraps single expressions; multi-statement needs IIFE: (()=>{const x=5; return x*2})()evaluate does NOT await Promises — async operations return {}{"blocked": true, "handoffRequired": true} — stop automation| Action | Wait After |
|---|---|
navigate | sleep 2 (page load + CSRF) |
click | sleep 1 (state change) |
type / fill | No wait needed |
attach (new tab) | sleep 2 (connection) |
evaluate (click) | sleep 1 |
scroll | sleep 1 (rendering) |
Use --tab <tabId> on every command to prevent tab contention:
thinkbrowse navigate "https://site-a.com" --tab 12345
thinkbrowse navigate "https://site-b.com" --tab 67890
Or: export THINKBROWSE_TAB_ID=12345
thinkbrowse doctor # Check bridge, extension, config
thinkbrowse config show # Show current config
thinkbrowse config set apiKey <key> # Set API key for cloud mode
thinkbrowse agent-init [--name <name>] # Set agent identity
references/command-reference.mdreferences/patterns.mdreferences/error-recovery.md