agentsclimarketplace

Agent webbridge

Skill jeet-dhandha/agent-webbridge/skills/agent-webbridge

Drive the user's REAL Chrome — multiple profiles with their LIVE logins, and MULTIPLE TABS PER PROFILE, all IN PARALLEL — through agent-webbridge. Clean-room, open-source (MIT), no account, no telemetry. Automates the user's actual Chrome with their real logged-in sessions (not headless/scrape like Playwright or Firecrawl). Use for any task needing a real browser across one or more logged-in Chrome profiles: multi-account workflows, acting as the user across several accounts at once, or driving N tabs in one profile concurrently.From its SKILL.md

Install
npx -y skills add jeet-dhandha/agent-webbridge --skill agent-webbridge

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

18.7 KB, as published. Nobody here has run it

agent-webbridge

Clean-room, open-source (MIT) browser automation for AI agents. Drives the user's actual Chrome — multiple profiles with their live logins, and multiple tabs per profile, all in parallel. A lightweight Node daemon runs per profile on a deterministic hashed port; a router on http://127.0.0.1:10086 proxies /command to the right daemon by a top-level "profile" field. macOS-first (Google Chrome).

agent-webbridge is clean-room and standalone: its own daemon (src/daemon/, only runtime dep is ws) and its own MV3 Chrome extension (agent-webbridge-extension/, stable id ifodkkbkmngjlkhiphcjmbceeolhpfeo). No closed-source dependency, no account, no telemetry, and no curl|bash installer. It is localhost-only: the extension connects only to a daemon you run on 127.0.0.1; no data leaves the machine and no remote server is ever contacted.

When to use

Use this whenever a task needs a real browser with the user's real logins, especially across more than one account or with several pages in flight at once:

  • Any browser / web / "open this URL" / navigate / click / read-a-page task in the user's own Chrome.
  • Multi-account or multi-profile workflows — acting as the user across several accounts (Work + Personal, multiple Gmail / Drive / Ads accounts) at once.
  • Throughput tasks where many tabs in one profile should run concurrently — agent- webbridge attaches chrome.debugger per tab, so N tabs in one profile run in parallel (the killer feature vs typical browser bridges, which funnel every call through one global "current tab").
  • Anything you would otherwise reach for a headless tool (Playwright, Firecrawl) for, but where the real logged-in session matters — here you get the user's actual Chrome instead.

Prefer this over headless/scrape tools when login state, real cookies, multiple simultaneous profiles, or per-profile tab parallelism matter.

Health check (always do this first)

awb status

Then act on the result:

  • daemonUp: true and extensionConnected: true for the profile(s) you want — healthy. Proceed with the tool calls below.
  • Anything else (router/daemon not up, extension not connected) — bring the fleet up with the commands under How to use, then re-check. See AGENTS.md for the full setup, install, and diagnose flow.

How to use

Full, copy-pasteable setup is in AGENTS.md. Quick version:

npm i -g agent-webbridge         # daemon + `awb` CLI
awb setup "Work" "Personal"      # install the extension (opens Web Store, "Add to Chrome") + bring the fleet up
awb connect "Work" "Personal"    # (re)point each extension at its daemon (zero clicks; closes Chrome)
awb up      "Work" "Personal"    # start each profile's daemon + router on :10086, open windows
awb status                       # verify: extensionConnected:true per profile
awb down                         # tear down the fleet

(Pre-publish, run node bin/awb.mjs <cmd> from the repo. Run awb doctor first for a read-only environment self-check.)

Installing the extension (one-time, from the Chrome Web Store)

The extension installs from the Chrome Web Store. awb setup <profile…> automates everything except the one click Chrome reserves for a human. When you (the agent) run it, guide the user through that click and poll for success rather than waiting blindly:

  1. Run awb setup "<profile>". It opens the Agent WebBridge Web Store listing in that profile.
  2. Tell the user to click Add to Chrome → confirm Add extension.
  3. Poll awb check "<profile>" --json. For each profile it reports loaded, enabled, daemonUp, connected, a ready boolean, and a single nextStep hint. Relay nextStep to the user and loop until ready: true. (awb setup also polls and continues on its own once the install lands, then connects + brings the fleet up.)

