This is the security model for Charlotte Remote (HTTP mode) — the trust boundary, the network guards, the OAuth handshake, and the sizing you should know before exposing Charlotte to claude.ai or any other MCP client over HTTPS. For the self-hosting walkthrough, see SELF_HOSTING.md; for container/sandbox internals, see DOCKER.md.
Anyone holding a valid bearer (your operator token, or a derived one claude.ai's connector obtained through consent) can drive Charlotte's browser to fetch anything the SSRF guard below doesn't block. Keep CHARLOTTE_AUTH_TOKEN as secret as any other credential.
To revoke access (e.g. claude.ai's stored derived token, or a leaked operator token): generate a new token and restart Charlotte with it — every previously derived credential stops matching immediately, since nothing is stored server-side to selectively revoke.
Every navigation Charlotte's browser makes is checked against a deny-by-default list of private address ranges — loopback, RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16, including the 169.254.169.254 cloud metadata address), CGNAT/Tailscale (100.64.0.0/10), and their IPv6 equivalents. This is checked against the resolved IP at request time (not just the URL string), so it isn't fooled by a hostname that resolves to a private address or a redirect chain that ends up there. http.allowPrivateNetworks (see docs/configuration.md) is the only way to punch a hole in this — leave it [] unless you have a specific reason your Charlotte needs to reach something on your own network.
Requests whose Host header doesn't match an expected hostname are refused with 403 before touching the session or browser — this defends against a malicious page in someone else's browser "DNS-rebinding" its own domain to your loopback address and POSTing straight to Charlotte's listener. The allowlist is derived automatically from localhost/127.0.0.1/[::1], your configured host, and your publicOrigin hostname; http.allowedHosts extends it if a proxy sits in between and presents some other hostname.
This is claude.ai's real, observed connector behavior against Charlotte's OAuth facade, not a guess:
- claude.ai
POSTs/mcpwith no credential →401. - It fetches Charlotte's OAuth discovery documents (
/.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server) — both only exist becausepublicOriginis set; without it these 404 and the connector add fails with an error about not being able to register a sign-in service. - It dynamically registers itself as an OAuth client (
POST /oauth/register) and gets back aclient_id— no manual credential-copying required on your end. - Your browser is redirected to Charlotte's consent page (
GET /oauth/authorize) — a plain server-rendered page with one field. Enter yourCHARLOTTE_AUTH_TOKENhere. This is the one and only place you type the token during connector setup. - On success, Charlotte redirects back to claude.ai with an authorization code; claude.ai's backend exchanges it (
POST /oauth/token) for a bearer token derived from your operator token — distinct from it, but cryptographically tied to it (no server-side storage: restart Charlotte with the same token and the derived credential is unchanged; restart with a different token and claude.ai's stored credential silently stops working, which is how you revoke it — see above). - From then on, claude.ai's connector authenticates every
/mcpcall with that derived bearer token, transparently — no further prompts.
Charlotte Remote is a single implicit session shared by every client that holds a valid token — it is not multi-tenant. Two different callers hitting the same server see the same tabs, same navigation history, same everything. Don't point multiple untrusted parties at one Charlotte instance expecting isolation between them.
Measured against Chromium's actual process tree: summed RSS (what a container memory limit / top will show you) peaks around 923 MB per idle session; the "honest" per-session share accounting for pages shared across Chromium's process tree (PSS) is about 330 MB. Size your host/container memory limit for the larger, ~1 GB figure — that's what will actually get enforced against you if you set a hard cgroup limit.
A screenshot or similar artifact up to 256 KB is returned inline in the tool response; above that cap, the tool call is refused with a steering message instead of returning a filesystem path — an HTTP client (unlike a local stdio client) has no way to read a path on your server's disk, so there is no "here's a file on the server, go read it" fallback in HTTP mode.
Chromium's own sandbox is a load-bearing part of this model — the Docker image ships with it enabled, not disabled, so a hostile page a session navigates to is exploiting the renderer inside the sandbox, not your server. See DOCKER.md § Sandbox / Security Posture for the full mechanics and troubleshooting.
- GHSA-frvp-7c67-39w9 (
@hono/node-server< 2.0.5, moderate): path traversal in that package'sserve-statichandler on Windows hosts via an encoded backslash. Present transitively through@modelcontextprotocol/node; Charlotte never invokes the affectedserveStaticcode path, and the documented deployment target is Docker on Linux. Accepted for v0.8.0 (2026-08-09); will clear when the MCP SDK moves to@hono/node-server≥ 2.0.5. If you run Charlotte's HTTP mode directly on Windows, weigh this yourself.