Security model
The Agent Protocol is a control surface for a live browser session. That includes browsing history, open tabs, cookies, and any credentials the browser has stored. The security model is the answer to “why is it OK to leave this on”.
Four layers stack on top of each other. An agent has to clear all four for a given operation to succeed.
- Same-user transport. Peer credentials on Unix, DPAPI token on Windows. Covered in Authentication.
- Allowed-domains policy (D1). Navigation requires explicit per-session user approval.
- Per-tab lease (OQ-3). Two agents cannot silently race on the same tab.
- Credential firewall (OQ-4). Every content-bearing response is scrubbed for four classes of secret before egress.
Credential firewall
Section titled “Credential firewall”The firewall is a compile-time and runtime construct. At the type level, no content leaves a tool handler except through Redacted<T>, a move-only wrapper whose constructor is private and whose only friend is the firewall itself. A handler that tries to return raw content will not compile.
At runtime, the firewall applies four passes to every payload. Each pass replaces matching material with the literal string [REDACTED].
Vector 1: Password fields
Section titled “Vector 1: Password fields”Any <input type="password"> value is redacted, regardless of the code path that produced it. Applies to:
- Direct DOM reads via
page_get_attributewithname="value". - Reader-mode extraction of pages that include a password input in the readable region.
- Accessibility snapshots (once shipped): the
valuefield on password inputs.
Vector 2: HTTP headers
Section titled “Vector 2: HTTP headers”Any header in the reserved set is redacted from every HAR entry, screenshot metadata, and error payload:
AuthorizationCookieSet-CookieProxy-Authorization- Any header configured under the user’s “custom auth headers” setting.
Only HAR and network capture responses carry headers, so this vector is dormant until the Wave 4.4 network surface ships.
Vector 3: Accessibility-tree autofill
Section titled “Vector 3: Accessibility-tree autofill”The accessibility surface (once shipped) marks nodes that Chrome’s autofill considers sensitive: credit card numbers, CVCs, addresses, SSN-like fields. Those values are redacted before the accessibility snapshot leaves the process.
An agent can still see that the node exists and can still browser_click or browser_type into it. The agent just cannot read the value the browser has already filled in.
Vector 4: URL fragments and query parameters
Section titled “Vector 4: URL fragments and query parameters”URLs are commonly OAuth callback vehicles. The firewall walks every URL that would egress and redacts:
- Query parameters matching
access_token,id_token,refresh_token,session,code,state,nonce, and any keys configured under the sensitive-URL-parameters setting. - The URL fragment (
#...) when it contains a matching key/value pair.
https://example.com/callback?code=abc&state=xyz egresses as https://example.com/callback?code=[REDACTED]&state=[REDACTED].
What the firewall does not cover
Section titled “What the firewall does not cover”The firewall is a leak-prevention layer, not a page sandbox. It does not:
- Prevent an agent from reading non-sensitive fields the user considers private. A user’s job title on a profile page is not redacted.
- Encrypt the content in flight. The transport is a local socket. The kernel already isolates it.
- Rate-limit exfiltration. If an agent reads a 500 KB article and forwards it to a cloud LLM, that is the user’s cloud LLM, and the firewall did its job by ensuring nothing in the 500 KB was a password or a token.
Per-tab lease
Section titled “Per-tab lease”Two connected agents can talk to the same browser at the same time. Without a lease, they can race on the same tab: one navigates, the other reads, and the read lands on the wrong page. The lease is the coordination primitive.
Lifecycle
Section titled “Lifecycle”An agent calls browser_acquire_lease on a tab. If no other session holds the lease, it is granted with a TTL. The agent must call browser_heartbeat_lease before the TTL elapses to keep it. If the TTL elapses without a heartbeat, the lease expires and the tab becomes available.
An agent releases explicitly with browser_release_lease. On session disconnect, all leases owned by that session are released.
TTL and heartbeat
Section titled “TTL and heartbeat”The default TTL is 30 seconds. The heartbeat cadence is 10 seconds. Both are configurable per acquire call within a bounded range (5-120 seconds for TTL). The server uses a monotonic clock so wall-clock jumps (NTP correction, daylight savings, laptop sleep) do not cause premature expiry.
Sleep and suspend are handled: on wake, the server evaluates lease expiry against the monotonic clock, not wall time. A lease that would have expired at 30 seconds monotonic still holds at 5 minutes wall-clock if the machine was asleep.
Force-steal
Section titled “Force-steal”If a tab is held and another agent needs it, the challenging agent can request a force-steal. The previous holder receives a notification frame:
{ "jsonrpc": "2.0", "method": "notifications/lease_stolen", "params": { "tab_id": "tab_1a2b", "stolen_by": "example-agent/0.1.0", "at_ms": 1719791234123 }}Subsequent tool calls from the previous holder that need the lease return -32011 lease_conflict. The previous holder is expected to either re-acquire (and possibly force-steal back) or drop the tab.
browser_acquire_lease
Section titled “browser_acquire_lease”Parameters:
| Name | Type | Required | Description |
|------|------|----------|-------------|
| tab_id | string | yes | Target tab. |
| ttl_seconds | integer | no | 5-120. Default: 30. |
| force_steal | boolean | no | Take the lease even if another session holds it. Default: false. |
Response:
{ "lease_id": "lease_7f", "tab_id": "tab_1a2b", "ttl_seconds": 30, "expires_at_ms_monotonic": 456789012}Errors: -32011 lease_conflict when another session holds it and force_steal was false.
browser_heartbeat_lease
Section titled “browser_heartbeat_lease”Parameters: lease_id. Response: new expires_at_ms_monotonic. Errors: -32012 lease_expired if the previous TTL already elapsed.
browser_release_lease
Section titled “browser_release_lease”Parameters: lease_id. Response: { "released": true }. Idempotent; releasing an already-released lease is a no-op.
Allowed domains (D1)
Section titled “Allowed domains (D1)”Detailed on the navigation page. Summary:
- Agents cannot navigate a tab to a URL unless the URL’s registrable domain matches an allowlist entry.
- The allowlist is set per session via
browser_set_allowed_domains. - Every distinct allowlist prompts the user. There is no “trust forever” checkbox.
- User-initiated navigation ignores the allowlist. This is a control on agents, not on the human.
The failure mode is a JSON-RPC error -32010 domain_approval_required with the requested URL and registrable domain in error.data.
Zero telemetry
Section titled “Zero telemetry”Nothing about your agent session leaves your machine because of Maho. There is no phone-home. There is no anonymized usage counter. If a user’s agent talks to a cloud model, that traffic is entirely between the agent and its model provider. Maho does not observe it, log it, or forward it.
Threat model summary
Section titled “Threat model summary”| Threat | Mitigation |
|--------|-----------|
| Another user on the same machine connects to your Maho | Peer credential check (Unix), DPAPI + DACL (Windows). Same-user only. |
| Network attacker reads the socket | No network socket exists. Local IPC only. |
| Agent reads a password out of the page | Vector 1 firewall pass replaces <input type="password"> value with [REDACTED]. |
| Agent captures an Authorization header from a HAR | Vector 2 firewall pass redacts headers before HAR egress. |
| Agent reads an OAuth token out of a redirect URL | Vector 4 firewall pass redacts token-shaped query and fragment parameters. |
| Two agents race on the same tab | Per-tab lease with TTL, heartbeat, and force-steal notification. |
| Agent navigates the user off to a phishing page | Allowed-domains policy requires user approval per session. |
| Stale token from a previous browser session | Windows session token regenerated on every launch; Unix has no persistent token. |
What the model does not cover
Section titled “What the model does not cover”- The agent itself is trusted with what the user gives it. If a user tells the agent “read my private page and post it publicly”, Maho does not stop that. The firewall is about credentials, not privacy at large.
- Extensions running in the browser are subject to their own permission model. The Agent Protocol does not interact with the extension surface.
- The agent’s own transport to its model provider is out of scope. Maho neither inspects nor secures that path.