# 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.
Authentication — Hangar