2026-09-18 · 21 min · agents · browser-automation · security · tooling · explainer
BrowserSkill is a Rust CLI, a local daemon, and an MV3 Chromium extension that together let any agent which can call a shell drive the browser you are already signed into. The agent runs bsk click @e3 --session ab12; the CLI talks to the daemon over a Unix socket; the daemon forwards a JSON-RPC frame to the extension over a loopback WebSocket; the extension drives Chrome DevTools Protocol in a separate Agent Window so your own windows keep working. It is MIT-licensed, 504 commits old as I write this, and the README's promise is one sentence:
Need the agent to touch a tab you already have open? It must borrow that tab explicitly, return it when the task is done, and leave the rest of your browser alone.
I spent a day checking that sentence. The interesting part is that it is exactly half true, in a way the code states on purpose and the documentation never does.
| Project | Tencent/BrowserSkill · MIT · Rust + TypeScript workspace |
| Version checked | CLI / extension / DSH plugin all 0.3.0, daemon protocol 1.3, commit d1356fd (2026-09-18) |
| Surface | 41 tool.* protocol methods · 40 dispatched in the extension · 37 top-level CLI commands |
| Extension permissions | debugger, tabs, scripting, downloads, notifications, webNavigation, windows, storage, alarms, idle, activeTab + host permission for all URLs |
| Runtime I used | cargo build -p bsk --locked, the repo's own pnpm ext:build, Chromium 153 via --load-extension |
| Defaults | WebSocket on 127.0.0.1:52800 · session idle timeout 5 min · daemon idle 30 min · borrow confirmation wait 60 s |
The only structural question
An agent that can call a shell already owns the shell. It can read your files, start processes, and edit anything your user account can edit. So in any tool of this shape, exactly one question decides whether a permission is a permission: is the switch on a surface the agent controls? A flag is not consent. An environment variable is not consent. A config file in the agent's own home directory is not consent. They are defaults, and an agent that wants them changed can change them.
BrowserSkill 0.3.0's headline change is an answer to that question, and it is a change that makes the product worse to use — which is the kind of change worth paying attention to. From CHANGELOG.md:
Automation settings: the extension's saved borrow-confirmation and human-help switches govern existing and new sessions.
--unattended,tab borrow --no-confirmandBSK_REQUEST_HELP=offare deprecated compatibility inputs and cannot override these switches.
Three ways to run unattended, all removed, all kept parseable so nobody's script hard-fails. The entire remaining implementation of those three inputs is this, from crates/bsk-cli/src/cli/interaction_policy.rs:
/// Keep legacy inputs parseable without letting them change browser policy.
pub(crate) fn warn_legacy_override(option: &str) {
tracing::warn!(
"{option} is deprecated and has no effect; Automation settings in the browser extension control confirmation and human help"
);
}Four call sites across three inputs — BSK_REQUEST_HELP=off is checked twice, once in the CLI and once in the daemon's inherited environment — and nothing downstream reads them. I checked it against the built binary rather than taking the comment's word for it. With a real daemon up and a real browser attached:
$ bsk tab borrow 1117016028 --session oevn --no-confirm --timeout 8s
WARN bsk::cli::interaction_policy: --no-confirm is deprecated and has no effect;
Automation settings in the browser extension control confirmation and human help
error: timed out waiting for human confirmation
hint: report the blocked step; do not automatically repeat the request or switch browser tools
details: Timed out waiting for tab borrow confirmationEight seconds of real waiting, then a refusal. --no-confirm did not skip the prompt; it printed a warning and the borrow sat there asking a human who was never going to answer. The same happened for --unattended on session start and for BSK_REQUEST_HELP=off on request-help. The agent can also read the policy it is subject to but not change it — bsk session list --json on that session returned "interaction": {"borrow_confirmation": "always", "request_help": "enabled"}.
The switch itself lives in chrome.storage.local, in your browser profile, and the normalizer is fail-closed. From apps/extension/src/lib/interaction-preferences.ts:
export const DEFAULT_INTERACTION_PREFERENCES: InteractionPreferences = {
confirmTabBorrow: true,
requestHelpEnabled: true,
}
export function normalizeInteractionPreferences(value: unknown): InteractionPreferences {
const prefs = value as Partial<InteractionPreferences> | null | undefined
return {
confirmTabBorrow: prefs?.confirmTabBorrow !== false,
requestHelpEnabled: prefs?.requestHelpEnabled !== false,
}
}!== false rather than a truthiness check, so corrupted, missing, half-written or wrongly-typed storage all resolve to on. The only value that disables confirmation is the literal boolean false, which only the popup writes. And the confirmation is rendered somewhere the agent cannot reach, for a reason the code spells out in apps/extension/src/entrypoints/background.ts:
// Skip every Agent Window when choosing where to render the
// overlay — Agent Windows boot on about:blank, which has no
// content script, so they cannot surface an authorization decision.
isAgentWindowId: (windowId) => sessions.findByWindowId(windowId) !== null,The approval prompt always appears in a window you own, plus an OS notification in case the Agent Window stole focus. That is careful work, and it is the part of this release I would copy.
the shell itselfthe agent runs bsk, edits ~/.bsk, can start an older daemon, can read the profile on disk--unattendedmoved in 0.3.0parsed, logged as deprecated, then discarded — warn_legacy_override() is its whole implementationtab borrow --no-confirmmoved in 0.3.0same: the borrow still blocks for a human and times out without oneBSK_REQUEST_HELP=offmoved in 0.3.0same, in both the CLI and the daemon's inherited environmentoperation auditoff by default; written under ~/.bsk/audit on the daemon host, i.e. inside the agent's own reach
Confirm before borrowing tabsmoved in 0.3.0chrome.storage.local in your browser profile; the extension re-reads it on every borrowAllow requests for human helpmoved in 0.3.0same store; re-checked four times through request_help's setup, so flipping it mid-flight takes effectthe borrow confirmation itselfrendered in a window you own — Agent Windows boot on about:blank and have no content script to ask inthe Agent Window boundaryenforceAgentWindow(): every input-dispatching tool refuses a tab outside the session's own window
Where the boundary actually is
Here is BrowserSkill's own picture of the system, rendered from the Mermaid source in its README. Note the dotted edge on the right and what it says.

