--- name: reference_ff_firefox_driver description: Drive Firefox via Playwright (.claude/scripts/ff.py) — Mike's preferred browser; replaces the disliked claude-in-chrome extension metadata: type: reference --- `.claude/scripts/ff.py` drives **Firefox** over Playwright — the Firefox sibling of [[reference_cdp_chrome_driver]]. Mike dislikes Chrome and the `claude-in-chrome` MCP extension, so when he asks to "look at a website / interact / collect the logs", use this, not Chrome. (The Chrome connector was disabled 2026-06-06: keys `claudeInChromeDefaultEnabled`, `cachedChromeExtensionInstalled` set false and `chromeExtension` pairing removed in `~/.claude.json`; backup at `~/.claude.json.bak-prechrome`. Re-toggle in the connectors UI if it reappears.) **Why a daemon, not stateless like cdp.py:** Firefox dropped most CDP support, so cdp.py's "new WS per command" trick doesn't port. `ff.py launch` spawns a background daemon holding ONE Playwright Firefox page on a **persistent profile** (`~/.claude/ff-profile`, logins survive); every other subcommand is a thin HTTP client to it on `localhost:9333` (env `FF_PORT`). The page persists between calls (nav now, shot later) and the daemon accumulates console + network logs. **Commands:** `launch [url] [--headless]` · `status` · `nav ` · `shot ` (real PNG to disk → feed to `agy image-analyze`/Grok) · `click ` · `type ` · `key ` · `eval ` · `console [--clear]` · `network [--clear]` · `stop`. Default headed (visible) so Mike can log into authenticated apps once; Claude still must NOT type passwords. **Gotchas (both bit during build, 2026-06-06):** - **`py` honors a script's shebang.** ff.py's `#!/usr/bin/env python` makes `py ff.py` resolve `python` via PATH → **Python 3.12**, while bare `py -c` uses the default **3.14**. Playwright is installed in BOTH now (`\python.exe -m pip install playwright` + `... -m playwright install firefox`), so it's interpreter-agnostic. If `ModuleNotFoundError: playwright` recurs after a Python upgrade, install playwright into whatever `py .claude/scripts/ff.py status` actually runs. - The detached daemon's stdio is redirected to `~/.claude/ff-daemon.log` (NOT inherited) — otherwise `launch` never returns control and startup crashes are invisible. Check that log if `launch` hangs. Verified end-to-end 2026-06-06: launch→status→eval→shot (26KB real render of example.com)→network (200 captured)→console (caught an injected log). See [[reference_cdp_chrome_driver]].