Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Control the user's Firefox browser with their logins and cookies intact. Use when you need to browse websites as the user, interact with authenticated pages, fill forms, click buttons, take screenshots, or get page content. (user)
.claude/skills/aiskillstore-firefox-browser/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 254% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 87% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 76% | 0% |
Control the user's actual Firefox browser session via WebSocket. This uses their real browser with existing logins and cookies - not a headless browser.
bash# 0. If Firefox isn't running, start it first nohup firefox &>/dev/null & # 1. Check connection browser ping # 2. See what tabs are open browser listTabs '{}' # 3. Start a new session (recommended) browser newSession '{"url": "https://example.com"}' # 4. Read the page with interactable elements marked browser getContent '{"format": "annotated"}'
bashbrowser <action> '<json_params>'
| Action | Description | Key Params | |--------|-------------|------------| | listTabs | List all open tabs across windows | - | | newSession | Create new tab to work in | url (optional) | | newWindow | Create a new normal window to work in | url (optional), focus (defaults true), sandbox (private window) | | setActiveTab | Switch which tab agent works on | tabId, focus | | getActiveTab | Get current tab info | - |
| Action | Description | Key Params | |--------|-------------|------------| | navigate | Go to URL in current tab | url, wait, newTab | | getContent | Get page content | format: annotated, text, html | | getInteractables | List clickable elements and inputs | selector (optional scope) | | screenshot | Capture visible area as PNG | filename (optional) |
| Action | Description | Key Params | |--------|-------------|------------| | click | Click element | selector, text, or x/y coords | | type | Type into input | selector, text, submit, clear | | fillForm | Fill multiple fields | fields[] with selector/value pairs | | waitFor | Wait for element/text | selector, text, timeout | | scroll | Scroll the page | y/x, selector, position | | evaluate | Execute JavaScript and return result | script |
| Action | Description | Key Params | |--------|-------------|------------| | fork | Duplicate tab into multiple paths | paths[] with name + commands | | killFork | Close a fork | fork (name) | | listForks | List active forks | - | | tryUntil | Try alternatives until one succeeds | alternatives[], timeout | | parallel | Run commands on multiple URLs | branches[] with url + commands |
| Action | Description | Key Params | |--------|-------------|------------| | autoLogin | Auto-fill credentials from Bitwarden vault and optionally submit | domain, submit (default false) | | vaultStatus | Check vault lock state and credential count | - | | vaultSync | Re-sync vault from Bitwarden server via API key | - | | getAuthContext | Detect login pages, available accounts | - | | requestAuth | Request user approval for auth | reason |
The type and fillForm actions automatically handle rich text editors (ProseMirror, Lexical/Reddit, Draft.js, Tiptap, Slate, CKEditor, Quill). They use document.execCommand("insertText") in the page world, which works with any contenteditable-based editor.
bash# Works on any rich text editor — ProseMirror, Lexical, etc. browser type '{"selector": "div[contenteditable=true]", "text": "Hello world!"}' # Response includes richEditor: true when execCommand path was used # Clear existing content and replace browser type '{"selector": ".ProseMirror", "text": "New content", "clear": true}' # Fill multiple rich text fields in a form browser fillForm '{"fields": [ {"selector": "#title", "value": "My Title"}, {"selector": "#body .ProseMirror", "value": "Article body text"} ]}'
No special handling needed — just use type or fillForm as normal. Falls back to textContent assignment if execCommand isn't available.
bashbrowser listTabs '{}'
Returns:
json{ "activeTabId": 123, "windows": [ { "windowId": 1, "focused": true, "tabs": [ {"tabId": 123, "url": "https://...", "title": "...", "active": true} ] } ], "totalTabs": 5 }
bash# Start fresh in a tab browser newSession '{"url": "https://amazon.com"}' # Start fresh in a new normal window browser newWindow '{"url": "https://example.com"}' # Or switch to existing tab browser setActiveTab '{"tabId": 456}'
bashbrowser getContent '{"format": "annotated"}'
Returns content with interactive elements marked inline:
Product Name Here
$4.99
[button: "Add to cart" | selector: #add-btn]
[input:text: "search" | value: "" | selector: #search-box]
[link: "View details" | href: /product/123 | selector: a.details-link]This shows what's clickable and where it is in context.
bash# Click using selector from annotated output browser click '{"selector": "#add-btn"}' # Or by text (prefers visible elements) browser click '{"text": "Add to cart"}' # Type into input browser type '{"selector": "#search-box", "text": "query", "submit": true}'
When you're not sure which path is right, fork the tab and try both:
bash# Create forks browser fork '{ "paths": [ { "name": "google-auth", "commands": [{"action": "click", "params": {"text": "Sign in with Google"}}] }, { "name": "email-auth", "commands": [{"action": "click", "params": {"text": "Sign in with Email"}}] } ] }'
Returns:
json{ "forked": true, "sourceTabId": 123, "forks": [ {"name": "google-auth", "tabId": 456, "url": "...", "commandResults": [...]}, {"name": "email-auth", "tabId": 789, "url": "...", "commandResults": [...]} ] }
Work on specific fork:
bashbrowser getContent '{"format": "annotated", "fork": "google-auth"}' browser click '{"text": "Continue", "fork": "google-auth"}'
Kill the wrong path:
bashbrowser killFork '{"fork": "email-auth"}'
When the exact button varies (cookie banners, A/B tests):
bashbrowser tryUntil '{ "alternatives": [ {"action": "click", "params": {"selector": "#accept-cookies"}}, {"action": "click", "params": {"text": "Accept All"}}, {"action": "click", "params": {"selector": ".cookie-dismiss"}} ], "timeout": 3000 }'
Tries each until one succeeds.
Compare prices across sites:
bashbrowser parallel '{ "branches": [ {"url": "https://amazon.com/product", "commands": [{"action": "getContent", "params": {"format": "text"}}]}, {"url": "https://walmart.com/product", "commands": [{"action": "getContent", "params": {"format": "text"}}]} ] }'
The bridge integrates with a Bitwarden vault (via bronzewarden) for fully autonomous credential fill. No human interaction needed.
bash# 1. Navigate to the site browser navigate '{"url": "https://github.com"}' # 2. Auto-fill credentials (looks up domain in vault, fills form) browser autoLogin '{"domain": "github.com", "submit": false}' # Returns: {"filled": true, "maskedUsername": "j***1", "matchedUri": "https://github.com/"} # 3. Or auto-fill AND submit in one step browser autoLogin '{"domain": "github.com", "submit": true}'
bash# Check vault status browser vaultStatus '{}' # Returns: {"locked": false, "entries": 322} # Re-sync vault from server (if credentials were updated) browser vaultSync '{}' # Returns: {"synced": true, "entries": 322}
autoLogin action sends credentials directly to the extension via the native messaging channel (never over WebSocket)bash# Detect login pages and available accounts browser getAuthContext '{}'
Execute arbitrary JavaScript in the page context and get the result back:
bash# Get page title browser evaluate '{"script": "return document.title"}' # Returns: {"result": "My Page Title", "type": "string"} # Count elements browser evaluate '{"script": "return document.querySelectorAll(\"input\").length"}' # Returns: {"result": 5, "type": "number"} # Get form values browser evaluate '{"script": "return document.querySelector(\"#email\").value"}' # Returns: {"result": "user@example.com", "type": "string"} # Complex queries browser evaluate '{"script": "return Array.from(document.querySelectorAll(\"input:checked\")).map(el => el.value)"}' # Returns: {"result": ["option1", "option3"], "type": "object"}
By default, evaluate runs in the content script's isolated world. To access page-level JavaScript variables (e.g., framework state, global objects set by the page), use pageWorld: true:
bash# Access page-level globals (React state, editor instances, app data) browser evaluate '{"script": "return window.__NEXT_DATA__", "pageWorld": true}' # Interact with ProseMirror/Lexical internals browser evaluate '{"script": "return Object.keys(window.__prosemirrorViews || {})", "pageWorld": true}' # Call page-level functions browser evaluate '{"script": "return window.myApp.getState()", "pageWorld": true}'
Note: Use return to get a value back. The script runs with full DOM access. Use pageWorld: true when you need to access variables set by the page's own JavaScript.
Scroll the page by pixels, to elements, or to positions:
bash# Scroll down 500 pixels browser scroll '{"y": 500}' # Scroll up 300 pixels browser scroll '{"y": -300}' # Scroll element into view browser scroll '{"selector": "#section-5"}' # Scroll to top/bottom browser scroll '{"position": "top"}' browser scroll '{"position": "bottom"}' # Smooth scrolling browser scroll '{"y": 500, "behavior": "smooth"}' # Scroll to absolute position browser scroll '{"scrollTo": {"x": 0, "y": 1000}}'
The getContent annotated format now shows form element states:
bashbrowser getContent '{"format": "annotated"}'
Output includes checked/selected states:
[input:radio: "Option A" | checked: true | selector: #opt-a]
[input:radio: "Option B" | checked: false | selector: #opt-b]
[input:checkbox: "Remember me" | checked: true | selector: #remember]
[select: "Country" | selected: "United States" | selector: #country]
[input:text: "Email" | value: "user@example.com" | selector: #email]This is useful for verifying form state without screenshots.
When running multiple tasks in parallel, use tabId to avoid conflicts:
bash# 1. Create isolated session - get a unique tabId browser newSession '{"url": "https://example.com"}' # Returns: {"tabId": 15, "url": "...", "windowId": 1} # 2. Use that tabId in ALL subsequent commands browser navigate '{"url": "https://example.com/page", "tabId": 15}' browser getContent '{"format": "annotated", "tabId": 15}' browser click '{"selector": "#btn", "tabId": 15}' browser type '{"selector": "#input", "text": "hello", "tabId": 15}'
This lets multiple agents work in parallel without stepping on each other.
listTabs to see what's opennewSession for a clean new tab or newWindow for a separate windowtabId for parallel/isolated executionannotated format - shows content + clickable elements togethernohup firefox &>/dev/null &browser pingabout:debuggingbrowser getContent '{"format": "annotated"}' to see what's on the page| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | fail→pass | 15,514 | 9,042 | -42% | 1 | 1 | 0% | 2,637 | 5,319 | +102% | 0 | 0 | — |
case-21 | pass→pass | 9,069 | 10,186 | +12% | 1 | 1 | 0% | 1,534 | 4,778 | +211% | 0 | 0 | — |
case-22 | pass→pass | 11,310 | 5,206 | -54% | 1 | 1 | 0% | 1,867 | 4,455 | +139% | 0 | 0 | — |
case-01 | fail→pass | 8,885 | 11,327 | +27% | 1 | 1 | 0% | 1,489 | 5,265 | +254% | 0 | 0 | — |
case-02 | fail→fail | 7,532 | 7,282 | -3% | 1 | 1 | 0% | 1,463 | 3,749 | +156% | 0 | 0 | — |
case-03 | fail→fail | 9,772 | 9,240 | -5% | 1 | 1 | 0% | 1,750 | 3,883 | +122% | 0 | 0 | — |
case-04 | fail→pass | 13,197 | 4,300 | -67% | 1 | 1 | 0% | 2,578 | 4,261 | +65% | 0 | 0 | — |
case-06 | fail→pass | 13,252 | 2,944 | -78% | 1 | 1 | 0% | 2,137 | 3,993 | +87% | 0 | 0 | — |
case-07 | fail→pass | 13,934 | 1,883 | -86% | 1 | 1 | 0% | 2,148 | 3,778 | +76% | 0 | 0 | — |
case-08 | fail→pass | 12,923 | 3,019 | -77% | 1 | 1 | 0% | 1,993 | 4,031 | +102% | 0 | 0 | — |
case-09 | fail→pass | 11,092 | 3,329 | -70% | 1 | 1 | 0% | 2,103 | 4,123 | +96% | 0 | 0 | — |
case-10 | fail→pass | 11,006 | 4,617 | -58% | 1 | 1 | 0% | 1,749 | 3,806 | +118% | 0 | 0 | — |
case-11 | fail→pass | 7,205 | 3,679 | -49% | 1 | 1 | 0% | 1,229 | 4,044 | +229% | 0 | 0 | — |
case-12 | fail→pass | 14,264 | 2,757 | -81% | 1 | 1 | 0% | 2,657 | 4,025 | +51% | 0 | 0 | — |
case-13 | fail→pass | 12,011 | 2,711 | -77% | 1 | 1 | 0% | 1,862 | 4,019 | +116% | 0 | 0 | — |
case-14 | fail→pass | 10,494 | 3,086 | -71% | 1 | 1 | 0% | 1,580 | 4,058 | +157% | 0 | 0 | — |
case-15 | fail→pass | 9,422 | 1,851 | -80% | 1 | 1 | 0% | 1,602 | 3,809 | +138% | 0 | 0 | — |
case-16 | pass→pass | 10,691 | 2,257 | -79% | 1 | 1 | 0% | 1,658 | 3,774 | +128% | 0 | 0 | — |
case-17 | fail→pass | 7,622 | 2,219 | -71% | 1 | 1 | 0% | 1,240 | 3,766 | +204% | 0 | 0 | — |
case-18 | fail→pass | 7,481 | 1,916 | -74% | 1 | 1 | 0% | 1,118 | 3,830 | +243% | 0 | 0 | — |
case-19 | fail→pass | 3,524 | 3,928 | +11% | 1 | 1 | 0% | 402 | 3,870 | +863% | 0 | 0 | — |
case-20 | pass→pass | 6,901 | 4,921 | -29% | 1 | 1 | 0% | 1,181 | 4,355 | +269% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +73 percentage points is the difference between those two pass rates over the 20 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.