Developers iterating on the extension source use awb install-dev instead, which Load-unpacks the in-repo build. The daemon recognizes both the store id and the dev id.

Zero-click connect: awb connect points each profile's extension at its daemon by writing local_url directly into the extension's on-disk storage.local — no popup, no clicks. It quits Chrome to write (LevelDB is single-writer) and the value persists, so later awb up runs just reconnect. If Chrome is already quit, awb up alone does the write for you.

Driving a profile

POST to the router on http://127.0.0.1:10086/command. The body is the normal command body plus a top-level "profile" field (name / email / directory) selecting the target profile. Omit "profile" to hit the default profile. Every command also carries a top-level "session" naming the current task — see Sessions.

curl -s -X POST http://127.0.0.1:10086/command \
  -H 'Content-Type: application/json' \
  -d '{"action":"navigate","args":{"url":"https://mail.google.com"},"session":"s1","profile":"Work"}'

Parallelism is cross-profile (N profiles) × per-tab (N tabs/profile): fire several /command requests at once — across different profiles, or at different tabs within the same profile — and they run concurrently.

High-throughput batch jobs (fan-out + resume)

For a large worklist (hundreds–thousands of items — scrape N companies, check N accounts, fill N forms), drive it as a fleet fan-out:

  • Defaults (unless the user specifies otherwise): 5 tabs per profile, across 2 profiles. First check what's available (awb status / awb check --json / awb profiles): use 2 profiles if at least two are set up + connected, otherwise fall back to 1 profile. Within each profile keep 5 concurrent tabs. If the user asks for a specific tabs-per-profile number, use that instead (per-profile parallelism is proven safe up to 10 tabs; 5 is the gentler default).
  • Total concurrency = #profiles × tabs-per-profile. Bring up several profiles (awb up "P1" "P2" …), split the worklist into one chunk per profile, and within each profile keep up to M /command requests (M tabs) in flight at once — e.g. 5 profiles × 10 tabs = 50 items processed concurrently. Per-tab parallelism is real (chrome.debugger is attached per tab), so tabs in one profile don't block each other.
  • Make it resumable. Write results to disk incrementally — rewrite/append the output file after each item, not once at the end. On (re)start, load the existing output, skip the items already done, and process only the remainder. An interrupted run (crash, awb down, machine sleep) then resumes where it stopped instead of redoing everything — and you can safely stop a run to re-tune concurrency, then relaunch.
  • Mind the shared egress IP. All profiles and tabs share one outbound IP. Fanning wider speeds up visiting many distinct sites, but for a step that hammers one service (e.g. a search engine), more parallel requests get rate-limited / CAPTCHA'd sooner — not finished faster. Throttle those steps.

Tools

13 tools, full parity, all verified live in a real browser:

ToolArgsReturnsNote
navigateurl, newTab(bool), group_title{success, url, tabId}First call opens a tab — see Tabs. group_title sets the group's visible label
find_taburl, active(bool){success, url, tabId}Select an already-open tab as the current one — see Tabs
snapshot—{url, title, tree} with @e refsAccessibility tree (text) — use this to read page content and locate elements
clickselector (@e ref or CSS){success, tag, text}Synthetic el.click()
fillselector, value{success, tag, mode}Works on <input>/<textarea> AND [contenteditable] (ProseMirror/Lexical/Slate). mode is "value" or "contenteditable"
evaluatecode (supports async/await){type, value}
screenshotformat(png|jpeg), quality(0-100), optional selector (@e/CSS), optional path{format, path, sizeBytes, mimeType}Returns a file path, not base64 — see Screenshots
networkcmd(start|stop|list|detail), filter, requestIdrequest/response dataCapture requests/responses
uploadselector, files(string[]){success, fileCount}
save_as_pdfpaper_format, landscape, scale, print_background, optional path{path, sizeBytes, mimeType, pageTitle}Render current page → PDF, returns a file path — see Save as PDF
list_tabs—{success, tabs:[{tabId, url, title, active, groupTitle}]}Inspect tabs in the current session
close_tab—{success, closed: bool}Close the current tab in the session
close_session—{success, closed: int}Close all tabs in the session — closed is the count. See Sessions

