Skip to content

Repository files navigation

Browser Target Selector (bts)

Make your browser searchable like a file system — only semantically.

bts finds running browser windows and tabs across Chrome, Edge, and Firefox, lets you pick one by describing it in plain language, and brings it to the foreground. It ships as a standalone CLI and an MCP server, so Claude Code, GitHub Copilot, Cursor, and Windsurf all share the same browser context.

$ bts find "the CRM with customer Contoso" --activate
Unique match for "the CRM with customer Contoso":

  Microsoft Edge - Work - Dynamics 365 | Contoso
  https://contoso.crm4.dynamics.com/main.aspx

Activated: Microsoft Edge - Work
Window brought to the foreground.

Status: working on Windows 11, 145 unit tests, CDP and BiDi paths verified against real browsers. See honest limitations.


The problem

When you ask an AI agent for a screenshot, a Playwright recording, or a visual check, it has to know which browser window you mean. Today you read a window title off your screen and retype it into the chat. With several Chrome profiles and a dozen open windows, that is slow and error-prone.

bts inventories what is actually running and offers a stable, numbered choice — or resolves your intent directly.

Design principles

  1. All logic lives in the CLI. Skills, agents, and MCP tools are thin adapters with no detection heuristics.
  2. One neutral JSON protocol for every host.
  3. Capability-driven setup: detect automatically, ask only about real gaps.
  4. Never disguise missing precision. If tabs cannot be determined, say so — never invent them.
  5. Never change a browser you did not start. No added flags, no restarts, no navigation, no silently enabled debug ports. (bts launch starts a separate instance with its own profile — it never modifies or reuses your personal one.)
  6. Privacy first. URLs are redacted by default; private windows are excluded unless asked for.
  7. Choose by number. Users never retype titles, profiles, or technical IDs.

How this compares

Several tools give AI agents access to browser tabs. They solve different problems — pick by what you actually need.

bts tabb OpenTabs
Goal pick and focus the right target manage tabs from terminal/agent act inside web apps
Browser access none required — process + window enumeration; CDP/BiDi only via a debug port Chrome extension + native messaging Chrome extension
Extension needed no yes yes
Platform Windows macOS, Linux Chrome, any MCP client
Browsers Chrome, Edge, Firefox Chromium family Chrome
Page control none by design close, reload, read content clicking, typing, screenshots, DOM, network, ~2000 tools via plugins
Reads page content no yes (markdown) yes, plus internal APIs of the logged-in app

What bts deliberately does not do: it does not read page content, does not click, type, or scrape, and does not call an application's APIs. If you want an agent to operate Slack, Jira, or GitHub through your logged-in session, OpenTabs is the far more capable tool and solves a problem bts does not attempt. If you are on macOS or Linux and want terminal tab management, look at tabb.

Where bts is different:

  • No extension, no page access. It works with what is observable from outside a running browser: windows, profiles, and the active tab title. Nothing is injected into your pages.
  • Full access only when you ask for it. For recordings and automation, bts launch starts a separate instance with a debug port and its own profile. Your everyday browser is never restarted, never reconfigured, and never given a debug port behind your back.
  • Honest degradation. When it cannot see background tabs, it says so via the capability level instead of guessing.
  • Selection, not control. The job is answering "which window/tab do you mean?" and bringing it forward — then handing off to Playwright, a screenshot tool, or an agent.

The three are complementary rather than mutually exclusive: bts can pick and focus the target, and a content-level tool can take it from there.


Install

Requirements: Windows 11, .NET SDK 8 or newer.

git clone <repository-url>
cd browser-target-selector
.\setup.ps1 -Hosts both -AddToPath

This builds bts.exe and bts-mcp.exe into %LOCALAPPDATA%\BrowserTargetSelector\bin, adds that directory to your user PATH, installs the host adapters, and runs a smoke test. Open a new terminal afterwards so PATH is picked up, then verify:

bts doctor

Uninstall with .\uninstall.ps1 -Hosts both.

-Hosts (not -Host): $Host is a reserved PowerShell variable.

Build from source

