Skip to content

URL: https://mkdocs.justinsforge.com/memory/general/reference_browser_stream/

Forge Browser Stream

Headed real Google Chrome on a virtual X display on Console, exposed as a web app via noVNC + Cloudflare tunnel + Cloudflare Access. Runs Playwright-driven or hand-driven. Watchable from Sol, Vector, Astra, anywhere.

Built 2026-05-01.

Architecture

Console (192.168.86.50)
  ├── Xvfb :99               (1920x1080x24 virtual display)
  ├── fluxbox                (window manager on :99)
  ├── google-chrome 147      (real Chrome from Google's apt repo, NOT snap)
  ├── x11vnc :5900           (VNC server, localhost-only, password-protected)
  ├── websockify :6080       (noVNC web frontend, bound to LAN IP)
  └── cloudflared (Finn)     (media-server tunnel routes browser.justinsforge.com -> 192.168.86.50:6080)
       └── Cloudflare Access (Google login, locked to [email protected] + [email protected])

Public URL: https://browser.justinsforge.com/

Key paths

Thing Path
CLI forge/scripts/forge_browser.py
Skill forge/.claude/skills/browser/SKILL.md (/browser slash command)
Driver script (legacy, kept for direct calls) forge/scripts/forge_browser_drive.py
Playwright venv ~/.forge-venvs/browser/bin/python (has both playwright and patchright)
Chrome profiles per display ~/.forge-browser-profiles/d<N>/ (cookies persist across runs)
VNC password ~/.forge-secrets/forge-browser.env (FORGE_BROWSER_VNC_PASSWORD)
VNC password file (x11vnc format) ~/.x11vnc/passwd
systemd units (primary) /etc/systemd/system/forge-browser-{xvfb,fluxbox,x11vnc,novnc}.service, forge-browser-stream.target
systemd units (templated, parallel) same names with @.service / @.target

Quick commands

Want Run
Open URL on primary stream /browser open https://example.com
Get the VNC password /browser pwd
List active displays /browser list
Spin up a parallel display /browser spawn 100 then /browser add-tunnel 100
Tear down a parallel display /browser kill 100
Direct Playwright run on the streamed browser DISPLAY=:99 ~/.forge-venvs/browser/bin/python script.py

Patchright (preferred over vanilla Playwright)

Vanilla Playwright leaks CDP Runtime.enable ID — Cloudflare/DataDome catch it. patchright is a drop-in replacement that fixes this. Identical API.

# OLD: from playwright.sync_api import sync_playwright
from patchright.sync_api import sync_playwright   # use this

Both are installed in ~/.forge-venvs/browser/. Use patchright by default unless a script needs a vanilla-Playwright-only feature.

Parallel sessions

Use [email protected] (templated). Display :N gets: - VNC port 5900 + (N - 99) - noVNC port 6080 + (N - 99) - Tunnel hostname browser{N-98}.justinsforge.com (e.g. :100 -> browser2.justinsforge.com)

/browser spawn 100         # start display :100
/browser add-tunnel 100    # add browser2.justinsforge.com -> 192.168.86.50:6081
/browser open https://shopify.com --display 100

Each display has its own Chrome profile at ~/.forge-browser-profiles/d<N>/ so logins don't collide.

Capacity (Console = 4 vCPU / 11GB RAM): comfortably 5-8 parallel headed-streamed sessions, 8-12 parallel headless. RAM is the bottleneck.

When to use vs. other tools

Need Tool
One-shot screenshot /screenshot
Drive Justin's actual local Chrome on Sol/Vector claude-in-chrome MCP
Bulk stock image download forge_assets_grab
Pure scrape, no login, no eyes Headless Playwright (no stream needed)
Login-walled long-running, or you want to watch /browser
Read/write Gmail/Calendar/Drive/Workspace OAuth API client (never browser-automate Google sign-in)
Captcha/2FA babysitting handoff to a worker /browser + Justin solves via VNC
Visual regression on deploy Headless Playwright + diff (use /browser only for debugging)

Don'ts

  1. Don't try to log into a Google account via Playwright. Google blocks it. Use OAuth APIs for Gmail/Calendar/Drive (already in use). For YouTube Studio / GA / Search Console UI, log in by hand via VNC, then have Playwright navigate the existing session.
  2. Don't share user-data-dir across simultaneous Chromes. They lock-collide and silently route to the first one. Use a different --user-data-dir per parallel job.
  3. Don't bind noVNC to 0.0.0.0. Bound to 192.168.86.50:6080 so only Finn's tunnel and LAN can reach it. CF Access gates everything from the public internet.
  4. Don't expect anti-bot stealth. Use Patchright (already installed) for CDP-detection bypass; for hardcore vendors (DataDome, Akamai, HUMAN), need WebGL renderer spoof + residential proxy beyond Patchright.

Mount-namespace caveat

/etc and /usr are mounted ro in Claude Code sessions spawned by certain forge systemd services (ProtectSystem=strict). The primary install of this stack happened after a sudo mount -o remount,rw /etc /usr lift. Installed files persist on disk; the protection re-applies on next service restart, which is correct.

If you need to add packages or units later from a Claude session and hit "read-only filesystem":

sudo mount -o remount,rw /etc
sudo mount -o remount,rw /usr
# do work
# (no need to restore: next systemd respawn restores ro)

Build history

  • 2026-05-01: Built end-to-end. Justin confirmed live via noVNC. Smiley demo via Playwright proved mouse control works.
  • 2026-05-01: Patchright added, Cloudflare Access wired (policy Justin Secure Only), templated systemd units installed, /browser skill shipped.