Skip to main content
Glama

Aperture

An AI-native, privacy-first browser. The primary user is an agent; the human is the one who stays in charge.

Status: early but real. The browser runs, the MCP server works, and Claude Code can drive it end to end — snapshot, act, diff, autofill, capture. The diff engine has been measured against a real competitor over a scored, preregistered head-to-head, and it won the cost primary and lost the precision primary. The vault has a working UI, tested crypto, and PSL-backed origin binding; its MCP fill path deliberately refuses rather than pretending. See What has been measured for the numbers and their scope, and Honest status for what is not done — nothing below is claimed as working unless it is marked working.


Why build this

Every "AI browser" shipping today — Dia, Comet, Neon, Edge Copilot Mode, Brave Leo — is a chat sidebar bolted onto Chromium, driving a model the vendor chose. Aperture inverts that: it is a browser whose control surface is an MCP server, so you bring the agent. Claude Code attaches to a browser you are already using, with your sessions and your logins, and you watch it work.

Opera Neon shipped an in-browser MCP endpoint in March 2026, so the idea is validated. Two things are still unoccupied: nobody has built this on Electron, and nobody has solved incremental page observation, which is where almost all the token cost in agentic browsing actually goes.

Related MCP server: Scout

The core idea: diffs, not re-dumps

Every browser-MCP today (playwright-mcp, browser-use, chrome-devtools-mcp) re-serializes the entire page after every single action. Aperture treats the loop as act → observe delta: browser_act returns what changed, against a page state the model already holds.

For scale, from npm run bench:live on four real sites: an Aperture full snapshot of Hacker News is 9,519 tokens (233 refs), a GitHub repo page 5,269 (100), a Wikipedia article 6,197 (209), an MDN reference 7,102 (163). An observation that reports nothing changed costs ~112–115 tokens on any of them. That gap is the whole design.

