Skip to content

Security model

Tesseron's security model is local-first, user-authorised. The MCP gateway binds to localhost and won't expose any action until a human types a short code out-of-band. These are the two gates.

writes manifest reads dials advertised binding no inbound port YOUR APP binds loopback only INSTANCE ~/.tesseron/instances/ MCP GATEWAY transport client ATTACKER remote host
Apps announce loopback URLs. The gateway only dials loopback. Nothing off the machine gets a connection.

Apps bind locally only - WebSocket servers on 127.0.0.1, Unix domain sockets in private temp dirs. The gateway refuses non-loopback URLs read from ~/.tesseron/instances/ and rejects UDS paths it can't connect() to as the running user. The gateway itself binds no ports, so a remote attacker has nothing to dial. Every hop is on the machine.

This is defence-in-depth. A drive-by page on evil.com can't reach your app's server - it would have to resolve 127.0.0.1 from the browser's origin, which the Same-Origin Policy blocks by default, and even a successful connection attempt would still face the claim-code gate below.

Even from a permitted origin, the session is inert until claimed. The flow:

  1. App connects and sends tesseron/hello.
  2. MCP gateway generates a random 6-char code (format XXXX-YY, excludes visually confusing characters) and returns it in welcome.
  3. The code is displayed out-of-band: gateway stderr, and wherever your app chooses to render it.
  4. The user types the code into the agent ("connect Tesseron session AB3X-7K").
  5. Agent calls tesseron__claim_session. If the code matches, the session transitions to Claimed and tools/list_changed fires.

The claim code is never sent on the WebSocket from the gateway to the agent - the user carries it across. That's the whole point: it's a human-performed authorisation gesture, not an electronic one.

Strength: ~1.5 billion possible codes (6 positions × ~32 unambiguous alphanumerics), drawn uniformly from the platform CSPRNG (crypto.getRandomValues) with rejection sampling — not Math.random(), which is not cryptographically secure. A brute-force attacker would need ~750M tries for a 50% hit rate. Codes are single-use; a failed match does not retry.

hello [shop actions] hello [admin actions] tools/list tools/call shop__addItem actions/invoke tools/call admin__banUser actions/invoke SHOP APP app.id = 'shop' searchProducts, addItem ADMIN APP app.id = 'admin' listUsers, banUser MCP GATEWAY routes by prefix AGENT shop__* | admin__*
Two apps, one gateway. Tools are namespaced by app.id; actions route back to the declaring session.

Multiple apps can be connected at once. Every MCP tool is prefixed with the app.id, so shop__addItem and admin__banUser never collide. Internally the gateway routes tools/call name=shop__addItem to the session whose app.id === "shop" - if that session has disconnected, the call errors with -32003 ActionNotFound.

This means you can, without coordinating, keep your dashboard and your customer app both connected to the same agent. Each has its own claim code, its own origin check, its own session.

  • Malicious code running in your app's process. If an attacker already executes JS in your tab or on your Node server, they can call your SDK and declare whatever actions they want. Tesseron is no worse - and no better - than the process it's embedded in.
  • Malicious MCP clients on the same machine. Any local process running as the same user can read ~/.tesseron/instances/ and dial one of the advertised endpoints. They would still need to send a valid tesseron/hello and convince the user to type the claim code into their agent. The claim code is the second gate, and it requires human cooperation - but it's the only thing stopping a rogue process from attaching to your session.
  • Prompt injection. If your handler's description or inputs are attacker-controlled, they can manipulate the agent's plans. Sanitise descriptions you show to the agent the same way you would sanitise HTML you show to users.
  • Exfiltration via resources. Anything you expose as a resource is readable by the claimed agent. Don't expose credentials, session tokens, or PII you haven't decided the user is okay sharing with Claude.
  • Bind to 127.0.0.1, never 0.0.0.0. The default in @tesseron/server and @tesseron/vite is loopback-only; don't override it unless you know exactly why. UDS hosts go through os.tmpdir() with a private (mode 0700) directory.
  • ~/.tesseron/ files are written private (mode 0600 inside a 0700 directory). Instance manifests and claim breadcrumbs are owner-only on POSIX. Sibling processes running as the same user can still open them — same-UID enforcement is the OS's job — but cross-user enumeration is closed. On Windows POSIX modes are advisory; the OS user model is the gate, same caveat as the UDS binding spec.
  • Treat the claim code as short-lived. Don't render it persistently in the UI after the session is claimed.
  • Clean up instance manifests on app exit. The built-in SDKs do this for you; if you port to a new runtime, handle the shutdown path so stale files don't pile up.
  • Log the agent.id that claimed the session - useful for auditing which agent actually ran which action.
  • For production tools, use per-user app IDs. shop_kenny vs shop_sarah prevents one user's agent from driving another user's tab, even if both are on the same machine.

That's the end of the Protocol section. The SDK section picks up from here - how to speak this protocol from TypeScript today and from other languages later.