dotnet build Bts.slnx
dotnet test tests/Bts.Tests/Bts.Tests.csproj
dotnet run --project src/Bts.Cli -- inventory

Usage

Find a target by describing it

bts find "unity tutorial video"
bts find "my mail" --activate          # activate straight away on a unique match
bts find "contoso" --activate          # several matches -> asks for a number, then activates

Search runs offline: weighted matching over host, title, path, profile, and browser, plus curated synonyms (CRM → Dynamics/Salesforce, mail → Outlook/Gmail) and diacritics folding. Nothing leaves your machine. A unique match is reported directly; ambiguous results become a numbered prompt.

What is searchable by default: for a browser you started normally, bts sees every window and its active tab title — but not background tabs and no URLs. Windows exposes no API for that. To search and activate all tabs, the browser has to run with a debug port; see Full tab lists and URLs. The capability level says which case you are in, so nothing is ever invented.

List and activate by number

bts inventory
bts activate 2          # activates window [2] from the list above
[1] Google Chrome - Default → win_chrome_1
    ▶ Power Apps
[2] Microsoft Edge - Work → win_edge_1
    ▶ Dynamics 365 | Contoso

Activate: bts activate <number>   e.g. bts activate 2
Or by context:  bts find "<context>" --activate
Snapshot: snap_ab12cd34 - valid until 14:05:12 UTC

If the snapshot has expired, bts activate <number> simply re-inventories and tells you it did.

Other commands

bts select --scope window --interactive    # numbered terminal menu
bts doctor                                 # installed browsers + capability report
bts groups                                 # tabs bundled by site, for large tab sets
bts dedupe                                 # tabs pointing at the same page (read-only)
bts export --out session.json              # portable, redacted state snapshot
bts config show
bts config set redaction strict
bts inventory --json                       # exactly one JSON document on stdout

dedupe reports duplicates — it never closes a tab. The same page in a different profile is not a duplicate, since "the CRM in Work" and "the CRM in Personal" are different targets. find uses the same identity rule to collapse duplicates, so three tabs on one page stay a single, unambiguous choice:

$ bts dedupe
1 duplicate tab(s) in 1 group(s), out of 6 tabs:

2x  Example Domain
      Google Chrome - Default  https://example.com/
      win_chrome_2_tab_1  (active)
      win_chrome_2_tab_3

Duplicates are only reported - bts never closes a tab.

groups bundles tabs by registrable domain, so make.powerapps.com and admin.powerapps.com land in one group. With dozens of tabs open, an agent should show the groups first and only then the tabs inside the chosen one — a flat list of 40 options is unusable. Tabs whose URL is unknown are bucketed per browser/profile and marked as such, never mixed into a domain group:

$ bts groups
8 tab(s) in 4 group(s):

[1] Google Chrome - Default  (3 tab(s))  (URL unknown)
    * Some page  -> win_chrome_2_active

[2] example.com  (2 tab(s))
    * Example Domain  -> win_chrome_1_tab_1
      Example Domain  -> win_chrome_1_tab_5

Activate a tab:  bts activate <tabTargetId>

export writes windows, profiles, and redacted tab URLs as JSON. It deliberately omits window handles, process IDs, and snapshot IDs — they are meaningless elsewhere and would leak process details into a file people tend to share.

Every command supports --json. Structured output goes to stdout, diagnostics to stderr.

Where do --target-id and --snapshot-id come from?

The [1] [2] … in bts inventory are a display counter, not the target ID. The real identifier (win_chrome_1) appears greyed out at the end of each line.

You normally never need thembts activate <number>, bts find … --activate, and bts select --interactive all resolve IDs internally. When you do need them, they come from the output of bts find --json, bts inventory --json, or bts select --json, and are valid only within that snapshot (30 s by default, configurable via bts config set snapshot-ttl).

Focusing a window for other tools

Activation is deliberately conservative: the existing window (and, with full capability, the tab) comes to the foreground. No navigation, no reload, no restart. That is exactly what you need before a tool acts on "the active window":

bts find "Power Apps" --activate     # focus the right window
# ... then run your screenshot / Playwright / automation tool

bts find "Power Apps" --json         # or hand snapshotId + targetId to another agent

