People and OAuth connectors¶
Admins and members, what a token can do, and the limits on OAuth apps. Part of the security model.
People¶
One hangar serves a team: run one per team rather than splitting one between teams. Every token belongs to a person, and a person is an admin or a member.
| Admin | Member | |
|---|---|---|
| Sandboxes, sessions, jobs, snapshots, schedules | everyone's | their own; anyone else's answers "not found", by id or by name |
| Environments | create and change any | create new ones and change their own; secrets, image, base, runtime_class and credential_mode: direct need an admin |
| Credentials | all; add team credentials (hangar cred add --team), assign one to a person (hangar cred owner) |
the team credentials and their own (hangar cred add stores it as theirs) |
| Tokens | mint for anyone, within that person's role | their own: user (their login), mcp (what hangar connect mints), jobs |
| REST | everything | /api/me, /api/connect, their tokens and credentials; jobs through a jobs token |
- A person's own credential (their API key, or their Claude or Codex login for interactive use) goes only into their sandboxes. A sandbox for someone who has their own credential for its harness uses it in place of the team's; anyone else gets the team's. Naming another person's credential is refused. A repo's policy can require one or the other (
credentials: person | team | person_then_team). - Team credentials are added by an admin on purpose (
hangar cred add --team); an admin's credential without--teamis their own. Jobs no person owns use them: webhooks, schedules without a person, and GitHub and Slack senders linked to nobody. They are API keys, or other credentials the vendor's terms allow for automation: a subscription login (Claude Pro or Max, ChatGPT) is refused as a team credential, since the vendors' terms make it one person's and Claude Code's forbid routing requests through a Pro or Max login on behalf of others (team.md). - Ownership is recorded as
personon sandboxes, snapshots, sessions, jobs and schedules. A job's session and sandbox, a fork and a best-of-N attempt are their source's person's. Work from a GitHub, Slack or Jira account an admin linked to a person (hangar person link, by ids that don't change hands: a GitHub user id, a Slack workspace and user id, a Jira account id) is that person's: their caps, their budget, their lists. A linked account acts as its person, so it is trusted at every front door, and only it continues that person's jobs there (team.md). Work from anyone linked to nobody, and from inbound webhooks, has no person, so only admins see it; the front door's own rule decides whether it waits for approval. - Existing installs keep working, and the owner's logins stay the owner's. Tokens minted before people, the server's admin token and the bootstrap token belong to
owner, an admin. Records made before have no person, so they are the admins'. Credentials stored before people become the owner's own on the first start, once (hangar cred migrationshows which), so your Claude or Codex login never reaches a member's sandbox or a job no person owns; control-plane ones stay the control plane's. Add team credentials for everyone else (team.md). - Removing someone revokes their tokens (OAuth grants they approved stop with them). Their work stays, visible to admins; their own credentials go into nobody's sandbox until an admin reassigns them. Demoting an admin drops the
adminscope from their tokens at once. - Visibility is enforced at the front doors (MCP tools and resources, REST), by the record each tool call names, so a new MCP tool with a
sandbox,session,job,ref,sourceorscheduleargument is covered without a change. - Listings filter in the database.
list_jobs,list_sessions, the recent-work resources andGET /api/jobsask the database for the caller's person's records (a member) or everyone's (an admin), then page through them with a cursor. A cursor is a position (a timestamp and a record id), not a credential: a member who passes a cursor made from someone else's record still gets only their own records after it. Archived jobs and sessions keep theirpersonand are listed and found by the same rules.
OAuth connectors¶
- Scope: OAuth access tokens reach
/mcponly, never the REST API or the dashboard. - Lifetime: every approval is tied to the hangar token that approved it and stops working when that token is revoked or the app is disconnected. Approvals last at most 90 days.
- Registration: limited to allowlisted redirect hosts (
HANGAR_OAUTH_REDIRECT_HOSTS; an empty list allows none,"*"any) and capped in number. Redirect URIs must behttps,httptolocalhost,127.0.0.1or[::1], or a native app's own scheme (RFC 8252). A host entry (claude.ai) covershttpsand its subdomains; a native app's scheme needs an exactscheme://hostentry, so listingclaude.aidoesn't let an app registerevil://claude.ai. RFC 8252 §7.1's reverse-domain form, which has no host (com.example.app:/oauth2redirect), needs an exactscheme:entry (com.example.app:). The consent page names the scheme of a non-web redirect (cursor://anysphere.cursor-mcp). Schemes a browser handles itself (javascript:,data:,file:and the like) are never allowed. A client that asks for several redirect URIs is registered with the allowed ones (RFC 7591 §3.2.1); it is refused only when none is allowed. - Cursor:
cursor://anysphere.cursor-mcpis in the default list. Any app on the user's machine can claim a custom scheme, just as any app there can listen on the loopback ports native clients use (also allowed by default), so the scheme adds no new exposure: the code goes to an app on the machine of the person who approved, PKCE (S256) binds it to the client that started the flow, and approving needs a hangar login token. Cursor also asks forhttps://www.cursor.com/agents/mcp/oauth/callback; that sends the code to Cursor's servers, so it is left out unless you listwww.cursor.com. - Audience: a client may ask for a token only for hangar's own MCP endpoint (
resource=HANGAR_PUBLIC_URLor…/mcp, RFC 8707); any other resource is refused at/authorize. - PKCE:
S256is required, and the code is bound to its redirect URI (the MCP SDK's token endpoint checks both). - Consent page: needs an admin token or a person's login (
user) token, can't be framed, and isn't cached. The grant acts as that token: a member's connector sees their own work.