General-purpose Chrome automation from the terminal. Control any tab, fill any form, read any page.
Claude Code → chrome.sh (CLI) → HTTP daemon (:7866) → WebSocket → Chrome MV3 Extension → Any webpage
| Layer | File | Role |
|---|---|---|
| CLI | scripts/chrome.sh |
Bash wrapper — parses args, curls daemon |
| Daemon | daemon/server.py |
Starlette HTTP server, WebSocket hub, request correlation |
| Extension | extension/ |
MV3 service worker — executes JS, captures screenshots, manages tabs |
| JS Helpers | lib/ |
Injected on-demand — cursor, forms, extract, wait |
# Option A: launchd (auto-start on login)
cp com.agent-chrome.daemon.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.agent-chrome.daemon.plist
# Option B: manual
uv run --python 3.12 --with starlette --with uvicorn --with websockets python3 daemon/server.py- Open
chrome://extensions/ - Enable Developer mode
- Click Load unpacked → select the
extension/directory inside your skill install path - Verify connection:
scripts/chrome.sh health
scripts/chrome.sh health # Should show extension: connected
scripts/chrome.sh tabs # Lists open tabs
scripts/chrome.sh exec "document.title" # Returns active tab's title# Navigation
chrome.sh navigate "https://example.com"
chrome.sh back
chrome.sh forward
chrome.sh scroll --to bottom
# Reading
chrome.sh read --mode structured # JSON: title, headings, tables, links
chrome.sh read --mode md # Markdown conversion
chrome.sh read --selector "h1" # Specific element
# Interaction
chrome.sh click "button.submit" # Click element
chrome.sh click "button.submit" --human # Animated cursor + click
chrome.sh type "#email" "user@test.com" # Type into field
chrome.sh forms # Detect all form fields
chrome.sh fill '{"fields":{"#email":"x@y.com","#name":"Test User"}}'
# Capture
chrome.sh screenshot --save /tmp/page.png
chrome.sh pdf --save /tmp/page.pdf
# Tabs
chrome.sh tabs # List all
chrome.sh tab:open "https://example.com" # Open new
chrome.sh tab:focus 12345 # Switch to tab
chrome.sh tab:close 12345 # Close tab
# Waiting
chrome.sh wait ".results" --timeout 10 # Wait for elementagent-chrome/
├── SKILL.md # Claude Code skill interface
├── README.md # This file
├── .env.example # CHROME_PORT=7866
├── daemon/
│ └── server.py # HTTP + WebSocket daemon (PEP 723 inline deps)
├── scripts/
│ └── chrome.sh # Bash CLI wrapper
├── extension/
│ ├── manifest.json # MV3 manifest
│ ├── background.js # Service worker
│ └── icons/ # Extension icons
├── lib/
│ ├── cursor.js # Human-like cursor animation (Bezier paths)
│ ├── forms.js # Form detection + React-compatible filling
│ ├── extract.js # Structured data extraction + markdown
│ └── wait.js # Element/condition waiting (MutationObserver)
├── com.agent-chrome.daemon.plist # launchd auto-start (edit paths before use)
└── dashboard/
└── index.html # Status page at http://127.0.0.1:7866
- HTTP daemon + CLI, not MCP — matches the speak skill pattern. Simpler, more debuggable, works with any shell.
- WebSocket to extension — bidirectional push needed. Keeps service worker alive.
world: "MAIN"for JS execution — access page JS, React state, framework APIs directly.- Async-aware execute — extension wraps code in
async () => {}and tries expression-first, then statement fallback. Supportsawaitin injected lib code (cursor, wait, etc.). - On-demand lib injection — cursor/forms/extract/wait only loaded when their endpoints are called. Keeps payloads small.
- Dual API surface — libs export both namespaced (
window.__claude_chrome_cursor.click()) and shorthand (window.__cursor_click()) APIs. Namespaced for directexecutecalls; shorthand for daemon injection compatibility. - No hardcoded selectors — everything parameterized via CSS selectors and JS expressions. Works on any website.
Dashboard: http://127.0.0.1:7866
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Status + extension connection state |
/events |
GET | SSE stream for dashboard |
/execute |
POST | Run arbitrary JS in a tab |
/navigate |
POST | Go to URL |
/click |
POST | Click element (optional human: true) |
/type |
POST | Type text (optional human: true) |
/read |
POST | Read page — modes: text, html, md, structured, selector |
/read/forms |
POST | Detect all forms + fields |
/fill |
POST | Fill form fields (React-compatible) |
/submit |
POST | Submit form |
/extract |
POST | Extract tables, links, images, headings |
/screenshot |
POST | Capture visible tab as PNG |
/pdf |
POST | Generate PDF via debugger protocol |
/tabs |
GET | List open tabs |
/tabs/open |
POST | Open new tab |
/tabs/close |
POST | Close tab |
/tabs/focus |
POST | Switch to tab |
/tabs/reload |
POST | Reload tab |
/wait |
POST | Wait for selector/condition/navigation |
/cookies |
GET | Read cookies for domain |
/storage |
GET | Read localStorage/sessionStorage |
/back |
POST | History back |
/forward |
POST | History forward |
/scroll |
POST | Scroll page |
All POST endpoints accept optional tab or tabId param (Chrome tab ID integer). Defaults to active tab.
Daemon → Extension: {"id": "req_xxx", "action": "execute"|"screenshot"|..., "params": {...}}
Extension → Daemon: {"id": "req_xxx", "ok": true|false, "result": ..., "elapsed": 42}
Extension push events: {"event": "tab_updated"|"tab_removed"|"tab_created"|"connected", "data": {...}}
- Localhost only — daemon binds to 127.0.0.1, origin guard rejects non-local POST requests.
- Save path validation —
/screenshotand/pdfsaveparam restricted to$HOMEor/tmp. - No remote code — all JS executed locally via
chrome.scripting.executeScriptin MAIN world.
| Symptom | Fix |
|---|---|
health shows extension disconnected |
Check chrome://extensions — is it enabled? Reload it. |
| Daemon not running | launchctl load ~/Library/LaunchAgents/com.agent-chrome.daemon.plist |
| JS execution fails on chrome:// pages | Chrome blocks script injection on internal pages. Use regular web pages. |
| Form fill doesn't trigger React state | Should work — uses native property descriptor + event dispatch. File a bug if not. |
| Screenshot is blank | Tab must be visible (not minimized). Use tab:focus first. |