Making the gap safe rather than merely small is the actual engineering:

  • Stable refs. e42 names a logical element, not a DOM node, so it survives a React re-render that replaced every node. Identity is data-testidname → non-generated id → role + accessible name + nearest named ancestor + semantic path. Framework-generated ids (:r1:, hash suffixes) are rejected, because keying on them would make every ref unstable.

  • Keyed reconciliation, not tree-edit-distance. Identity keys turn matching into a hash lookup, so diffing is O(n), with a longest-increasing-subsequence pass so "one row jumped to the top" is one op, not twenty.

  • Explicit resets. Diffs name the state they apply to (diff from #7.3). On navigation, on >30% change, or after 12 diffs, the engine emits a full snapshot headed FULL SNAPSHOT — replaces all prior state, which tells a model whose context was compacted to discard its mental model. The fallback thresholds are reasoned choices, not tuned values — nothing has measured them against alternatives.

  • Explicit retirement. A replace op carries a gone: list naming every ref it destroyed, and a - gone: op reports deaths that have no addressable root to hang off. Restating a subtree without saying what died is how an agent ends up believing in elements that no longer exist.

  • Noise suppression. A clock is recognized by shape and suppressed — even mid-task, when every observation follows an action (this specifically did not work until it was benchmarked: in an act-observe loop the statistical demotion path can never fire, and the shape path was gated behind it). Anything changing repeatedly on its own is demoted; the element the agent acts on or reads is promoted straight back. Without this, one ticking timestamp defeats the entire diff argument.

  • Positional fallback — the known weak point, and it has now cost us a benchmark. Elements distinguishable only by position (ten identical "Add to cart" buttons, a queue of identical rows) get a document-order ordinal appended to their key. Reordering is exactly what breaks positional identity. In the head-to-head this was measured in the field: under row removal those ordinals re-key to positions, a plan captured before the removal executes one row off, and the click lands. See Precision — it is the one preregistered primary Aperture failed, and it is being fixed.

  • Ref discipline. Only actionable elements get refs. On the four real sites above, Aperture's full snapshots are 1.25×–2.77× smaller than @playwright/mcp's of the same URLs, averaging ~1.9×. That is a per-snapshot byte comparison and nothing more — per-observation is not per-dollar, which the campaigns below spent three waves learning.

What has been measured

Four scored campaigns with preregistered rules, plus the engine's own fidelity benches. Every claim here is quoted at the scope its verdict grants. The measurement detail, the corrections, and the failed analyses live in bench/RESULTS.md and the adjudications in docs/design/{wave3,sweep,h2h}-evaluation.md.

Cost, against the real incumbent

On a 13-task benchmark against Playwright MCP 0.0.78, both products sealed to an identical three-tool surface, driven by claude-sonnet-5 (385 episodes; design preregistered in docs/design/headtohead.md, adjudicated in docs/design/h2h-evaluation.md):

  • On realistic-weight pages — the preregistered neutral fixtures, 5.5–6k Aperture tokens and ~22k tokens in Playwright's dialect — end-to-end agent cost was 0.31× Playwright MCP's [0.27, 0.36], with the saving attributable to observation bytes.

  • On the small adversarial home fixtures Aperture was 1.30× DEARER [1.04, 1.59], paid in generation-side tokens (6.3k vs 3.1k output tokens per episode at equal turns) — the bookkeeping tax, billed where earlier waves predicted it. On small neutral pages the difference is null (0.957× [0.90, 1.02]).

Where the crossover sits, from a page-size sweep on one task at the same engine stamp (54 episodes, full-snapshot weights 1,116–38,081 chars): the diff arm is never significantly dearer at any measured size, and is significantly cheaper from ≈10k chars (≈2.5k tokens) up — 19% / 39% / 43% cheaper per episode at the top three rungs, growing monotonically with page weight. At the two smallest rungs the intervals include zero, so the sweep neither confirms nor refutes the +4–6% small-page premium that the earlier waves measured; it caps any such premium at +20% of episode cost. One task, one model, synthetic inert padding.

Correctness, and the primary we lost

  • Reliability (head-to-head primary): the −10pp non-inferiority bound HOLDS — +7.3pp [−3.8, +18.2] for Aperture-diff over sealed Playwright, pooled over all 13 tasks. The delta is carried entirely by one task (catalog-order, where sealed Playwright scored 0/10 and every other arm 100%); excluding it, the delta is −2.0pp [−13.1, +9.1], which straddles the bound — inconclusive at this sample size. On the disclosed-adversarial home set alone the incumbent led on success, 82% vs 76%.

  • Precision (head-to-head primary): the +0.2/run wrong-element bound FAILS. +0.173 [0.018, 0.345] pooled; +0.380 [0.060, 0.740] on the home set, where all of it lives. The cause is our engine, not the diff mechanism (diff − re-dump −0.10 [−0.36, +0.15]; re-dump − sealed +0.27 [0.08, 0.49]): on re-rendering identical-row lists Aperture's persistent ordinal refs re-key to positions under row removal, so a stale plan lands one row off, silently, where Playwright's per-snapshot refs error out (75 refused dead-ref acts, 9× Aperture's). This is a correctness hazard, not a cost — the wrong actions mutate real state. The fix cycle is owed and a fresh cohort must measure it before this sentence can be retired.

  • Diff bookkeeping penalty (wave 3, our own suite): none found at the stated size. On a 3-task positional-identity suite with claude-sonnet-5, no diff-bookkeeping penalty larger than 10pp in task success or +0.4 wrong-element actions per run was found (n=105/arm). The smallest true drop this run could distinguish from that bound is ~23.5pp; anything subtler is invisible to it. Point estimates favour diffs, but every interval includes zero — direction, not a finding.

  • Sealing the incumbent cost it capability, and that matters more than our win. Stock Playwright MCP with its full default surface (code-execution, network-inspection and screenshot tools disabled) outscored its own sealed configuration 89% to 74%. The sealed comparison understates the incumbent, the stock numbers are the deployment-relevant ones, and stock Playwright remains the stronger choice where its full surface is acceptable.

Scope, and it is narrow. One model (claude-sonnet-5). Our fixtures — synthetic, static, logged-out, no anti-bot, no iframes, no auth. MCP mode only: Playwright's own recommended CLI/skills mode, which its README concedes is the token-efficient path, is unmeasured, and if that mode wins the economics then MCP-vs-MCP was the wrong fight. Every Playwright episode ran branded Chrome 150.0.7871.187, not the pinned chromium build the spec named — that build cannot spawn on this machine, so the pinned browser never ran (Aperture ran its own Electron-bundled Chromium, as always). Sealed Playwright ran with codegen off, which makes key/scroll/type-without-submit return zero bytes — its shipped conduct, disclosed in the shared tool description. Sonnet has trained on Playwright's dialect and never on Aperture's; that asymmetry is unclaimable in either direction. The suite's own report exits non-zero on this store (a tripwire fired on catalog-order; the investigation is complete and ruled a genuine product difference), so the verdict was computed out of band in the adjudication. The full fourteen-item disclosure block is in bench/RESULTS.md.

The mechanism, on our own bench

  • Diff fidelity: five scenarios GREEN (npm run bench:fidelity) — typing with a mid-run resync, full DOM teardowns, clicks and state flips through a shadow root with a ticking clock suppressed, mass ref death and revival through the size-cap resync, and native <select>s plus a custom ARIA combobox. Vacuity guards mean a run that measured nothing exits without printing a verdict at all. What a green licenses: the diff stream is complete and unambiguous for a mechanical rule-following reader — if a real agent's model drifts, the fault is its bookkeeping, not missing information in the stream. It does not prove an LLM does that bookkeeping correctly, and it does not check containment or position: a stream that reordered the world would still pass.

  • Refusals and retractions: 11/11 (npm run bench:guards), judged against the fixture's own change-event log rather than Aperture's own report, because an error: reply is not evidence that nothing was written. The same probe scored 1/11 on the build immediately before the fixes.

  • Ref stability across a re-snapshot: 100% on the four real sites, including one whose content changes underneath. Through a full re-render refs survive when the element has a distinguishing name and fail when siblings are identical — the positional hazard above, measured in the lab before the head-to-head measured it in the field.

  • The synthetic token model (npm run bench) puts observation cost at 6.6×–10.2× lower in diff mode over a 20-action sequence, rising with page size. It is a model of one fixture family, not a measurement of anything an agent did, and the campaigns above supersede it for any deployment claim.

An earlier version of this file claimed ~50×, then ~40×, from design targets nobody had measured. Both were wrong, in the same way: neither accounted for fixed per-response overhead, and neither counted the turns an agent spends deciding. The envelope overhead has since been cut from 420 to 104 chars — 79 tokens per response, reconciled to the byte — and the honest headline is the one above, which is smaller than the first draft and better evidenced.

What none of this settles

  • Live websites. Every scored fixture is synthetic. No anti-bot, no A/B drift, no iframes, no auth.

  • Other models. One model throughout. A model that reads 22k-token dumps reliably, or one that cannot read 6k, moves every headline number.

  • The truncation regime — an agent on a page bigger than its budget. That is the default product experience, and the sweep priced the enabler world instead.

  • Hard tasks on big pages. The one unmeasured quadrant of the cost picture, and the one place the sweep's flat voluntary-observation residue could plausibly break.

  • Long horizons. Everything is ≤16 actions with budgets that always fit.

  • Structure, containment, position, iframes — outside what the fidelity bench calls faithful.

Small tool surface, deliberately — with a measured counterweight

Every registered MCP tool costs schema tokens before it does anything, and playwright-mcp (~50 tools) and chrome-devtools-mcp (51) levy that tax on every session. Aperture ships 14, kept down by putting related operations behind an action discriminator rather than splitting them into a tool each:

browser_tabs      browser_navigate   browser_snapshot   browser_read
browser_act       browser_fill_form  browser_profile    browser_attach
browser_capture   browser_container  browser_theme      browser_console
vault_entries_for_origin             vault_request_fill

browser_act covers click, type, clear, hover, scroll, key and select.

The counterweight, because it was measured against us: in the head-to-head, giving the incumbent more tools made it better, not worse — stock Playwright beat its own three-tool sealed configuration by 15 points, and the dividend was concentrated exactly where a scoping affordance was missing. A small surface is a real token saving and a real capability cost, and this project has now paid the second half.

Note what is absent: there is no vault_reveal, no vault_unlock, no vault_export, and no tool that reads the filesystem. Those capabilities do not exist on this surface at all — see below for why that is the enforcement mechanism rather than a policy.

Autofill: the thing that makes this useful daily

A job application asks for the same twelve facts the last one did. Aperture stores them once, and the agent proposes filling them:

profile "Demo" · 11 of 12 fields ready

e3  "First name"             → givenName: Brad
e5  "Email address"          → email ~85%: brad@example.com
e8  "Apartment, suite, etc." → addressLine2: —  SKIP: no-value
e16 "Date of birth"          → dateOfBirth ~85%: (from profile — value not shown)

Calling apply then raises a native OS dialog naming the origin and the fields. The agent cannot render it, see it, click it, or pass a parameter that skips it, and sensitive fields never ride on a prior grant.

That gate used to be a sentence in the tool description asking the agent to check with the human — which made the approval the agent's judgement, and the agent is exactly the component we assume a hostile page can steer. The vault got origin binding with no override while profile autofill got a polite suggestion. That inconsistency was the worst thing in this codebase and is now fixed.

Note what is not in the plan above: "Why do you want this role?" is a prose question, not an identity field, and matching a stray keyword inside one is how you end up submitting "Director" as an essay answer. Free-text prompts are excluded by shape.

Matching uses the HTML autocomplete attribute first — it is a standardized declaration of exactly what a field wants, and a surprising share of real forms set it correctly — then label heuristics. Low-confidence matches are reported but not filled: silently putting a wrong value in a field the human then submits is worse than leaving it blank.

Attachments (CV, cover letter, portfolio) work the same way, with one hard constraint: the agent picks files by id from a library the human curated, and cannot pass a path. "Attach my CV" and "read any file on this machine and upload it somewhere" are the same primitive if the agent controls the path — so it doesn't.

This is also the honest answer to CAPTCHAs (see below): the human proves they're human, the agent does the typing. No evasion involved, which is exactly why it keeps working.

Dark mode

Three layers, because no single one covers every case:

  1. Tell the site the truthprefers-color-scheme reports the real preference, so sites shipping their own dark theme use it. Always better than anything synthesized.

  2. Chromium force-dark for sites without one. It works on the rendered layout tree and runs in the compositor, so it costs nothing per frame. Applied per-tab via CDP Emulation.setAutoDarkModeOverrideverified working on Electron 43, which is what makes per-site control possible at all (the Blink setting alone is process-wide).

  3. Filter inversion as fallback and for brightness/contrast, which force-dark doesn't expose at runtime.

Per-site policy is Auto / On / Off, where Auto measures the page's background luminance and skips sites that are already dark.

The filter fallback and the control model are adapted from Nightfall — specifically its counter-inversion set with the nesting guard, which stops a photo inside an already-counter-inverted container coming out net-inverted. An extension can't reach layer 2; owning the browser is the only way to get it.

Privacy

  • Identity containers — isolated cookie jar, cache, and storage per container, with a per-container fingerprint seed. Containers cannot be merged and sites cannot be moved between them from the agent surface: a page that could talk the agent into merging two containers defeats the isolation in one move, so that stays a human decision.

  • Fingerprint consistency over randomness. Randomizing canvas noise per load makes you more identifiable — real browsers are boringly stable, so a machine whose fingerprint changes every load is wearing a sign. One seed per container, every surface derived from it, frozen while the container holds state. (The seed exists and is stable; the per-surface derivation is not yet applied — see Honest status.)

  • Tracker blocking via Ghostery's compiled engine (EasyList/EasyPrivacy scale), because an agent makes far more requests per minute than a human and per-request matching cost is on the hot path.

  • Chromium's phone-home features (variations, domain reliability, autofill server) are switched off at launch, and WebRTC is prevented from leaking the local IP.

The password vault is agent-blind by construction

We are building a password manager into a browser an agent can drive. That creates an unusual requirement: the agent must be able to cause a login without ever being able to read the credential. Agent context goes to a model API, gets logged, and may be summarized or persisted — treat a credential entering it as a full compromise.

Two properties do the work, and both are structural rather than procedural:

  1. No getter exists. Not disabled, not flagged — absent. vault_request_fill performs the insertion and reports which fields it touched. A getter that exists can be called, and anything the agent can call, a hostile page can talk it into calling.

  2. Unnameability. vault_entries_for_origin returns only entries matching the page's own origin. On evil.com the agent is never told a google.com entry exists, so a prompt injection has no identifier to weaponize. Removing the vocabulary beats blocking the action.

Origin mismatch is terminal — no force flag anywhere in the surface. The agent is precisely the component we assume is manipulable; giving it an override reduces the design to the agent's judgment under adversarial input.

Crypto: Argon2id (moderate limits, calibrated) → XChaCha20-Poly1305. The 192-bit nonce means random nonces are safe without counter state.

Taint redaction is best-effort, not a guarantee. Mirrored values are caught by exact substring match, so a reformatted date (fill 1990-01-05, page echoes January 5, 1990), a case change, or a value split across text nodes all defeat it — and it over-redacts, turning every "Anna" on the page into a marker if that is your first name. It raises the cost of an accidental echo; it is not a boundary. The boundaries are origin binding and the process split.

What this does not claim: a page can read back its own DOM, so a password delivered to an origin is a password that origin has. A password manager's real guarantee is correct routing, not secrecy from the site. Passkeys are the actual fix; every password in a vault is technical debt.

The vault window

The password manager is a separate window with three properties that are structural rather than policy:

  1. The agent cannot address it. TabManager never learns about it, so it has no tab id — and every agent action routes through TabManager by id. There is no argument the agent could pass to reach it. Not a blocked one, an unrepresentable one. Verified: with the vault window open, browser_tabs lists only the browser tab.

  2. It is excluded from capture. setContentProtection(true) maps to WDA_EXCLUDEFROMCAPTURE on Windows, so screenshots and screen shares see a blank region — including the agent's own capture tool. This is asserted by the code and not yet verified by a probe on the deployment OS; it is queued.

  3. Its IPC is separate and sender-checked. The vaultui: channels verify the caller is the vault window on every call, so knowing a channel name is not enough to use it.

revealForHuman is the one function in the codebase that returns a plaintext password. It is safe only because of where it can be called from, and it must never grow a caller outside that window.

Honest limitation: the security design calls for this to run in its own OS process (hardened: its own low-privilege account). It currently runs in the browser process. That is sound against the threat that actually matters — a hostile page steering the agent — and not sound against local code execution as the same user. That is the T-Base tier in docs/design/security.md.

Two-factor codes

The vault stores TOTP seeds and shows live codes with a countdown. No registration, no accounts, no network calls — TOTP (RFC 6238) is an offline standard, and "Google Authenticator" is just one client for it. Paste what the site prints next to its QR code. Verified against the RFC's own test vectors.

On using 2FA to unlock the vault itself: don't bother, and here's why. The vault is encrypted with a key derived from your passphrase. A TOTP check would be an if statement in the UI — and an attacker with the vault file ignores the UI entirely and attacks the passphrase. Worse, verifying codes requires storing the seed in the vault, so it is available to exactly the attacker it claims to stop. A second factor only means something at rest if it contributes key material, which is what a TPM or Windows Hello does and what TOTP structurally cannot. Storing codes for other sites is genuinely valuable; gating the vault with one is theatre.

Crash reporting (off by default)

Reports go to your own uh-oh instance, and only if you turn it on.

A browser is the worst app to wire crash reporting into carelessly, because the crash context is the sensitive data: URLs are browsing history, titles are page content, stack traces can carry form values, and this process holds the MCP token and your profile. So:

  • Allowlist, not denylist. The event is rebuilt from an empty object with only named fields copied in — a delete pass would leak whatever the SDK adds in a future version.

  • URLs become a salted hash of the origin. You get "these crashes cluster on one site" without recording which site. The salt is per-install, so hashes can't be compared across users or matched against a list of popular domains.

  • Your home directory is stripped from file paths, since it usually contains your real name.

  • The vault reports nothing. Not scrubbed — excluded, by tag and by any vault module appearing in any stack frame. False positives cost a diagnostic; a false negative costs a credential.

  • Fail closed. uh-oh's client documents that a beforeSend which throws results in the event being sent unmodified — backwards for a scrubber — so ours never throws and returns null on any failure.

  • A final independent gate re-scans the serialized payload and drops the whole event if a secret survived, precisely because it doesn't depend on the structural pass being correct.

A dedicated envelope suite covers this, including "an unknown top-level field is dropped" and "a malformed event fails closed."

Verify it end-to-end, not just in unit tests. npx electron . --test-crash sends a probe error deliberately containing a URL, an email, a bearer token and your home path, so you can inspect what actually landed on the server. That flag exists because the unit tests were green while the pipeline was completely broken — three separate ways:

  • The scrubber rebuilt events against a shape I had assumed rather than the real EventEnvelopeSchema (stacktrace is a flat array, not {frames}; mechanism is required; breadcrumbs use ts, not timestamp). The server 400'd every event. The tests passed because they used the same invented shape as the code did.

  • That same wrong shape meant originatesInVault found no frames — so the vault exclusion, the strongest claim in this section, silently excluded nothing.

  • With events finally arriving, the leak audit found the username in every stack frame: real frames are file:///C:/Users/name/… with forward slashes, while the stripper matched os.homedir()'s backslash form.

All three are fixed and now covered, including an assertion that scrubbed output still satisfies the wire contract. The lesson is worth keeping: a test that shares the code's assumptions validates the assumption, not the behaviour.

Capture → Notion

One button, on a fallback chain that matches what you actually mean:

  1. A Notion page is open in a tab → append the capture there.

  2. Notion configured but no page open → append to today's dated page.

  3. Otherwise → write a PNG to Pictures/Aperture.

Every step falls through on failure, and step 3 cannot fail short of a full disk. Losing a screenshot because an API call returned 400 would be the worst outcome for a button whose entire job is "keep this".

The capture is filed, never returned to the agent's context — invisible text in a screenshot is a known prompt-injection vector against vision models.

The Notion API path is unverified. Two pages of Notion's own docs disagree on the field names for file uploads, and I had no workspace token to test against. It is written to fail loudly and fall back to disk. Validate it before trusting it; the token goes in the vault window's Notion tab, never through the agent or a chat message.

Prompt injection is the defining threat

Brave demonstrated hidden text in a Reddit post making Comet exfiltrate a user's one-time passcode, and invisible text in screenshots steering a vision model. This is not patchable at the model layer.

Aperture's response is structural: every tool result carrying page text is wrapped in an <untrusted-page-content> envelope whose delimiters carry a fresh per-call random nonce that is stripped from the body — so the closing tag cannot occur inside the content no matter what the page writes, by construction rather than by the nonce staying secret. What the envelope means is stated in the tool descriptions, not repeated in every response: clients re-send tool descriptions on every request, so that explanation survives context compaction. Harness speech never appears inside an envelope, and page bytes never appear outside one — including browser_tabs' list of page-authored tab titles and browser_fill_form's page-authored field labels, both of which reached the agent bare until an audit caught them. The snapshot format reinforces it: all page-authored text is quoted and control/bidi characters are stripped, so a page cannot emit text that parses as snapshot structure or forge a FULL SNAPSHOT header.

About CAPTCHAs — read this before expecting Camoufox behavior

The original goal here was "avoids CAPTCHAs like Camoufox." Having researched the 2026 state of the art, that premise does not hold, and building toward it would be building the wrong thing.

CAPTCHA decisions are made across four layers:

Layer

Weight

Where Aperture stands

IP / ASN reputation

hard gate

Your proxy choice; nothing a browser fixes

TLS (JA3/JA4) + HTTP/2

hard gate

Free — a real Chromium gives a real Chrome fingerprint

Automation-protocol tells

high

Avoid CDP Runtime.enable; use isolated worlds

Browser fingerprint

consistency tax

Containers (above) get you to unremarkable, not good

Behavior

the decision variable

The hard part, and not a browser problem

Camoufox is excellent at the fingerprint layer and its own docs call the behavioral layer work-in-progress. But Cloudflare shipped Precursor on 2026-07-13: continuous, session-scoped behavioral scoring explicitly built to detect agentic behavior, which survives page refresh. Cloudflare names "mathematically perfect Bézier curves" as a bot signal — indicting every humanization library by technique.

And an LLM agent has a signature no browser fork can hide: bursts of perfect action separated by multi-second silences while the model thinks.

The strategically correct answer in late 2026 is the opposite of hiding. Web Bot Auth (RFC 9421 HTTP message signatures) lets an agent cryptographically identify itself and be allowed. From 2026-09-15 Cloudflare blocks Agent-class traffic by default on ad-monetized pages for newly onboarded domains, with bot operators enrolling as signed agents through its dashboard.

That date is not Aperture's deadline, and chasing it would be a mistake. Enrollment as deployed assumes a hosted agent whose requests egress from operator infrastructure holding the operator's private key. Aperture is a local personal browser: a project-level key would ship inside every install, i.e. be public, i.e. be worthless — anyone could sign as "Aperture", and the first abuser burns the key's reputation for everyone. There is no sound custody story for a registered project key in a client-distributed browser today. So the plan is the capability, not the registration: per-install Ed25519 signing, offered per agent session, off by default, signing only requests attributable to the agent and never the human's own browsing — because marking the human's traffic with a bot signature inverts the product's privacy premise. See docs/design/tier2.md §7.

Aperture is built for your browsing: your accounts, your sessions, your automation. It is not a mass-evasion tool and will not be pointed in that direction.

Honest status

Area

State

Electron shell, tab model (WebContentsView), browser UI

Working — launches and browses

MCP server over Streamable HTTP, 14 tools

Working — verified against a live browser

Bearer auth + DNS-rebinding guards

Working — verified (401 / 403)

Untrusted-content envelope

Working — verified in both directions by bench:live on every site: page bytes always inside, Aperture's own ok … always outside

Snapshot engine end-to-end (walker → refs → diff → render)

Working — verified on real sites and five fidelity fixtures

browser_act (click/type/clear/hover/scroll/key/select)

Working — trusted CDP input for pointer/keyboard, isolated-world setter for select; returns a diff

Input witness (act acknowledged ⇒ input actually reached the page)

Working — covers targeted acts plus scroll and key; unknown never fails an act, and a page that self-navigates mid-settle is invisible to it

Diff fidelity — typing, re-renders, clicks/state flips, shadow DOM, both resync fallbacks, native + ARIA selects

GREEN across five scenarios (npm run bench:fidelity), with vacuity guards so an empty run cannot score

Refusals and retractions (disabled, obstructed, blank-query, bounded error text, option-list retraction)

GREEN — 11/11 (npm run bench:guards), judged against the page's own event log

Ref stability across a re-snapshot

Measured: 100% on four real sites

Ref survival through a full re-render

Measured: survives for named elements; FAILS for identical siblings

Positional refs under row removal

Measured, and it lost a preregistered primary — stale plans land one row off. Open engine defect; see docs/HANDOFF.md

Positional refs under row insertion

Fixed — a positional family that gains a member escalates to a full replace

Cost vs Playwright MCP

Measured — 0.31× on realistic-weight pages, 1.30× dearer on small ones; see above for scope

Task success on diffs vs full re-dumps

Measured — no penalty larger than 10pp found on our positional-identity suite; ceiling-free but with a ~23.5pp resolution floor

Page-size cost crossover

Measured — diffs never significantly dearer, significantly cheaper from ≈10k chars up; lower edge unresolved

Autofill: profile matching, plan/apply, sensitive-field redaction

Working — verified end-to-end

Autofill consent gate

Working — native OS dialog the agent cannot render, see, click, or bypass

Dark mode (per-tab force-dark + per-site policy)

Working — CDP override verified on Electron 43

Password manager UI

Working — content-protected window, entry CRUD, reveal with auto-hide, generator, identity + attachment + Notion editors

Vault MCP fill path

Refuses deliberately — crypto, origin binding (bundled PSL) and API shape done; the insertion path is not wired and says so

2FA (TOTP)

Working — verified against the test vectors in RFC 6238

Capture → Notion

Working; disk fallback verified. The Notion API path is unverified

Crash reporting to uh-oh

Working — verified end-to-end against a live server, payload audited for leaks. Off by default

Attachments (CV upload via DOM.setFileInputFiles)

Built; library is human-curated. Multi-upload forms need the ref→node bridge

Tracker blocking

Wired; not yet measured

Identity containers

Sessions, partitions and a stable per-container seed working; per-surface fingerprint derivation not applied

inert / pointer-events: none / small modal dialogs

Known gap — only :disabled and the covering-overlay hit-test are enforced

Structure/containment/position fidelity, iframes, model-side budget truncation

Not measured by any benchmark

Live-web behaviour

Not measured — every scored fixture is synthetic

Extensions

Not started — see below

A bug worth recording, because testing caught it and the design predicted it. The first end-to-end autofill run filled the date of birth through the agent-blind path, and then leaked it straight back out in the next snapshot — because the walker reads input values from the DOM. Filling correctly is only half the job: the value now lives in the page, and the agent has tools that read the page. Fixed with a per-tab taint set that redacts those fields before anything downstream sees them, cleared on navigation. The redaction is a fixed marker rather than a length-accurate mask, because length is real information about a secret.

Known risks, stated rather than buried:

  • The removal-side positional-ref hazard is live. On a re-rendering list of identical rows, removing one silently re-binds the refs below it. An agent acting on a ref it captured before the removal acts on the wrong row, and the action succeeds. This is measured, reproducible, and the top open defect.

  • Electron has no declarativeNetRequest and no chrome.action, so modern MV3 content blockers and extension toolbar UI do not work out of the box. The community shim (electron-chrome-extensions) is ~13 months stale, GPL-3, and untested against Electron 43. Aperture uses Ghostery for blocking instead and treats Chrome-extension compatibility as an open question, not a promise.

  • Several Electron API behaviors the design depends on still need verification — chiefly whether Electron's WebAuthn support can host a platform authenticator (if not, passkeys become a Chromium-patch project and the vault roadmap stays password-primary), and whether setContentProtection actually excludes the vault window from capture on Windows 11. docs/design/security.md and docs/design/tier2.md §6 carry the ranked queue. One item already resolved unfavourably: overriding the UA does not keep Sec-CH-UA client hints coherent, which cost a claim this file used to make.

Getting started

npm install && npm run build && npx electron .

On launch it prints a ready-to-paste command and writes the same config to %APPDATA%/aperture/mcp.json. The bearer token is regenerated every launch.

claude mcp add --transport http aperture http://127.0.0.1:8817/mcp -H "Authorization: Bearer <token>"
npm test         # full suite — snapshot engine, security, vault, bench readers
npm run typecheck

Benchmarks, and what each one answers, are in docs/HANDOFF.md; the results and their adjudications are in bench/RESULTS.md.

Layout

src/
  main/       Electron main: window, tab manager, IPC
  preload/    shell.ts (trusted chrome UI) · page.ts (hostile territory)
  renderer/   the browser chrome UI
  core/       snapshot engine — walker, registry, diff, render, volatility
  mcp/        MCP server + tool surface
  privacy/    containers, tracker blocking
  vault/      agent-blind password vault
bench/        tokens · live · fidelity · guards · task · size · headtohead
docs/design/  snapshot.md · security.md · the tier specs · the adjudications
A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.

  • Screenshot, diff, audit and sitemap-capture any web page — 5 MCP tools for AI agents.

  • Live browser debugging for AI assistants — DOM, console, network via MCP.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cunninghambe/aperture'

If you have feedback or need assistance with the MCP directory API, please join our Discord server