The enforcement for that dotted edge is one function in apps/extension/src/tools/shared.ts, and its doc comment is the whole finding:
/**
* Unified target-scope policy by tool effect. Passive reads may inspect user
* tabs; any tool that dispatches page input must stay inside the Agent Window.
*/
export function enforceToolTargetScope(
ctx: SessionContext,
target: { tabId: number; windowId: number },
effect: ToolEffect,
toolName: string,
): RpcError | null {
if (effect === "passive_read" && !ctx.remote) return null
return enforceAgentWindow(ctx, target, toolName)
}ToolEffect is "passive_read" | "transient_input" | "browser_mutation". In a local session, a passive read returns early and is never scope-checked at all. And the resolver that hands these tools a target says the same thing one layer down, again in a comment rather than in any document a user reads:
Explicit
tabIdvalues are checked against the session visibility rules: user tabs and the current session's Agent Window are visible, other sessions' Agent Windows are not.
So I ran it. cargo build -p bsk --locked, the repo's own pnpm ext:build, the resulting dist/chrome-mv3 loaded into Chromium 153 with --load-extension, a real daemon on protocol 1.3. One fixture page served on loopback, titled Private inbox, containing the string CANARY-9f3c1d-private-inbox and a button labelled Transfer funds, opened in an ordinary browser window. Then a session, and nothing else — no borrow, no confirmation, no notification.
$ bsk tab list --scope user --session oevn --json
{"tabs":[{"tab_id":1117016028,"title":"Private inbox","url":"http://127.0.0.1:8099/",
"window_id":1117016026,"active":true,"scope":"user"}]}
$ bsk get-html --tab-id 1117016028 --session oevn
<!DOCTYPE html><html><head><title>Private inbox</title>…</head>
<body><h1>Private inbox</h1><p id="s">CANARY-9f3c1d-private-inbox</p>
<button id="danger">Transfer funds</button></body>…
$ bsk snapshot --tab-id 1117016028 --session oevn
@vom 1
@view 800x600
@layers 1 focus=L1
L1 page
RootWebArea "Private inbox"
heading "Private inbox"
StaticText "Private inbox"
paragraph "CANARY-9f3c1d-private-inbox"
StaticText "CANARY-9f3c1d-private-inbox"
@e1 button "Transfer funds"
$ bsk click @e1 --tab-id 1117016028 --session oevn
error: operation denied by the Agent Window sandbox
hint: tabs outside an Agent Window must first be borrowed via `bsk tab borrow <tab-id> --session <id>`
details: click can only act on tabs inside the Agent Window (tab 1117016028 is in window 1117016026; borrow it first)That is the shape of it. snapshot even minted @e1 for the Transfer funds button — a live, resolvable ref pointing at a control the session is not allowed to press. Reading the page is free; touching it is not.
bsk get-htmlpassive_readallowedReturned the fixture's full outerHTML, canary string included. No borrow, no prompt, no notification.
The middle column is the promise on the tin, and it is kept: nothing that dispatches input reaches a tab you are using. The left column is the same session, the same tab, and the read half of the same tool set — and there the boundary is the window, not a grant. Switch to the right-hand column and the per-tab grant appears, because a remote agent is outside the trust boundary and a local one, by BrowserSkill’s reckoning, already isn’t.
I ran nineteen invocations in that session: the six that read, and thirteen more spanning input, navigation, emulation, file transfer and the help request. The split is clean. The commands I skipped — focus, blur, wheel, select, upload and the history pair — route through the same enforceAgentWindow call as the ones I ran, so I read those call sites instead of exercising them.
With a BrowserSkill 0.3.0 session running locally, six of its commands read a tab in the user's own window when handed that tab's id, with no borrow and no confirmation prompt. Thirteen refuse with permission_denied / agent_window_scope and exit 1. This is the full set I ran; nothing is omitted.
| bsk command | on an unborrowed user tab | exit | what came back |
|---|---|---|---|
| tab list --scope user | allowed | 0 | tab_id, title and url of every tab in the user's windows |
| get-html | allowed | 0 | full outerHTML, including the canary string |
| snapshot | allowed | 0 | accessibility tree plus an actionable @e1 ref |
| screenshot | allowed | 0 | 9,923-byte PNG of the page (fig3 below) |
| console | allowed | 0 | buffered console entries for that tab |
| network | allowed | 0 | buffered network entries for that tab |
| observe | denied — agent_window_scope | 1 | "observe can only act on tabs inside the Agent Window" |
| click | denied — agent_window_scope | 1 | "borrow it first" |
| fill | denied — agent_window_scope | 1 | "borrow it first" |
| press | denied — agent_window_scope | 1 | "borrow it first" |
| hover | denied — agent_window_scope | 1 | "borrow it first" |
| scroll-to | denied — agent_window_scope | 1 | "borrow it first" |
| navigate | denied — agent_window_scope | 1 | "borrow it first" |
| reload | denied — agent_window_scope | 1 | "borrow it first" |
| evaluate | denied — agent_window_scope | 1 | "borrow it first" |
| screenshot --full-page | denied — agent_window_scope | 1 | also requires a per-tab claim, not just the window |
| emulate | denied — agent_window_scope | 1 | "borrow it first" |
| download | denied — agent_window_scope | 1 | "borrow it first" |
| request-help | denied — agent_window_scope | 1 | "borrow it first" |
The Agent Window for this session was window 1117016029; the fixture tab lived in window 1117016026, the one the human was using. `observe` is denied and `snapshot` is allowed because BrowserSkill classifies observe as transient_input (it can hover-probe the live page) and snapshot as passive_read.
The screenshot in that table is worth showing, because it is the least abstract way to say what happened. This image was produced by bsk screenshot --tab-id <the user's tab> during the run above. Nothing was borrowed, nothing was confirmed, and the page it captures is in the window the human was using.