bts is a target selector, not a Playwright replacement. It reliably determines which target is meant.


Full tab lists and URLs (optional)

By default bts sees windows and the active tab title (window_and_active_tab). To search and activate all tabs including redacted URLs (full), the browser must run with a loopback debug port. bts never enables one itself — that is your deliberate decision. Close the browser fully, then:

& "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
& "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --remote-debugging-port=9222
& "C:\Program Files\Mozilla Firefox\firefox.exe" --remote-debugging-port=9222   # WebDriver BiDi

bts detects the port from the process command line and enriches exactly those windows. Nothing else about the invocation changes.

Listing and activating background tabs

bts inventory --include-tabs --json
{
  "snapshotId": "snap_ab12cd34",
  "browsers": [{ "displayName": "Google Chrome", "windows": [{
    "targetId": "win_chrome_1",
    "capability": "full",                       // <- all tabs are known
    "tabs": [
      { "targetId": "win_chrome_1_tab_1", "title": "Example Domain",  "url": "https://example.com/",  "isActive": true  },
      { "targetId": "win_chrome_1_tab_2", "title": "Example Domains", "url": "https://www.iana.org/help/example-domains", "isActive": false },
      { "targetId": "win_chrome_1_tab_3", "title": "Example Domain",  "url": "https://example.org/",  "isActive": false }
    ]}]}]
}

Pick a tab from a numbered list, or activate one directly:

# numbered tab list for one window
bts select --scope tab --window win_chrome_1 --snapshot-id snap_ab12cd34

# activate a BACKGROUND tab (brings the window forward AND switches to that tab)
bts activate win_chrome_1_tab_2 --snapshot-id snap_ab12cd34
#   -> Window brought to the foreground. Tab activated via CDP.

# or just describe it - background tab content is searchable now
bts find "iana help example domains" --activate

bts activate <number> always means a window. For a specific tab, pass its target ID (win_chrome_1_tab_2) or use bts find … --activate.

Tab activation uses CDP for Chrome/Edge and WebDriver BiDi for Firefox — it switches the tab without navigating or reloading it.

Security note: the port is bound to 127.0.0.1, but while it is open any local application can use it. Enable it only when you need it, and restart the browser normally afterwards.

bts launch — full capability in one command

If you are going to record or drive a browser anyway (Playwright, screenshots, E2E work), you do not need to restart your personal browser at all. bts launch starts a controlled, separate instance with a debug port and its own profile:

bts launch --browser chrome --url https://example.com
Started Google Chrome (controlled instance)
  PID 2696 - debug endpoint http://127.0.0.1:36057
  Dedicated profile: C:\Users\you\AppData\Local\BrowserTargetSelector\profiles\chrome
  Your personal browser profile is untouched.
  Debug endpoint is ready - bts now has full tab access for this instance.

Next:  bts inventory --include-tabs

From that moment the instance has full capability — all tabs, all URLs, tab activation:

bts inventory --include-tabs
bts find "the app under test" --activate

Options: --port (default: a free port is picked), --profile-dir (default: a managed directory per browser, so logins survive between runs), --fresh (throwaway profile), --url (repeatable), --json.

Safety properties, enforced in code and covered by tests:

  • always a dedicated profile directory — your personal profile is never reused, so no profile locks and no session bleed
  • the instance is started detached: it survives a short-lived agent process, and it never blocks a caller that pipes bts output
  • already running browsers are left completely untouched

That covers the automation use case completely — no browser extension required. An extension would only help in the other case: reading all tabs of a browser you already have open and do not want to restart. It remains deliberately unbuilt — see ADR 0004.

Browser Without debug port With --remote-debugging-port
Chrome / Edge windows + active tab title all tabs + redacted URLs (CDP)
Firefox windows + active tab title, profile from profiles.ini all tabs + redacted URLs (WebDriver BiDi)

Use it from an AI agent

MCP server (host-neutral, recommended)

bts-mcp.exe speaks JSON-RPC over stdio and exposes the same logic as tools:

