# Authentication
Hangar speaks two auth schemes. Pick the one that matches your caller.
## Sessions (browsers)
The dashboard uses [Better Auth](https://better-auth.com) DB-backed
sessions. The session cookie is named `hangar.session` (or
`__Secure-hangar.session` in production). You don't need to do
anything — the login page sets it.
## Personal Access Tokens (everything else)
Every non-browser caller — MCP clients, agents, CLIs, CI jobs — uses
a Personal Access Token. PATs are bearer tokens prefixed with `oss_`.
### Mint a token
Visit [/dashboard/settings/tokens](/dashboard/settings/tokens) or
`POST /api/tokens`:
```bash
curl -X POST https://hangar.so/api/tokens \
-H "Cookie: hangar.session=…" \
-H "Content-Type: application/json" \
-d '{
"name": "Claude Desktop",
"scopes": ["mcp:*"],
"expiresInDays": 90
}'
```
The response includes the **plaintext** token. **It is shown ONCE**.
Store it securely; we keep only a hash on the server.
```json
{
"id": "tok_...",
"name": "Claude Desktop",
"tokenPrefix": "oss_aBcD...",
"plaintext": "oss_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345",
"scopes": ["mcp:*"],
"expiresAt": "2026-08-10T00:00:00Z",
"createdAt": "2026-05-12T00:00:00Z"
}
```
### Use a token
Send it as a Bearer header on every request:
```bash
curl https://hangar.so/api/instance/status \
-H "Authorization: Bearer oss_..."
```
### List tokens
```bash
curl https://hangar.so/api/tokens \
-H "Authorization: Bearer oss_..."
```
The plaintext is **never** returned again — only the prefix and metadata.
### Revoke
```bash
curl -X DELETE "https://hangar.so/api/tokens?id=tok_..." \
-H "Authorization: Bearer oss_..."
```
## Scopes
PAT scopes are explicit. The default mint is `mcp:*` (every MCP tool).
For least-privilege tokens, mint with explicit scopes:
| Scope | Tools |
|---|---|
| `mcp:wallet.read` | `wallet.balance`, `wallet.transactions` |
| `mcp:wallet.write` | `wallet.checkoutTopUp` |
| `mcp:instance.read` | `instance.status` |
| `mcp:instance.write` | `instance.pause`, `instance.resume`, `instance.refreshEnv`, `instance.setModel` |
| `mcp:personas.read` | `personas.list` |
| `mcp:personas.write` | `personas.activate`, `personas.deactivate` |
| `mcp:skills.read` | `skills.list`, `skills.show` |
A read-only Claude token typically wants:
```json
{
"name": "Read-only Claude",
"scopes": [
"mcp:wallet.read",
"mcp:instance.read",
"mcp:personas.read",
"mcp:skills.read"
]
}
```
## Failure modes
- `401 unauthorized` — token missing, malformed, or revoked. Mint a new
one.
- `403 forbidden` — token lacks the scope. Mint a token that includes
the scope you need.
- An invalid Bearer token does **not** fall back to the cookie session.
This prevents typos from "still working" via the user's browser
session.
## Audience-scoped HMAC tokens (machine-side)
When Hangar provisions a Fly Machine, it injects three short-lived
HMAC tokens, each scoped to a single audience:
- `llmToken` — `/api/llm/proxy` (wallet-billed LLM calls).
- `filesToken` — `/api/agent/files` (first-boot config sync).
- `agentToken` — `/api/agent/{prompt,context,observe,feedback}`.
These are not user-facing. They exist so a leaked LLM token cannot
read the secret vault, and a leaked files token cannot run billed LLM
calls. Audit-driven separation, not theatre.