Aperture
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ApertureGo to my bank and download last month's statement"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
e42names a logical element, not a DOM node, so it survives a React re-render that replaced every node. Identity isdata-testid→name→ non-generatedid→ 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 headedFULL 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
replaceop carries agone: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 anerror: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_fillbrowser_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:
Tell the site the truth —
prefers-color-schemereports the real preference, so sites shipping their own dark theme use it. Always better than anything synthesized.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.setAutoDarkModeOverride— verified working on Electron 43, which is what makes per-site control possible at all (the Blink setting alone is process-wide).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:
No getter exists. Not disabled, not flagged — absent.
vault_request_fillperforms 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.Unnameability.
vault_entries_for_originreturns only entries matching the page's own origin. Onevil.comthe agent is never told agoogle.comentry 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:
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_tabslists only the browser tab.It is excluded from capture.
setContentProtection(true)maps toWDA_EXCLUDEFROMCAPTUREon 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.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
deletepass 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
beforeSendwhich 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(stacktraceis a flat array, not{frames};mechanismis required; breadcrumbs usets, nottimestamp). 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
originatesInVaultfound 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 matchedos.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:
A Notion page is open in a tab → append the capture there.
Notion configured but no page open → append to today's dated page.
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 |
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 ( | 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 |
Snapshot engine end-to-end (walker → refs → diff → render) | Working — verified on real sites and five fidelity fixtures |
| Working — trusted CDP input for pointer/keyboard, isolated-world setter for |
Input witness (act acknowledged ⇒ input actually reached the page) | Working — covers targeted acts plus scroll and key; |
Diff fidelity — typing, re-renders, clicks/state flips, shadow DOM, both resync fallbacks, native + ARIA selects | GREEN across five scenarios ( |
Refusals and retractions (disabled, obstructed, blank-query, bounded error text, option-list retraction) | GREEN — 11/11 ( |
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 |
Positional refs under row insertion | Fixed — a positional family that gains a member escalates to a full |
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 | 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 |
| Known gap — only |
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
declarativeNetRequestand nochrome.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
setContentProtectionactually excludes the vault window from capture on Windows 11.docs/design/security.mdanddocs/design/tier2.md§6 carry the ranked queue. One item already resolved unfavourably: overriding the UA does not keepSec-CH-UAclient 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 typecheckBenchmarks, 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 adjudicationsThis server cannot be installed
Maintenance
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
- Alicense-qualityDmaintenanceThe Zero-Setup Local Browser MCP. Enables AI agents to control web browsers via CDP with zero vision tokens and high-speed DOM mapping.Last updated17MIT
- AlicenseBqualityCmaintenanceBrowser MCP server that connects to your existing browser, preserving sessions, passwords, and extensions, enabling AI agents to interact with web pages without bot detection.Last updated31201MIT
- AlicenseAqualityBmaintenanceEnables AI agents to control a browser via MCP with structured perception, verified actions, and self-healing capabilities.Last updated613211MIT
- Alicense-qualityAmaintenanceMCP-first browser-control toolkit enabling AI agents to safely automate browser actions using redacted snapshots and ref-based interactions.Last updatedApache 2.0
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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