Security and permissions

What a credential can reach, what refuses it, and where each decision is made. This is the operational picture; the security programme is at /security.

7 min

Two ways to authenticate

Prefer the first.

Browser OAuthMinted token
HowThe client opens a browser; you sign in and approveA ck_mcp_… bearer you paste into a config
Best forAny client that can open a browserHeadless — CI, a container, a server script
RevokeDisconnect the clientRevoke the token in Settings → Developers
LifetimeRefreshed automaticallyLong-lived until revoked

OAuth is the default and the recommendation, because a token is a long-lived secret somebody has to store and remember to revoke. The token form exists because a headless box cannot open a browser — not as a convenience.

Scopes

Three, and they nest.

ScopeGrants
rpc:readRead the workspace — list, get, search.
rpc:writeCreate and modify. Includes read.
rpc:adminOperator surfaces — cross-workspace dispatch, and reaching the local machine.

Scope is checked at the boundary, on every call. It is separate from the allowlist, which decides whether an action is reachable by an agent at all — both have to pass.

A credential is bound to one workspace

And, for MCP, to one agent.

https://relay.clearly.sh/mcp                        # the workspace this login belongs to
https://relay.clearly.sh/mcp/w/<workspaceId>        # a specific workspace
https://relay.clearly.sh/mcp/w/<workspaceId>/a/<agent>   # …acting as a specific agent

The workspace is in the URL rather than in ambient session state, because a client stores one credential per server entry — two workspaces behind a single entry collide, and authenticating the second silently overwrites the first. A distinct URL is a distinct credential key, which is what makes two live workspaces possible with no client change.

The agent segment is addressing, never authority. The server refuses unless it matches the token’s own agent. If the URL could win, any token holder could act as any agent by editing a string — straight past the per-agent allowlist that is the whole point of binding a credential to an agent.

Asking for a workspace you are not a member of, and asking for one that does not exist, return the same refusal. Two different messages would make the endpoint an enumeration oracle for workspace ids.

Where the gate lives

One place, and it fails closed.

Membership is checked in the edge worker, against the same roster your account lists — not inside the workspace. That is not a preference: a workspace receives gateway calls without a caller identity attached, so it has no way to learn who is asking. The worker is the last layer that holds the real user, so if the check were not there, nothing downstream would catch it.

A membership lookup that fails is treated as a refusal, never as a pass. The result is cached briefly to keep the common path cheap.

What refuses, and why

The boundaries worth knowing before you hit one.

BoundaryRefuses unless
Reaching /local — your own machine, through the daemonthe caller holds rpc:admin. A bearer token is long-lived and pasteable into a third-party client; an in-app agent driving your laptop is a person driving their own machine. Different risk, same verb.
Addressing another workspacethe caller holds rpc:admin — otherwise a write-scoped token would reach every workspace in the deployment.
The approval familya person does it. An agent approving its own edit is a self-approval hole.
An agent holding a live sessionnever — an agent principal may not hold a socket. Membership confers identity and attribution, not a session.
An unbound MCP credentialit names an agent. One that did not was silently rebound to the workspace default, so the log said what happened and never who asked.

Agents cost no seat

Which is exactly why the role is guarded.

An agent is a workspace member with its own role, and it is invisible to seat counting — otherwise a single-seat plan would spend its only seat on a robot and refuse the owner’s first real invite. Because that role is free and carries full write, it is unreachable from the human invite path: admitting a person under it, or an agent under a human role, is refused in both directions and at the chokepoint rather than at each call site.

Reporting something

If you believe you have found a vulnerability, email [email protected] rather than opening a public issue. The security programme, including what is delivered versus roadmap, is at /security.

Where to go next