Skip to main content
Glama

Browser Task Runner

Run repetitive admin work in a web browser, in batches, with a person reviewing every decision before anything is changed. Or just ask your AI assistant to do something on a site, and let it line up the steps and carry them out in one go.

Browser Task Runner is a local tool with two halves that share one engine:

  • Batch skills. For high-volume, rules-based queues (moderating reviews, triaging tickets, approving submissions). Items are read without changing anything, a person or an AI session decides a verdict for each, and only then are the decisions applied, with a dry run first, a staged rollout, a rate limit, a circuit breaker, an append-only audit log and a check afterwards that each change landed.

  • General browser control. Navigate, read, fill and click on any allowed site, from any MCP client. A plan of many steps runs in one call. Irreversible actions stop and ask for confirmation, and password, card and ID fields are never typed into.

Everything runs on your machine, in a browser you are already logged into. There is no hosted service and no API key.

Copyright (c) 2026 Manish Verma. MIT License.


How it works

                        +------------------------------+
  MCP client            |  mcp/server.mjs  (stdio)     |
  (Claude Code,  <----> |  batch tools + browser tools |
   Claude Desktop,      +--------------+---------------+
   others)                             |
                                       v
  bridge/*.js CLI  ---->  +------------------------------+      audit log
                          |  engine/   (shared)          | ---> logs/*.jsonl
                          |  skills, templating, rate    |      runs in
                          |  limit, circuit breaker,     | ---> exchange/runs/
                          |  rollout, verify, safety     |
                          +------+---------------+-------+
                                 |               |
                     CDP (puppeteer-core)    bridge server :8777
                                 |               |  (job queue)
                                 v               v
                   +-----------------------------------------+
                   |  Your Chromium-based browser            |
                   |  (Chrome, Brave, Edge, Chromium)        |
                   |  requests run in a tab on the target    |
                   |  site, with your existing session       |
                   |  - or via the optional extension -      |
                   +-----------------------------------------+

The batch flow for one skill:

 list_queue ──► harvest ──► (person / AI writes verdicts) ──► submit_verdicts
  read-only     read-only        using the skill's ruleset      validated
                                                                    │
         verify ◄── act(live) ◄── person approves the plan ◄── act(dry run)
      re-reads       capped by         shown every request        default
      each item      staged rollout    it would send

Two front ends drive the same engine:

  1. MCP server (mcp/server.mjs). Connects to your browser over the Chrome DevTools Protocol, either by attaching to one you started with remote debugging or by launching one. Requests run inside a tab on the target site, so they carry your logged-in session. This is the main way to use it.

  2. Browser extension + bridge (optional). An unpacked Manifest V3 extension that long-polls a small local server (bridge/server.js, port 8777) for jobs and runs them in your browser. Useful if you prefer not to start the browser with remote debugging. The bridge also serves a live dashboard at http://127.0.0.1:8777/.


Related MCP server: webpilot

Requirements

  • Node.js 22.12.0 or newer

  • A Chromium-based browser: Chrome, Chromium, Brave or Edge

git clone <this repo> browser-task-runner
cd browser-task-runner
npm install
npm test          # static checks, engine, browser and MCP tests

Quick start: the demo, end to end

The repo includes a tiny local admin app, a fake product-review queue, so you can try everything safely. Its state lives in memory and resets when it restarts.

1. Start the demo app

node demo/server.js            # http://127.0.0.1:8790

2. Start a browser for automation, with remote debugging on and a separate profile, so your everyday profile is never exposed:

# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --remote-debugging-port=9222 --user-data-dir="$HOME/.btr-chrome-profile"

# Linux
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.btr-chrome-profile"

# Windows (PowerShell)
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
  --remote-debugging-port=9222 --user-data-dir="$env:USERPROFILE\.btr-chrome-profile"

Log in to any site you want the tool to work on in this window. Anything that can reach port 9222 can control this browser, so keep it on 127.0.0.1 and close it when you are done.

3. Add the MCP server to your client (see the next section).

4. Ask. For example:

Use browser-task-runner. List the pending items in the demo review queue, harvest them, read the skill's ruleset, and propose a verdict for each. Show me the verdicts, then do a dry run.

Review what it proposes, then:

Looks right. Approve it and run it live.

Only now does the assistant call approve_live, which returns a single-use approval bound to that exact plan, valid for 15 minutes, and pass it to act with dry_run: false. A dry run on its own never unlocks a live run.

The first live batch is capped at 1 item, the next at 3, then up to the batch size, so a mistake in a skill or a verdict shows up on one item rather than a hundred. Then:

Verify the run and show me the audit log.

Open http://127.0.0.1:8790/ to see the result.

The same flow without an AI client

node bridge/queue.js demo-review-queue --out=exchange/demo/ids.json   # or use demo/ids_to_harvest.json
node bridge/harvest.js demo-review-queue demo/ids_to_harvest.json     # prints a run_id and the items
node bridge/act.js <run_id> --verdicts=demo/verdicts.example.json     # dry run
node bridge/approve.js <run_id>                                       # after reading the plan: prints an approval token
node bridge/act.js <run_id> --live --approval=<token>                 # first live batch (1 item)
node bridge/approve.js <run_id> && node bridge/act.js <run_id> --live --approval=<token>   # next batch (3 items) ...
node bridge/verify.js <run_id>

The command-line tools use the extension by default (npm run bridge, extension loaded). To use the CDP connection instead: BTR_TRANSPORT=cdp BTR_BROWSER_URL=http://127.0.0.1:9222 node bridge/harvest.js .... For a site that needs no login, such as the demo, BTR_TRANSPORT=node uses plain Node requests with no browser at all.


Use with Claude Code, Claude Desktop or any MCP client

The server speaks MCP over stdio. Run it from a clone (node /path/to/browser-task-runner/mcp/server.mjs) or through the package bin (npx browser-task-runner-mcp once installed).

Claude Code

claude mcp add browser-task-runner \
  --env BTR_BROWSER_URL=http://127.0.0.1:9222 \
  --env BTR_ALLOW_HOSTS=admin.example.com \
  -- node /path/to/browser-task-runner/mcp/server.mjs

Claude Desktop (claude_desktop_config.json), or any client that uses the same mcpServers format:

{
  "mcpServers": {
    "browser-task-runner": {
      "command": "node",
      "args": ["/path/to/browser-task-runner/mcp/server.mjs"],
      "env": {
        "BTR_BROWSER_URL": "http://127.0.0.1:9222",
        "BTR_ALLOW_HOSTS": "admin.example.com"
      }
    }
  }
}

Configuration

Variable

Meaning

BTR_BROWSER_URL

Attach to a running browser, e.g. http://127.0.0.1:9222

BTR_WS_ENDPOINT

Or attach by its WebSocket endpoint

BTR_USER_DATA_DIR

Or attach through a profile's DevToolsActivePort file

BTR_CHROME_PATH

Or launch this browser executable (BTR_HEADLESS=1 for headless; with BTR_USER_DATA_DIR as its profile)

BTR_ALLOW_HOSTS

Extra hosts the tools may touch, comma separated. *.example.com allowed. localhost is always allowed

BTR_TRANSPORT

Batch transport: cdp (default when a browser is configured), node, or bridge

BTR_DATA_DIR

Where runs, logs and the bridge token are written (default: the repo folder)

BTR_ALLOW_EVALUATE

Set to 1 to offer the evaluate tool. Off by default; every call still needs confirm: true

Without any browser setting the batch tools still work over plain Node requests (fine for the demo), and the browser tools are not offered.


Just ask

With a browser attached, you can describe a task in plain language and the assistant chains the tools itself. run_plan lets it send many steps in one call, so a ten-step task is one or two round trips rather than ten.

"Find the open tickets assigned to me on helpdesk.example.com and list their titles and dates." navigate to the ticket list, snapshot to see the filters, then one run_plan: select_option (assignee), click (Apply filter), wait_for (results), extract with {"title": "td.title", "date": "td.date"}. Read-only, so nothing needs confirmation.

"Fill in the contact form on the demo app: name Test User, topic Bug report, and say the export button does nothing." One run_plan: navigate to /contact, fill x2, select_option, click "Send message". The click submits a form, so the plan stops with needs_confirmation and a description. The assistant shows you what it is about to send; after you say yes it repeats that click with confirm: true, then extracts the confirmation text.

"Moderate the demo review queue using its ruleset." describe_skill, list_queue, harvest, get_harvest, decides verdicts, submit_verdicts, act (dry run), shows you the plan, calls approve_live only after you agree, then act with dry_run: false and the approval, in staged batches, then verify and read_audit_log.

If a page asks for a password, card number or ID number, the tools refuse to type it and the assistant should ask you to enter it yourself. Values already in such fields are redacted from everything the tools read.


Tool reference

Batch skills

Tool

What it does

Changes the site?

list_skills

Installed skills, their site and verdicts

No

describe_skill(skill)

Config, ruleset and notes

No

list_queue(skill)

Ids waiting in the queue (if the skill defines a list)

No

harvest(skill, ids, limit?)

Reads each item, extracts fields, starts a run

No

get_harvest(run_id)

The harvested items

No

submit_verdicts(run_id, verdicts)

Validates verdicts against the skill's actionOrder

No

act(run_id, dry_run=true, limit?, approval?)

Dry run: lists every request. Live: needs an approval; sends the requests within the caps

Only when dry_run=false

approve_live(run_id)

Single-use approval for the last dry-run plan, 15 min. Call only after the user approves the plan

No

run_status(run_id)

Counts and state of a run

No

read_audit_log(run_id | skill, tail)

The append-only audit log

No

verify(run_id)

Re-reads acted items and checks the expected result

No

Browser control

Tool

What it does

list_pages, new_page(url), select_page(id), close_page(id)

Tabs

navigate(url | back | forward | reload)

Go somewhere in the current tab (refused if the current tab is on a disallowed host; use new_page or close_page)

snapshot()

Text outline of the page; interactive elements get uids like e12

screenshot(full_page?)

PNG of the tab, with sensitive fields masked

find(text, role?)

Element uid by visible text or label

click(target, confirm?)

Click; irreversible clicks need confirm

fill(target, value)

Replace a field's value

type_text(text)

Type into the focused element

press_key(key, confirm?)

Key press; Enter in a form, or Enter/Space on an irreversible-looking button or link, needs confirm; refused while a sensitive field has focus

select_option(target, value)

Choose a select option

hover(target)

Hover

wait_for(text | selector, timeout)

Wait for the page

extract(selector | uid, fields?)

Structured data: {"name": "css"} or "css@attr"

get_text(target?), get_attribute(target, name), query_all(selector, attributes?)

Read-only queries; sensitive field values come back as [redacted: sensitive field]

evaluate(js, confirm)

Only offered with BTR_ALLOW_EVALUATE=1; every call needs confirm

upload_file(target, path)

Attach a local file to a file input

get_network(filter), get_console()

Recent requests and console messages

run_plan(steps, stop_on_error=true)

Many of the above in one call

A target is any of uid (from snapshot or find), selector (CSS), or text (visible text or label, with an optional role of button, link or textbox).

run_plan steps look like {"tool": "fill", "args": {"text": "Email", "value": "a@b.test"}}. An argument can reuse an earlier result: {"tool": "click", "args": {"uid": {"use": "step3.uid"}}}.


Safety model

There are two kinds of control. Enforced controls are checked in code on every path that can reach a site: the MCP tools, the command-line tools and the extension. Approval controls stop and wait for a yes. The tool can make the request for that yes unavoidable, but it cannot tell whether a person actually gave it: an MCP client could pass confirm: true or call approve_live on its own. So the user should run it with a client that shows tool calls and asks before running them.

Enforced in code

Control

Applies to

Host allow-list: localhost plus BTR_ALLOW_HOSTS. Every tool that touches the current page runs through one wrapper (withPage) that refuses a page on a disallowed host, runs the sensitive-field check, runs the tool, then re-checks the host; if the page moved off the allow-list during the call (redirect, link, script, key press, wait), the tab is sent to about:blank and nothing from it is returned. The sensitive-field check must succeed or the tool does not run, and the after-call host check runs even if the tool fails; a tab that cannot be sent to about:blank is closed. navigate gets the same before-check (so reload, back, forward and navigating away are all refused on a disallowed tab; the way out is new_page or close_page) and also checks its target URL. new_page checks the requested URL, and the final URL after redirects even when loading fails or times out. list_pages and select_page show a disallowed tab only as blocked (id and host, no URL path or title)

All browser tools (the selftest fails if a page tool is not wrapped); skill baseUrl; bridge jobs; extension jobs (extension list set in its popup)

No redirect is followed unless it stays on the skill's origin and an allowed host, and the destination is visible (node transport). In the browser (CDP and extension), redirects are never followed and fail the request (REDIRECT_BLOCKED)

Batch read, list, act and verify

Same-origin action URLs; item values are URI-encoded into paths

Batch act

Harvest, list and verify only send GET requests

Batch

Dry run is the default; a live act needs an approval token that is single use, expires after 15 minutes, and is bound to the run, its verdict hash and the dry-run plan. Changing the verdicts voids it

Batch act

Staged rollout and batch cap; a batch with an error does not advance the stage

Batch act

One live act per skill at a time, across processes (lock file; a stale lock from a dead process is cleared)

Batch act

Fresh read before every write: identity guard, stale-content check, precondition

Batch act

Escalation verdicts (empty action list) are never acted on

Batch act

Rate limit; circuit breaker after N consecutive errors

Batch harvest and act

Processed index written before the next item, so a re-run never repeats a write

Batch act

Sensitive fields are classified by one function, run in the page before every read, write, target lookup and screenshot, so the read and write rules cannot differ: type="password"; autocomplete of cc-*, one-time-code, current-password or new-password; or a name, id, any of its labels, placeholder or aria-label matching password, PIN, OTP, card number, CVV/CVC/security code, IBAN, account or routing number, SSN, passport, national ID, tax ID, driver's licence and similar

All browser tools

Typing into a sensitive field is refused, including single key presses while it has focus

fill, type_text, press_key, select_option

A sensitive field's value is never returned: it is replaced by [redacted: sensitive field] in snapshot text, extract results, query_all text and attributes, get_attribute (every attribute except descriptive ones such as type, name, id, autocomplete, placeholder), and get_text (on the field, and on containers, where sensitive descendants are skipped). find and text targets never match a field's value, and a sensitive button's value is shown as the marker

snapshot, run_plan final snapshot, extract, query_all, get_attribute, get_text, find

Screenshots mask every sensitive field during capture (characters hidden by an injected style, and an opaque box drawn over each field, including selects and textareas); the masking is removed afterwards, also on error

screenshot, screenshots inside run_plan

evaluate (arbitrary JavaScript) is not offered unless BTR_ALLOW_EVALUATE=1

MCP and run_plan

Bridge: per-install token on every endpoint except /health; requests from web-page origins and CORS preflights refused; no CORS headers; only fetch, navigate and closeTab jobs (no script execution); loopback only

Bridge and extension

Extension: refuses a non-GET request not marked as a write by the engine

Extension

Audit logs: logs/<skill>.audit.jsonl and logs/browser.audit.jsonl (typed values logged by length only)

Everything

The test suite includes a safety matrix (test/matrix.test.js) that takes the tool list from the MCP server's registry and checks every browser tool on a disallowed page, on a focused sensitive field, and (for tools that can move the page) after moving off the allow-list. A new tool fails the test until it is given a row.

Needs a person to approve

  • Irreversible browser actions (form submits; clicks whose text reads like submit, send, pay, buy, order, delete, publish, post, confirm, approve, reject, transfer and similar; Enter inside a form field; Enter or Space on a button or link that looks irreversible) and every evaluate call return needs_confirmation. The client must show the user what will happen and repeat the call with confirm: true only after a clear yes. run_plan stops at such a step. Detection is by pattern, so it errs towards asking; an action that does not look irreversible will not be stopped.

  • Live batch runs need approve_live. The client must show the user the dry-run plan (plan_summary) and call it only after the user agrees.

  • Verdicts are judgements made by a person or an AI session. The engine checks their form, not whether they are right. Read the dry run.

Known limits

  • Rules for "irreversible" and "sensitive field" are heuristics. A sensitive value shown as ordinary page text (not in a form field) is not recognised: read tools return it and screenshots show it.

  • Screenshot masking covers fields in the main document; fields inside iframes or closed shadow roots are not masked.

  • The extension enforces its own host list but cannot check an approval token; it relies on the bridge token so that only the local engine can send it work.

  • Sites that answer a successful POST with a redirect need "acceptRedirect": true on that action (the redirect is still not followed).

  • It does not solve CAPTCHAs or handle logins or MFA; log in yourself. It does not store cookies or passwords.

Use it on systems you are authorised to operate, and follow each site's terms of use.


The optional extension

  1. Open chrome://extensions (or brave://extensions, edge://extensions), turn on Developer mode, click Load unpacked and select this folder.

  2. Start the bridge: npm run bridge, then check curl -s http://127.0.0.1:8777/health. On first run it creates a random pairing token in .btr-token (gitignored, readable only by you) and prints its location. On Windows, file mode 600 is not enforced: the token file inherits the folder's permissions, so keep the data directory inside your own user profile.

  3. Pair: open the extension popup, paste the contents of .btr-token into "pairing token", add any extra hosts, and click Save. Every bridge endpoint except /health rejects requests without the token, and requests from web pages are refused. The live dashboard is at http://127.0.0.1:8777/#token=<token> (the popup links to it). To pin the bridge to one extension, set BTR_EXTENSION_ORIGIN=chrome-extension://<extension id>.

  4. Add each site you want the extension to work on to host_permissions in manifest.json (for example "https://admin.example.com/*"), and reload the extension. By default it only has access to the bridge and the demo app.

The popup shows bridge status and the latest activity, and links to the live dashboard. To start the bridge automatically at login, see bridge/autostart/ (a launchd template for macOS and a scheduled-task script for Windows).


Write your own skill

A skill is one folder and one line:

src/skills/<your-skill>/skill.json    what to read, what to send, per verdict
src/skills/<your-skill>/ruleset.md    how to decide verdicts (for the reviewer)
src/skills/<your-skill>/NOTES.md      what you confirmed about the site
src/skills/registry.json              add "<your-skill>"

The engine never changes. See SKILL-AUTHORING.md for the full format and a checklist, and src/skills/demo-review-queue/ for a working example. Add the site's host to BTR_ALLOW_HOSTS, run node bridge/selftest.js to validate the skill, and always start with a dry run.


Project layout

engine/        shared engine: skills.js, index.js (runs), browser.js (browser tools),
               transports.js, cdp.js
mcp/           MCP server (stdio)
bridge/        local bridge server, dashboard, command-line tools, selftest
src/           extension service worker and popup; skills/
demo/          demo admin app, sample ids and verdicts
test/          engine, browser and MCP tests

See SCOPE.md for the design and NOTICE.md for third-party notices. The browser connection and tool set are adapted from chrome-devtools-mcp (Apache-2.0); it is not a runtime dependency.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables MCP clients such as Claude Desktop, Claude Code and IDE agents to call a tool that drives a real Playwright browser to complete web tasks, with sensitive actions automatically refused when no human is available to approve them.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables an MCP client to drive a real, already-logged-in Firefox session through a WebExtension rather than a WebDriver connection, so tabs, cookies and logins work as-is without relaunching the browser under remote-control flags. Exposes tools for tab listing and selection, navigation, accessibility-style snapshots, clicking, filling, typing, key presses, file uploads, screenshots and waiting, with per-origin permission grants and opt-in script evaluation.
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables MCP agents to drive the user's already-open Chrome profile through a Manifest V3 extension and loopback bridge, listing and controlling existing tabs, capturing screenshots, and sending clicks, typing, drags and key holds via an independent on-page cursor without moving the system mouse. It also supports form filling, console and network inspection, read-only or confirmed JavaScript execution, file and PDF downloads, and game-style screenshot/action loops with snapshot recovery and an emergency stop.
    MIT