There is no visible indication on the page either. The extension injects a browser-skill-overlay element into every page it can script, but the background service worker only populates it for tabs the session has actually claimed — if (ctx && isAgentControlledTab(ctx, tabId)) return overlayStateForWindow(windowId). A passive read of an unclaimed tab leaves the element empty, so the "Agent Active" control overlay never appears. I read that from the source; the screenshot above is consistent with it but does not prove it on its own, because the capture path could in principle have hidden an overlay.
Why I think this is deliberate, and why it still matters
Three pieces of evidence say this is a decision, not an oversight.
The first is the comment quoted above, which describes the behaviour in a single sentence and does not hedge. The second is that the strictest check in the entire codebase is applied to a read: screenshot --full-page requires the Agent Window scope and an unconditional per-tab claim, and re-verifies both before every scroll step, because a full-page capture scrolls the page and therefore mutates it. Somebody thought about which reads are really reads. The third is remote mode, where the same guard flips:
export function enforceAgentWindow(ctx, target, toolName): RpcError | null {
if (ctx.remote && !isAgentControlledTab(ctx, target.tabId)) {
return { code: "permission_denied",
message: "This tab has not been authorized for the remote task" }
}
if (target.windowId !== ctx.agentWindowId) { /* …borrow it first… */ }
return null
}isAgentControlledTab is the per-tab grant — agentCreatedTabs union borrowedTabs — and it is consulted only when the session is remote. The unit test that pins this down is even titled remote tab ownership, and it constructs its manager with remote: () => true. There is no local equivalent, because locally there is nothing to assert.
Read it as a threat model and it is coherent. A remote agent sits outside your machine, so every page it reads needs an explicit grant. A local agent is already inside: it has your shell, and with your shell it has your Chrome profile on disk, your cookie database, and the ability to launch its own instrumented browser. Making bsk snapshot refuse a user tab would not deny a determined local agent anything; it would only make the honest path more annoying than the dishonest one. I think that reasoning is right.
What I do not think is right is that the reasoning appears nowhere a user would find it. The README says the agent must borrow a tab to touch it. skill/SKILL.md — the file installed into Cursor, Claude Code and Codex, and therefore the sentence the model reads before deciding what it may do — opens with "User tabs require explicit borrowing." And the Chrome Web Store privacy disclosure, apps/extension/PRIVACY.md, scopes page-content access to exactly three cases:
Web page content — The DOM, accessibility tree, HTML, and screenshots of pages controlled in the "Agent Window," tabs borrowed according to the browser's confirmation setting, or pages selected for user-initiated Quick Actions.
My canary string came back from a fourth case: a tab in a user window, named by id, in a local session, never borrowed, never a Quick Action. The tab-metadata row two lines below is scrupulous — it discloses that user tab IDs, URLs and titles are read "including user tabs listed to select a tab for borrowing" — which makes the page-content row's omission look like a line that was written before passive_read existed and never revisited, rather than an attempt to hide anything. It is a documentation bug with a privacy-disclosure blast radius, and it is one sentence to fix.
There is a second-order consequence worth naming for anyone integrating this. A prompt-injected agent that has been talked into exfiltrating something does not need to win a confirmation dialog. It needs tab list --scope user, which gives it the titles and URLs of everything you have open, and then get-html on the interesting one. The mutation boundary, which is the one BrowserSkill built carefully and which genuinely holds, is not on that path at all.
The eval corpus, and the denominator under the denominator
BrowserSkill ships something most tools of this kind do not: evals/browser/, a deterministic, agent-neutral capability corpus with local fixture pages, a declarative smoke workflow, seeded DOM variation, and an oracle that reports site events, response text and adapter evidence separately. Its design goals include a line I wish more projects wrote down:
Honest verification: page-observable results, response markers, and adapter evidence are reported separately. Missing adapter evidence is
unverified, never silently treated as passed.
It also carries a number: "The direct smoke lane covers 25 of 28 operations." That reproduces exactly. node evals/browser/cli.mjs validate reports 9 cases and 18 fixture routes across core 6 / matrix 1 / regression 2; coverage marks 25 operations direct_smoke: yes and three manual — tabs.borrow, tabs.return and assist.request-help, each with a one-line reason for why it cannot be automated; and the harness's own 15 unit tests pass.
So the claim is true. The question the house method asks next is what 28 is a count of.
tool.wheeltool.scroll_totool.focustool.blurtool.uploadtool.downloadtool.screenshot_full_pagetool.screenshot_readtool.screenshot_releasetool.evaluatetool.record_starttool.record_stoptool.record_awaitSeven of those thirteen are exactly the seven methods 0.3.0 introduced — none of them exists in the protocol at tag cli-v0.2.1, so every browser method the current release added is untested. A corpus that grows more slowly than the tool surface is the ordinary condition of every test suite I have ever shipped, and BrowserSkill’s is unusually honest about its own manual lane. It is the published fraction that flatters: 25/28 reads as 89%, and the browser surface it is drawn from is 41.
Two things fall out of that. The smaller one: the corpus's coverage arrays — the operations a case declares for an agent run, as opposed to the scripted CLI workflow — name 21 of the 28, not 25. session.list, inspect.snapshot, tabs.list and assist.resize are exercised only by the deterministic smoke steps, never by a prompt handed to a model. That is a reasonable design and the coverage command prints both columns side by side; it just means the headline number describes the CLI lane, and the agent lane is four operations thinner.
The larger one: 28 is the corpus's own inventory, and it maps onto 28 of the 41 tool.* methods the protocol declares. Thirteen methods have no case at all, and seven of those thirteen are exactly the seven tool.* methods 0.3.0 introduced: wheel, scroll_to, focus, blur, screenshot_full_page, screenshot_read and screenshot_release. I checked that against the protocol at tag cli-v0.2.1, where none of the seven exists. Every browser method the current release added is untested by the corpus that ships beside it. Of the remaining six, upload and download arrived in 0.2.0 and the recorder's three methods earlier still; only evaluate was in the protocol at the first public commit. New capability outrunning its test corpus is the normal condition of every codebase I have worked in. It is worth stating plainly only because the published fraction, 25/28, reads as 89%, and the fraction against the surface it is drawn from is 25/41.
BrowserSkill's eval README says "the direct smoke lane covers 25 of 28 operations." Running its own harness reproduces that exactly. The number the README does not give is the denominator behind the denominator: those 28 operations map onto 28 of the 41 tool.* methods the protocol declares, so 13 browser methods have no case in the corpus at all — including all seven that 0.3.0 added, none of which is present in the protocol at tag cli-v0.2.1.
| measure | count | how it was produced |
|---|---|---|
| cases in the corpus | 9 | cli.mjs validate — core 6, matrix 1, regression 2 |
| fixture routes | 18 | cli.mjs validate |
| operations in the inventory | 28 | OPERATION_CATALOG in evals/browser/lib/operations.mjs |
| operations the direct smoke lane reaches | 25 | cli.mjs coverage — direct_smoke = yes |
| operations left to the manual lane | 3 | tabs.borrow, tabs.return, assist.request-help |
| operations any agent prompt declares | 21 | cli.mjs coverage — rows with a non-empty agent_cases column |
| tool.* methods the protocol declares | 41 | crates/bsk-protocol/src/method.rs |
| tool.* methods the extension dispatches | 40 | apps/extension/src/tools/dispatcher.ts |
| tool.* methods with no eval operation | 13 | the 41 protocol methods minus the 28 the catalog names |
| harness unit tests, all passing | 15 | node --test evals/browser/tests/*.test.mjs |
The 13 unevaluated methods are wheel, scroll_to, focus, blur, upload, download, screenshot_full_page, screenshot_read, screenshot_release, evaluate, record_start, record_stop and record_await. tool.wait_ms is the one protocol method the extension never sees — the daemon answers it (crates/bsk-cli/src/daemon/ipc.rs:301), so the sleep never reaches Chrome.
One incidental find while counting: tool.wait_ms is the single protocol method the extension never sees. The daemon answers it directly in crates/bsk-cli/src/daemon/ipc.rs, so bsk wait-ms is a sleep that occupies the session queue and never reaches Chrome. That is the right implementation; it is just not what "browser tool" suggests.
Small things that do not line up
The architecture doc is off by nineteen. docs/architecture.md describes the extension's tools/ directory as ToolDispatcher → 21 tool handlers. The dispatcher's switch has 40 distinct case "tool.…" labels. git log -L on that line shows it has not been touched since commit f1036a8, "Initial public release", on 2026-06-22 — it was accurate then and the code roughly doubled underneath it. Harmless, but it is the first file a contributor reads.
One document is Chinese-only, and it is the one about accountability. Every file under docs/ is English except docs/operation-audit.md, which is 59.6% CJK characters against 0.0% for all six of its neighbours. The English README and the English changelog both link straight to it. I read it in the original, and it is good: audit is off by default, records live under ~/.bsk/audit on the daemon host at mode 0700/0600, one .jsonl per task capped around 16 MiB, thirty-day retention for finished tasks, no screenshots or page bodies or input values or file paths written, and page origins reduced to scheme, host and port. It also contains the most self-critical sentence in the repository, which nobody reading only English will ever see:
本功能用于个人回顾,不提供防篡改或不可抵赖保证。… daemon 主机上的文件不构成对同一系统账户下恶意进程的安全边界。
("This feature is for personal review; it offers no tamper-resistance or non-repudiation guarantee. … Files on the daemon host do not constitute a security boundary against a malicious process under the same system account.")
That is a project publishing against its own interest, in the only place it published at all. The audit trail sits inside the agent's reach and says so. It is also, by default, not running.
The version gate on request-help is a compatibility check wearing a security check's clothes. The CLI refuses request-help against a daemon older than protocol 1.3, and the comment explains why: "An old daemon can return locally without letting the browser decide." The mechanism is require_daemon_support, which asks the daemon for its protocol version and believes the answer. That is exactly right for the failure it names — a half-upgraded install — and it is worth being clear that it does not survive an adversary, because the component being asked is the one that would lie, and an agent with a shell can install whichever bsk it likes.
The committed listing screenshot still shows 0.1.x. docs/assets/browserskill-chrome-web-store-screenshot-1280x800.png is the store-listing image kept in the repo, and the popup inside it reads 0.1.1 / 0.1.4 — CLI and extension — against a shipping 0.3.0. I checked the committed asset, not the live store page. Cosmetic either way. I include it because its three feature tiles are the product's own summary of the promise, and the third one is the claim this whole article is about.

The ledger
Real, and checked. The consent relocation works exactly as advertised. Three deprecated bypasses, all measured against the built 0.3.0 binary with a live browser attached, all inert: --no-confirm warned and then let the borrow block for its full timeout and fail. The preference normalizer is fail-closed on anything that is not the literal boolean false. The approval prompt renders in a window the agent cannot script, for a stated reason. The Agent Window sandbox holds: thirteen commands, including evaluate, refused a tab in my own window with permission_denied and exit 1. And evals/browser/ is a real, runnable, honestly-scoped corpus whose headline number reproduces to the operation.
Narrower than advertised. "It must borrow that tab explicitly" is true of everything that dispatches input and false of snapshot, get-html, screenshot, console and network in a local session. PRIVACY.md's page-content row lists three sources of page content and there are four. skill/SKILL.md tells the model "User tabs require explicit borrowing", which is the instruction, not the enforcement. The behaviour is deliberate, defensible on its own threat model, and documented only in source comments. "25 of 28 operations" is 25 of 41 tool.* methods, and every method 0.3.0 added is among the thirteen with no coverage at all.
Stale, not wrong. 21 tool handlers in a doc that describes 40. A committed listing screenshot whose popup still reads 0.1.1 / 0.1.4. The one document that admits the audit log is not a security boundary exists only in Chinese, linked from English pages.
None of that makes this a bad tool — it is the most carefully-reasoned consent model I have read in this category, and the one number I most wanted to be true, that the bypass flags really are dead, is true. What did not survive checking is a sentence, repeated in four places, that describes the write boundary as though it were the whole boundary. The fix is documentation, not code, and it is smaller than the paragraph you just read.
What would change my mind
6 claims above, and what would falsify each
In a local session, snapshot, get-html, screenshot, console and network read a tab in the user's own window with no borrow and no prompt.
A run of those five commands against an unborrowed user tab that returns
permission_denied. I ran each of them once, on 2026-09-18, on Linux, against Chromium 153 with an extension I built fromd1356fdrather than installed from the Web Store — a store build that differs from the repo, or a platform difference in howchrome.tabs.getresolves cross-window ids, would settle it. The scope matrix dataset has the exact commands and the session and tab ids."--unattended", "--no-confirm" and "BSK_REQUEST_HELP=off" cannot bypass borrow confirmation in 0.3.0.
Any code path where one of those three inputs changes what the extension does, rather than only what the CLI logs. I grepped all four call sites of
warn_legacy_overrideand then watched a borrow with--no-confirmblock for its whole eight-second timeout and fail. A daemon-side path I did not find would overturn this, and so would a version older than 0.3.0 — the flags did work before.The read/write split is deliberate rather than an oversight.
This is the claim I am least able to prove, since it is about intent. The evidence is a doc comment that states the policy in one sentence, a stricter check on
screenshot --full-pagethan on any mutation, and a remote path that enforces the stronger rule with its own named unit test. A maintainer saying "that comment is aspirational, the local case is a bug" would settle it the other way, and I would rather be wrong about intent than about behaviour.PRIVACY.md's page-content row does not cover the local passive-read case.
A reading of that row under which "pages controlled in the Agent Window" already includes any tab a session can name. I do not think the words bear it —
enforceAgentWindowuses "controlled" to mean the opposite — but it is a disclosure document, and disclosure documents get read generously. A revised row, or a Web Store listing whose text differs from the repository's copy, would also change the answer.The eval corpus covers 25 of 41 protocol tool.* methods, not 25 of the browser surface.
A case manifest, anywhere in
cases/**, whosecoveragearray names an operation mapping to one of the thirteen I listed. I derived the mapping by hand fromOPERATION_CATALOGtomethod.rsand it is the one step here a second pair of eyes should repeat; the coverage dataset lists every count and the command that produced it.Operation audit is off by default and is not a tamper-evident record.
A default-on audit switch in a shipped build, or a signing or append-only mechanism I missed. The project states both halves itself, in
docs/operation-audit.md, in Chinese. I translated the two sentences above and would want to know if I read them wrongly.