Authentication
The Agent Protocol has one job at connect time: prove that the process on the other end of the socket belongs to the same user that launched Maho. There is no username, no password, no OAuth flow. The operating system is the source of truth for identity.
Same-user only
Section titled “Same-user only”Maho binds a Unix domain socket at:
~/Library/Application Support/Maho/maho.sockOn accept, the server queries the peer’s credentials via LOCAL_PEERCRED (getsockopt with SOL_LOCAL / LOCAL_PEERCRED). If the peer’s effective UID does not match the browser’s own UID, the connection is closed before the first byte is read.
The socket file is created with mode 0600, so even before the peer-credential check, the filesystem refuses open() for anyone but the owner.
Maho binds a Unix domain socket at:
$XDG_STATE_HOME/maho/maho.sockIf XDG_STATE_HOME is unset, the path falls back to ~/.local/state/maho/maho.sock.
On accept, the server queries SO_PEERCRED (getsockopt with SOL_SOCKET / SO_PEERCRED). If the peer’s UID does not match the browser’s UID, the connection is closed. The socket file is 0600 and the containing directory is 0700.
Windows has no direct peer-UID query for Named Pipes with the same semantics as SO_PEERCRED. Maho uses a DPAPI-encrypted session token instead.
On browser launch, Maho generates a 32-byte random token, writes it to:
%LOCALAPPDATA%\Maho\mcp-session-tokenThe file is encrypted with CryptProtectData under the user’s DPAPI scope, and the parent directory has a restrictive DACL that only grants access to the current user and SYSTEM. A new token is generated on every browser launch, so a stale token from a previous session is useless.
The pipe path is:
\\.\pipe\maho-browser-<username>The <username> component is the account name from GetUserNameW. The pipe’s security descriptor grants FILE_ALL_ACCESS to the interactive user only.
A connecting client must decrypt the session token from mcp-session-token and echo it back inside the initialize handshake (see below). The server compares byte-for-byte and rejects any mismatch.
The handshake
Section titled “The handshake”Every session begins with an initialize request. Servers that speak MCP already expect this call, so any conforming MCP client will produce it for you. If you are writing a client from scratch, this is the shape.
Request
Section titled “Request”{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "example-agent", "version": "0.1.0" } }}On Windows, the client also includes the decrypted session token:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "example-agent", "version": "0.1.0" }, "sessionToken": "b3a7...redacted..." }}The maho mcp CLI handles the token read, decrypt, and echo automatically. A raw client has to do it itself.
Response
Section titled “Response”{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { "name": "maho-browser-mcp", "version": "0.1.0" } }}After a successful response, the client sends notifications/initialized, and the session is ready to call tools.
Error codes
Section titled “Error codes”Authentication failures come back as standard JSON-RPC 2.0 error objects. The relevant codes:
| Code | Name | When it fires |
|------|------|---------------|
| -32001 | not_authenticated | A tool call arrived before initialize completed, or the session was invalidated. |
| -32002 | authentication_failed | Peer credential mismatch (Unix), missing or wrong session token (Windows), or DPAPI decrypt failure. |
Example error frame:
{ "jsonrpc": "2.0", "id": 2, "error": { "code": -32002, "message": "authentication_failed", "data": { "reason": "peer_uid_mismatch" } }}On -32002 the server closes the connection after sending the response. On -32001 the connection stays open and the client can retry initialize.
What is not part of authentication
Section titled “What is not part of authentication”- Per-tool authorization. Every tool available to the server is available to any authenticated session. There is no role or scope model. Access control lives one layer up in the security model (leases, allowed domains, credential firewall).
- Rate limiting. Not enforced at the auth layer. The browser will still refuse individual tool calls that violate the session policy.
- Multi-agent identity. All sessions with a valid peer credential are equal peers. If you need to distinguish agents, put an identifier in
clientInfo.nameand inspect the audit log yourself.
What to read next
Section titled “What to read next”- Tools reference for what an authenticated session can call.
- Security model for the constraints applied to every tool call after authentication.