Tabs and the current tab

Single-tab tools (snapshot, click, fill, screenshot, save_as_pdf) act on the current tab — the one you most recently opened with navigate or selected with find_tab. Because the daemon attaches chrome.debugger per tab, multiple tabs in the same profile can be driven concurrently from parallel /command requests.

  • Opening pages: use newTab:true when pages should coexist (comparing, cross-referencing, or running in parallel); omit it to send the current tab to a new URL.
  • Going back to an earlier tab: call find_tab to make a tab you already opened the current one again. Pass the tab's full URL — take it from list_tabs or the earlier navigate result. A bare root domain may miss a www. tab, so prefer the exact URL. active:true picks the tab the user is currently viewing; otherwise the leftmost match wins.
  • If find_tab returns "no open tab found", the page isn't open — navigate with newTab:true instead.
curl -s -X POST http://127.0.0.1:10086/command \
  -d '{"action":"find_tab","args":{"url":"https://www.example.com","active":true},"session":"research","profile":"Work"}'

Call Format

Every command carries a top-level session naming the current task — see Sessions. Add a top-level profile to target a specific profile (omit for the default). The examples in later sections omit them only for brevity; in real calls always include session (and profile when driving a non-default profile).

curl -s -X POST http://127.0.0.1:10086/command \
  -H 'Content-Type: application/json' \
  -d '{"action":"navigate","args":{"url":"https://example.com","newTab":true,"group_title":"My task"},"session":"my-task","profile":"Work"}'

Sessions

One task = one session = one tab group. A session collects every tab this task opens into a single Chrome tab group, so the user sees one group representing "what the agent is doing right now". Pass it as a top-level field of the request body (not inside args).