Tool Purpose
browser_find describe a context → scored matches + confident flag
browser_inventory snapshot of all windows/tabs
browser_select numbered selection / resolve a number
browser_activate bring a window/tab to the foreground
browser_launch start a controlled instance with full tab access (see above)
browser_groups bundle tabs by site so large tab sets stay selectable
browser_dedupe report tabs pointing at the same page (read-only)
browser_export portable, redacted snapshot of the browser state

Same configuration shape everywhere, only the file location differs:

{
  "mcpServers": {
    "bts": {
      "command": "C:\\Users\\<you>\\AppData\\Local\\BrowserTargetSelector\\bin\\bts-mcp.exe",
      "args": []
    }
  }
}
  • Claude Code: claude mcp add bts -- "<path>\bts-mcp.exe", or an entry in .mcp.json
  • Cursor: .cursor/mcp.json (project) or ~/.cursor/mcp.json (global)
  • Windsurf: ~/.codeium/windsurf/mcp_config.json
  • GitHub Copilot (VS Code): .vscode/mcp.json

Then just ask: "Find the tab with the Contoso CRM and bring it to the front."

Claude Code skill

setup.ps1 (or bts install-host --host claude --scope user) writes ~/.claude/skills/browser-target-selector/SKILL.md. Allow the CLI once in ~/.claude/settings.json:

{
  "permissions": {
    "allow": ["Bash(bts *)", "Bash(bts.exe *)"]
  }
}

The skill activates automatically when your request matches, or explicitly via /browser-target-selector.

GitHub Copilot

bts install-host --host copilot --scope project

Writes .github/copilot/agents/browser-target-selector.agent.md. Add a short pointer to .github/copilot-instructions.md telling Copilot to use bts for browser targets.


Language

English is the default. German is fully translated:

bts inventory --lang de
$env:BTS_LANG = "de"          # per session
bts config set language de     # persistent

Resolution order: --lang > BTS_LANG > configuration file > English. A unit test enforces that the German table covers every English key.


Privacy and security

  • URL redaction (default balanced): query and fragment removed. strict keeps only the domain. Even at off, known secret parameters (token, code, password, …) stay masked, and file:/data:/blob: URLs are always masked.
  • Private/incognito windows are excluded unless you pass --include-private.
  • Snapshots are short-lived and contain only redacted data.
  • No telemetry, no network calls except to 127.0.0.1 debug ports you opened yourself.
  • No admin rights required.

Details and the honestly documented detection gap: docs/SECURITY_PRIVACY.md.

Capability levels

bts always reports what it can actually deliver:

Level Meaning
full all windows, tabs, titles and redacted URLs
window_and_active_tab windows plus the active tab
window_only windows reliable, tabs unknown
process_only only the process/installation detected
unavailable adapter not usable

Honest limitations

  • Without a debug port only the active tab title is available — background tabs and URLs are not.
  • about: pages in Firefox return no title via BiDi (privileged); shown as (untitled).
  • A Chromium incognito window showing a regular page cannot be detected passively (same process, no title marker). Documented in docs/SECURITY_PRIVACY.md.
  • Windows may refuse foreground activation in rare cases; bts reports this instead of claiming success.
  • Windows only for now. Other platforms are an adapter concern, not a rewrite.

Project layout

src/
├─ Bts.Core/         domain model, wire contracts, localization, exit codes, configuration
├─ Bts.Adapters/     IBrowserAdapter, Windows baseline (process + user32),
│                    CDP enricher (Chromium), BiDi enricher (Firefox), native interop
├─ Bts.Application/  discovery, capability, inventory, selection, activation, redaction,
│                    semantic search, snapshot store
├─ Bts.Cli/          System.CommandLine front end, host installer, setup wizard
└─ Bts.Mcp/          host-neutral MCP server (stdio JSON-RPC)
tests/Bts.Tests/     xUnit (145 tests)
docs/                architecture, CLI spec, security, ADRs, implementation status
schemas/             JSON Schema (Draft 2020-12) for the wire protocol

Documentation

Contributing

See CONTRIBUTING.md. The design rules above are non-negotiable; please add an ADR for architectural changes.

License

MIT © Andreas Brunsmann (@brunsforge)

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages