Browser automation for AI agents. Gives Claude Code (and other AI coding tools) autonomous access to a real Firefox browser so they can verify their own work, debug UI issues, and test web apps without asking you what's on screen.
You're building a web app with an AI agent. Instead of you being the middleman ("what does the page look like?", "is the button there?", "any errors?"), the agent launches Firefox, navigates to your app, reads the page via ARIA snapshots, clicks buttons, fills forms, checks for console errors and failed network requests, takes screenshots, and verifies everything works. Autonomously.
You: "build a signup page and make sure it works"
Agent: *writes the code*
Agent: firecode browse app navigate "http://localhost:3000/signup"
Agent: firecode snapshot app
→ sees textbox "Email" [ref=e3], button "Sign Up" [ref=e5]
Agent: firecode browse app fill e3 "test@example.com"
Agent: firecode browse app click e5
Agent: firecode snapshot app
→ sees "Welcome! Check your email."
Agent: firecode console app
→ No console errors
Agent: firecode network app
→ No failed requests
Agent: "signup page is working, no errors"
Prerequisites: Node.js 20+ and pnpm (npm install -g pnpm).
git clone https://github.com/samoht9277/firecode.git
cd firecode
./install.shgit clone https://github.com/samoht9277/firecode.git
cd firecode
./install.ps1The installer builds the CLI, downloads the Playwright Firefox browser, puts firecode on your PATH, and installs the Claude Code skill. Pass --no-skill (bash) or -NoSkill (PowerShell) to skip the skill.
If
firecodeisn't found after install, open a new terminal (the installer added a directory to your PATH). On macOS/Linux that's~/.local/bin— make sure it's on your PATH.
Using it with Claude Code: the installer symlinks the skill into ~/.claude/skills/firecode. In any Claude Code session, just ask Claude to verify something in the browser (e.g. "open localhost:3000 and check the signup flow works") and it'll use firecode automatically.
If you'd rather not run the script:
pnpm install
pnpm build
pnpm --filter @firecode/server exec playwright install firefox
# put packages/cli/dist/index.js on your PATH as `firecode`, e.g.:
ln -sf "$(pwd)/packages/cli/dist/index.js" ~/.local/bin/firecode
# (optional) install the Claude Code skill:
ln -sfn "$(pwd)/skills/firecode" ~/.claude/skills/firecode./uninstall.sh # macOS / Linux
./uninstall.ps1 # Windows# Start Firefox (auto-starts in headless mode if you skip this)
firecode start
# Navigate to your app
firecode browse main navigate "http://localhost:3000"
# See the page structure
firecode snapshot main
# Interact using ref IDs from the snapshot
firecode browse main click e4
firecode browse main fill e5 "hello@example.com"
# Check for problems
firecode console main # JS errors, warnings
firecode network main # failed HTTP requests
firecode screenshot main # visual check
# Done
firecode stop| Command | Description |
|---|---|
firecode start [--headless] [--extension <xpi>] |
Launch Firefox and start the server |
firecode stop |
Shut down (force kills if unresponsive) |
firecode status |
Show server state and open pages |
firecode wallet import [--seed-file <path>] |
Onboard MetaMask from $FIRECODE_WALLET_SEED (throwaway seeds only) |
firecode wallet unlock |
Unlock MetaMask with $FIRECODE_WALLET_PASSWORD |
firecode wallet approve |
Approve the pending MetaMask popup |
| Command | Description |
|---|---|
browse <page> navigate <url> |
Go to a URL (creates page if needed) |
browse <page> click <ref> [--force] |
Click an element by ref ID |
browse <page> fill <ref> <value> |
Clear and fill a text input |
browse <page> type <ref> <text> |
Type text character by character |
browse <page> select <ref> <value> |
Select a dropdown option |
browse <page> hover <ref> |
Hover over an element |
browse <page> click-text "<text>" |
Click by visible text (no snapshot needed) |
browse <page> keyboard <key> |
Press a key (ArrowRight, Enter, Tab, etc.) |
browse <page> scroll down|up|<ref> |
Scroll the page or to an element |
browse <page> evaluate "<js>" |
Run JavaScript and get the result |
browse <page> viewport mobile|tablet|desktop|<w> <h> |
Set viewport size |
browse <page> reload |
Refresh the page |
browse <page> back / forward |
Browser history navigation |
browse <page> wait <ms> |
Wait for a duration |
browse <page> wait-for "<text>" |
Wait for text to appear |
browse <page> wait-idle |
Wait for network to be idle |
browse <page> assert-text "<text>" |
Check if text exists (fails if not) |
browse <page> upload "<trigger>" <file> |
Upload a file: click the trigger (text/selector) and answer the file dialog |
browse <page> upload "<input-sel>" <file> --input |
Set an <input type=file> directly (searches all frames; - = any) |
| Command | Description |
|---|---|
snapshot <page> |
ARIA accessibility tree with ref IDs |
screenshot <page> [path] |
Capture PNG |
screenshot <page> [path] --diff <baseline> |
Pixel-level visual comparison |
text <page> |
Visible text content (lighter than snapshot) |
console <page> [--clear] |
Browser console logs |
network <page> [--all] [--clear] |
Network requests (failures by default) |
cookies <page> |
Page cookies |
storage <page> [--session] [--clear] |
localStorage/sessionStorage |
pdf <page> [path] |
Export page as PDF (headless only) |
| Command | Description |
|---|---|
record start <page> |
Start recording actions |
record stop <page> |
Stop and show captured steps |
record save <page> <path> |
Save recording to JSON |
replay <page> <path> |
Replay a saved recording |
| Command | Description |
|---|---|
test [--target unstaged|branch|changes] |
Generate and run tests from git diff |
test --base-url <url> |
Set app URL (default: localhost:3000) |
test -y |
Skip plan review |
firecode snapshot returns a YAML-like accessibility tree. Interactive elements get [ref=eN] tags:
- navigation:
- link "Dashboard" [ref=e1]
- link "Settings" [ref=e2]
- main:
- heading "Profile" [ref=e3]
- textbox "Name" [ref=e4]
- button "Save" [ref=e5]
- button [ref=e6]:Use refs to interact: firecode browse main click e5 clicks "Save". Unnamed elements (like buttons with HTML content) also get refs.
Refs are tied to the last snapshot. If the page changes, take a new snapshot.
| Preset | Size |
|---|---|
mobile |
375x812 |
tablet |
768x1024 |
desktop |
1920x1080 |
desktop-hd |
3840x2160 |
Or custom: firecode browse main viewport 1440 900
# Take a baseline
firecode screenshot app /tmp/before.png
# Make changes, then compare
firecode screenshot app /tmp/after.png --diff /tmp/before.png
# → "Changed: 2.3% (1847/80000 pixels differ)"
# → Diff image saved to /tmp/after-diff.pngIf the server isn't running when you run a command, it auto-starts in headless mode. No need to manually firecode start every time.
Load a signed Firefox extension (.xpi) into firecode's browser. The main use case is MetaMask: it injects window.ethereum into every page, so wallet-gated dapps that require a provider to be present (e.g. app.safe.global) will render and let you reach their "Connect wallet" step instead of failing outright.
# Download signed MetaMask from Mozilla Add-ons
curl -L -o metamask.xpi "https://addons.mozilla.org/firefox/downloads/latest/ether-metamask/latest.xpi"
firecode start --extension metamask.xpi
# → Extension loaded: webextension@metamask.io → moz-extension://<uuid>/The extension loads from a persistent profile at ~/.firecode/profile-<instance>, so its state survives restarts. Delete that directory for a factory reset. --extension is repeatable for multiple extensions. The moz-extension://<uuid>/ base URL (also shown in firecode status) is stable across runs.
Verify the provider is present on any dapp:
firecode browse main navigate "https://app.safe.global"
firecode browse main evaluate "return window.ethereum && window.ethereum.isMetaMask" # → truePlaywright's Firefox build cannot navigate a tab to a moz-extension:// URL — page.goto() throws NS_ERROR_NOT_AVAILABLE in both headless and headed mode. Because MetaMask's onboarding, unlock, and confirmation popups all live behind such URLs, firecode cannot script the MetaMask UI itself (importing a seed, unlocking, or approving a connect/signature popup). The extension loading and provider injection above still work; only in-extension UI automation is blocked.
The firecode wallet import|unlock|approve commands exist but will report this limitation when run. Automated wallet-UI flows are a Chromium-only capability today (see Synpress/dappwright, which rely on Chromium's --load-extension); a Chromium backend for firecode would be the path to full support.
Firefox can inject window.ethereum but cannot script the MetaMask UI (Playwright's Firefox can't open moz-extension:// pages). For full wallet automation — importing a seed, unlocking, approving popups — use the Chromium wallet engine:
# One-time: install Chromium for Playwright
pnpm exec playwright install chromium # or: npx playwright install chromium
# MetaMask's Chrome build (GitHub release .zip or an unpacked dir)
firecode start --wallet --extension metamask-chrome-13.x.zip
# → Extension loaded: <id> → chrome-extension://<id>/
# → Engine: chromium
export FIRECODE_WALLET_SEED="test test ... junk" # throwaway/testnet only
export FIRECODE_WALLET_PASSWORD="some-password" # min 8 chars
firecode browse main navigate https://app.safe.global
firecode wallet import # onboards MetaMask from the seed
firecode browse main click-text "Connect"
firecode wallet approve # approves the connect popup--wallet requires --extension and runs a dedicated Chromium instance (headed by default; --headless supported). Firefox stays the default engine for all non-wallet work.
Use throwaway/testnet seeds only if you experiment with these commands. Secrets come from the environment ($FIRECODE_WALLET_SEED, $FIRECODE_WALLET_PASSWORD) or --seed-file, never from arguments.
firecode/
packages/
server/ Playwright Firefox + Fastify HTTP API
cli/ Commander.js CLI
testgen/ Git diff → test generation
skills/
firecode/ Claude Code skill instructions
Single Firefox instance, one OS window with tabs. The CLI talks to the server over HTTP. State file at ~/.firecode/server.json.
- Playwright (Firefox)
- Fastify
- Commander.js
- pixelmatch
- TypeScript, pnpm, turbo
Firefox is the default engine for all browsing, verification, and testing. Chromium is used only for MetaMask wallet automation (start --wallet), because Playwright's Firefox can't script moz-extension:// pages; see "MetaMask wallet automation (Chromium)" above.
Each firecode server is isolated by instance. To run two Claude sessions against firecode on the same machine without collisions, set FIRECODE_INSTANCE per session:
FIRECODE_INSTANCE=projectA firecode browse main navigate "http://localhost:3000"
FIRECODE_INSTANCE=projectB firecode browse main navigate "http://localhost:4000"Each instance gets its own Firefox process, port, auth token, and pages.
MIT — see LICENSE.