Rules:

  1. Pick one session name when the task starts, put it on every command, and never change it mid-task.
  2. One task uses one session — even across multiple sites. Searching and then opening results on three different domains all share the same session and land in the same group. Do not switch session names per site — that is the #1 cause of fragmented tab groups.
  3. Name the session after the task, not the site or domain — e.g. camping-research, phone-compare.
  4. group_title is the human-readable label shown on the group in the browser. Pass it on the first navigate; later calls in the same session don't need it.
  5. Use multiple sessions only when the user asks for several unrelated tasks at once — one session per task.
  6. Reuse an existing group — don't fragment. Before opening tabs, check what's already open (awb groups, or list_tabs and read each tab's groupTitle). If a group for this task already exists (agent:<session>), reuse that exact session name instead of starting a new one. The daemon already re-joins a group by session name across service-worker restarts — fragmentation comes from the caller picking a new name, not from the fleet.
  7. Parallel sub-agents / workflows share ONE session. When you fan out several agents/workers for the same task, pass them all the same session name. Letting each pick its own (probe-x, probe-y, …) scatters the work across many groups — and orphans them if a worker crashes mid-run. (One task = one group still holds, no matter how many workers.)
  8. Clean up temporary sessions — even on failure. For scratch/probe sessions, close_session when done and on error/early-exit paths. List strays with awb groups and prune any with awb close <session> (the name shown after agent:).
# First tab of the task: set session + human-readable label
curl -s -X POST http://127.0.0.1:10086/command \
  -d '{"action":"navigate","args":{"url":"https://www.google.com/search?q=tents","newTab":true,"group_title":"Camping gear research"},"session":"camping-research","profile":"Work"}'

# Another SITE, SAME task → same session → joins the same group automatically
curl -s -X POST http://127.0.0.1:10086/command \
  -d '{"action":"navigate","args":{"url":"https://www.example.com/search?q=tents","newTab":true},"session":"camping-research","profile":"Work"}'

# Every later command carries the same session
curl -s -X POST http://127.0.0.1:10086/command \
  -d '{"action":"snapshot","args":{},"session":"camping-research","profile":"Work"}'

When the task is finished and the user no longer needs these pages, close_session clears the whole group. If they might want to look further, deliver your answer first and leave the tabs open — closing too eagerly throws away work the user can still see.

Screenshots

The daemon writes the image to disk and returns {format, path, sizeBytes, mimeType} — never base64, since the model can't read raw image bytes. Take the .path and open it with the Read tool to actually see it.

# Default: PNG of the visible viewport, daemon picks a temp path
curl ... -d '{"action":"screenshot","args":{}}'
# Options (each independent): JPEG quality, element-only via @e/CSS selector, custom output path
curl ... -d '{"action":"screenshot","args":{"format":"jpeg","quality":60}}'
curl ... -d '{"action":"screenshot","args":{"selector":"@e123"}}'

A caller-supplied path is honored verbatim (parent dirs created, existing file overwritten) — use a unique name to avoid clobbering. save_as_pdf follows the same rule.

Prefer snapshot over CSS/JS selectors

snapshot returns interactive elements with stable @e refs based on semantic role/name. Use them directly with click/fill — they survive CSS class hash changes that break manually-written selectors.

Fall back to evaluate (JS) only when:

  • The target has no @e ref in the snapshot
  • You need attributes not in the snapshot (e.g., href)
  • You need to dispatch complex event sequences, or scroll

Evaluate tips

  • Always use compact JSON.stringify(data) — never add null, 2 formatting. Indentation and newlines can inflate the response several times over, causing truncation during transmission.
  • evaluate calls share the page's JS realm — re-declaring the same const/let across two calls throws SyntaxError. Wrap in an IIFE for a fresh scope: (() => { const x = ...; return x; })().

Text input — use fill

fill handles native inputs and contenteditable. Pass selector (CSS or @e ref) + value:

TargetWhat fill doesReturned mode
<input> / <textarea>Sets .value via native setter, fires input/change."value"
[contenteditable] (ProseMirror / TipTap / Lexical / Slate / Quill etc.)Focuses, selects all existing content, calls document.execCommand('insertText', ...) which fires beforeinput/input with inputType:'insertText' and data:value."contenteditable"
Other elementBest-effort .value + events."value"

fill is clear-and-insert: existing content is replaced. For "append to existing text", read the current value via evaluate, concatenate, then fill with the result.

Form submit / special keys

There's no separate "press Enter" tool. To submit a form, click the submit button directly (its @e ref or selector). To dispatch a key event programmatically (e.g. Escape to close a modal):

{"action":"evaluate","args":{"code":"document.activeElement.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))"}}

Save the current page as PDF

save_as_pdf renders the current page to PDF and returns the file path. All args optional:

  • paper_format: letter (default) | a4 | legal | a3 | tabloid
  • landscape: false (default)
  • scale: 1.0 (default), range [0.1, 2.0]
  • print_background: true (default) — keep background colors
  • path: caller-supplied output path; if absent, daemon picks a default under OS temp dir using the page title as the filename

path semantics match screenshot: written verbatim, parent dirs auto-created, existing files overwritten.

Known limitations

  • Sites that strictly check event.isTrusted (some banking portals, captcha challenges) reject fill and click because both go through DOM-level synthetic events (isTrusted=false). This is a product boundary, not a bug.
  • Cross-origin iframes: fill, click, evaluate, and snapshot operate on the top frame. If a target element lives in a same-page iframe from a different origin, navigate to the iframe's URL directly instead.

Scope and limits

  • macOS-first (Google Chrome). Linux/Windows is a documented follow-up.
  • One daemon per profile, each on a deterministic hashed port with its own isolated state.
  • :10086 is reserved for the router — it is never assigned to a profile.
  • Localhost-only: the extension connects only to a 127.0.0.1 daemon you run. No remote server is ever contacted, no analytics, no account.
  • node engines >=18.

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.