The token is the whole trust boundary
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.Outbound SSRF / navigation guard
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 Configuration) 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.
Inbound Host-header guard
Requests whoseHost 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.
The claude.ai OAuth handshake, under the hood
This is claude.ai’s real, observed connector behavior against Charlotte’s OAuth facade, not a guess:- Unauthenticated probe. claude.ai
POSTs/mcpwith no credential →401. - Discovery. 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. - Dynamic client registration. It dynamically registers itself as an OAuth client (
POST /oauth/register) and gets back aclient_id— no manual credential-copying required on your end. - Consent. 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. - Token exchange. 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). - Steady state. From then on, claude.ai’s connector authenticates every
/mcpcall with that derived bearer token, transparently — no further prompts.
Not multi-tenant
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.
Sizing: budget about 1 GB of RAM per running session
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.
Screenshots and other artifacts are capped, not paths
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.Container sandbox posture
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: Sandbox and security posture for the full mechanics and troubleshooting.Known accepted risks
- GHSA-jmr9-qjv8-65gv (
extract-zip≤ 2.0.1, high): unvalidated symlink path traversal when extracting a malicious zip archive. Present transitively throughpuppeteer@24→@puppeteer/browsers@2; no patched version exists — the package is unmaintained and upstream resolved it by dropping the dependency. Exposure is install-time only: extract-zip runs solely when@puppeteer/browsersunpacks the Chromium archive it downloads from Google’s CDN over HTTPS. Charlotte never extracts archives at runtime, and no untrusted party controls a zip it opens — exploitation would require compromising the Chromium download itself, the same trust already placed in the Chromium binary. Accepted 2026-08-18; will clear with the Puppeteer 25 upgrade (@puppeteer/browsers3.x replaced extract-zip withmodern-tar), which is planned as its own verified slice rather than a security drive-by. CI’s audit gate carries this ID in its reviewed allowlist (scripts/audit-check.mts) so new advisories still fail the build. - 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.