Personal Access Tokens
Personal access tokens (PATs) let scripts, CI/CD pipelines, and integrations authenticate with WarmHub without interactive login.
To attribute writes to an agent persona while retaining your user accountability, see Create an agent identity.
Overview
Section titled “Overview”| Property | Detail |
|---|---|
| Format | Signed JWT |
| Default expiry | 30 days |
| Maximum expiry | 1 year (set via --expires) |
| Management | Web UI (/settings), CLI (wh token), or SDK (client.token.*) |
| Management auth | Interactive session (manages all your tokens) or a PAT (manages only tokens in its own descendant subtree) |
Token Lifecycle
Section titled “Token Lifecycle”Create → Use → [Expire] → Revoke- Create — Generate a named token via
wh token create. The token value is displayed once — save it immediately. - Use — Set the
WH_TOKENenvironment variable or pass the token in theAuthorization: Bearerheader. WarmHub validates the token on every request. - Expire — Tokens expire after 30 days by default. Use
--expiresto set a custom duration (max 1 year). Expired tokens are rejected immediately. - Revoke — Revoke a token by name via
wh token revoke. Takes effect immediately.
Expired and revoked tokens are preserved for audit visibility.
Scopes
Section titled “Scopes”Scopes limit what a token can do. They are optional — a token created without scopes has the full permissions of its owning user. If the token is itself created by a PAT (rather than an interactive session), omitting scopes causes the child token to inherit the parent PAT’s scopes rather than the owner’s full rights.
Each scope entry binds a resource to a set of permissions:
| Tier | Resource format | Example | Meaning |
|---|---|---|---|
| Repo-scoped | org/repo | myorg/myrepo | Specific repo only |
| Org-level | org | myorg | All repos in org, capped by role |
| Global wildcard | (omitted) | All resources, capped by role |
More specific entries take precedence. Scopes can only narrow access, never escalate beyond the user’s role.
Available permissions
Section titled “Available permissions”Each --scope entry lists one or more permissions: repo:read, repo:checkpoint-read, repo:checkpoint-generate, repo:write, repo:configure, repo:admin, repo:action-callback, org:read, org:configure, org:admin, or org:action-callback. Scopes are independent — repo:checkpoint-read does not include ordinary repository read authority — so request every permission your token needs. For what each permission grants and the minimum scope per task, see the access reference.
Role shorthand
Section titled “Role shorthand”Instead of listing individual permissions, a scope entry can name a role with role:<name>. The role expands to a fixed set of permissions when the token is created:
| Role | Repo permissions | Org-level entry also grants |
|---|---|---|
role:owner | repo:read, repo:checkpoint-read, repo:checkpoint-generate, repo:write, repo:configure, repo:admin, repo:action-callback | org:read, org:configure, org:admin, org:action-callback |
role:admin | repo:read, repo:checkpoint-read, repo:checkpoint-generate, repo:write, repo:configure, repo:admin, repo:action-callback | org:read, org:configure, org:action-callback |
role:editor | repo:read, repo:write | org:read |
role:viewer | repo:read | org:read |
A role: entry requires a concrete repo or org resource — it is rejected on the global wildcard. (Omit --scope entirely for full user access.) On a repo-scoped entry, role:owner and role:admin grant the same permissions; they differ only on an org-level entry. Like any scope, a role can only narrow access, never escalate beyond your own role.
The role is expanded at creation time, so redefining a role later does not change tokens already issued.
Managing Tokens in the Web UI
Section titled “Managing Tokens in the Web UI”You can create, view, and revoke tokens from the Settings page (/settings) in the web app. The Settings token list shows your active tokens and lets you revoke any of them directly. For operations that require the full CLI flag set — such as --scopes-json or --committer-identity — use the CLI or SDK instead.
CLI Usage
Section titled “CLI Usage”You can run wh token commands from an interactive session — which manages all your tokens — or with a PAT, which manages only the tokens in its own descendant subtree (the tokens it created, and theirs). Component (action) tokens cannot manage tokens.
Create a token
Section titled “Create a token”The --scope flag accepts three forms:
| Form | Meaning | Example |
|---|---|---|
org/repo=perms | Repo-scoped | --scope myorg/myrepo=repo:read,repo:write |
org=perms | Org-level (all repos in org) | --scope myorg=repo:read |
perms | Global wildcard (all resources) | --scope repo:read,repo:write |
Separate multiple permissions with commas. Repeat --scope for multiple entries. In the repo-scoped and org-level forms, the permission list can be a role:<name> shorthand instead of individual permissions.
# Full user access, default 30-day expirywh token create --name ci-bot
# Repo-scoped tokenwh token create --name ci-bot --scope myorg/myrepo=repo:read,repo:write
# Role shorthand — same as myorg/myrepo=repo:read,repo:writewh token create --name ci-bot --scope myorg/myrepo=role:editor
# Org-level wildcard (all repos in org, capped by role)wh token create --name org-reader --scope myorg=repo:read
# Global wildcard with description and custom expirywh token create -n deploy --scope repo:write -d "Deploy pipeline" --expires 90d
# Multiple scopes — repeat --scope for each entrywh token create --name ci-bot \ --scope myorg/private-repo=repo:read,repo:write \ --scope myorg=repo:read \ --expires 90dRestricted tokens with --scopes-json
Section titled “Restricted tokens with --scopes-json”For advanced use cases, the --scopes-json flag accepts a JSON array of scope entries. An entry restricts what it grants through grants: a list of pairs, each pairing name paths (allowedMatches) with a Shape clause (allowedShapes). A Thing is admitted when one pair covers it whole: its path matches that pair’s paths and its Shapes satisfy that pair’s clause.
--scopes-json is mutually exclusive with --scope — you cannot use both in the same command.
Each entry in the JSON array has:
| Field | Type | Required | Description |
|---|---|---|---|
resource | string | No | Resource name: "org/repo", "org", or omitted (global wildcard) |
permissions | string[] | Yes | Permissions for this resource |
grants | pair[] | No | The (paths, Shapes) pairs this entry grants. Omit it for an unrestricted entry. Repo-scoped entries only ("org/repo"); refused on org-level and global entries. An entry holds up to 64 pairs. |
Each pair has:
| Field | Type | Description |
|---|---|---|
allowedMatches | string[] | Glob patterns over name paths. Omit for every path. |
allowedShapes | object | A Shape clause. Omit for any Shape. See the Shape clause below. |
A pair must restrict something: a non-empty allowedMatches, or a Shape clause with at least one key set. {"allowedShapes": {"shapeless": false}} alone is a valid pair (see shapeless below). grants: [], {} and {"allowedShapes": {}} are refused. Entry-level allowedMatches and allowedShapes are no longer accepted: put them in a pair.
Name paths
Section titled “Name paths”allowedMatches patterns match these paths:
- A Thing’s name, for example
sensors/temp-1. The path carries no Shape: restrict Shapes with the pair’s clause. - A Shape definition, at
hub/shapes/<Name>(for examplehub/shapes/Sensor). - A subscription, at
Subscription/<name>, and a credential set, atCredentialSet/<name>. Forrepo:configureentries,bindCredentialsrequires both the subscription’sSubscription/<name>path and the credential set’sCredentialSet/<name>path to match.
The same pairs are enforced across thing operations, shape reads, all subscription management operations (including creating and reading subscriptions), every credential-set operation (including creating a set), and subscription-keyed action reads (the action live feed, run listings and stats, run attempts, and notifications). For credential sets, that covers wh credential create, list, view, set, unset, audit, delete, and revoke. wh credential list returns only the sets whose CredentialSet/<name> path the token’s pairs match, rather than refusing the request.
Pattern rules:
- Patterns use glob syntax:
sensors/*,config/**,Subscription/sub-foo,CredentialSet/my-creds. - A leading
!marks a deny carve-out:["players/**", "!players/secret/**"]grants every path underplayers/except those underplayers/secret/. Positives establish the grant, carve-outs subtract from it, and a carve-out always wins regardless of order. - Carve-outs match the full path, not a sub-path of the positives:
["players/**", "!secret/**"]still grants everything underplayers/. - A list of only carve-outs (e.g.
["!secret/**"]) grants every path except the carved-out ones. - Extglob negation
!(...)is rejected at token creation; express denies with a leading!instead.
The Shape clause
Section titled “The Shape clause”allowedShapes is an object with the following optional keys:
| Key | Type | Description |
|---|---|---|
shapes | string[] | Bare Shape names the pair covers (e.g. ["Sensor", "Config"]). |
shapesAll | string[] | Bare Shape names a Thing must carry all of. Also covered by the pair. |
shapesNot | string[] | Bare Shape names to exclude, wherever they sit in a Thing’s Shapes (composed Shapes included). A clause with only shapesNot covers every Shape it does not exclude. |
shapeless | boolean | When true, the pair also covers Things with no Shape. Omitted or false, a pair with a clause excludes Things with no Shape. |
A Thing’s declared Shapes must all be covered by the pair; a Shape that a named composite composes is covered by that composite. Every list must be non-empty. A Shape definition is admitted when the clause names that Shape.
Examples:
# Paths only: a clause-less pair, read-only under sensors/wh token create --name reader --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:read"],"grants":[{"allowedMatches":["sensors/**"]}]}]'
# Shapes only: Sensor and Config Things anywhere, excluding Internalwh token create --name shape-scoped --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:read"], "grants":[{"allowedShapes":{"shapes":["Sensor","Config"],"shapesNot":["Internal"]}}]}]'
# Paths and Shapes together, and a narrower write entrywh token create --name scoped-bot --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:read"], "grants":[{"allowedMatches":["sensors/**","config/**"],"allowedShapes":{"shapes":["Sensor","Config"]}}]}, {"resource":"myorg/myrepo","permissions":["repo:write"], "grants":[{"allowedMatches":["sensors/**"],"allowedShapes":{"shapes":["Sensor"]}}]}]'
# Subscription-scoped: configure only one subscriptionwh token create --name sub-manager --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:configure"],"grants":[{"allowedMatches":["Subscription/sub-foo"]}]}]'
# Credential-set-scoped: manage only one credential set, including creating itwh token create --name cred-manager --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:configure"],"grants":[{"allowedMatches":["CredentialSet/my-creds"]}]}]'
# No grants: same as the --scope shorthandwh token create --name full --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:read","repo:write"]}]'Only --scopes-json (in the SDK, structured: true) accepts several entries for one resource, so different permissions on it can carry different grants. Each (resource, permission) pair must still be unique. Repeated --scope flags, the web UI, and org member scopes allow one entry per resource. Both kinds of duplicate are rejected with a validation error.
The token is printed once. Save it to a secret store or environment variable immediately.
Child-token containment
Section titled “Child-token containment”When a PAT creates a child token, each of the child’s pairs must sit inside one of the parent’s pairs for the same resource and permission, paths and Shape clause together. A child pair is never admitted by combining two parent pairs.
Within a pair:
- Paths: the child’s positive patterns must fall within the parent’s, and the child must keep or broaden every parent carve-out that can affect them. For example, under a parent pair with
["players/**", "!players/secret/**"], a child pair must carry a carve-out covering at leastplayers/secret/**(!players/secret/**or the broader!players/**). Dropping it is rejected with an authorization error. - Shapes: the child cannot drop the parent’s clause, widen the covered Shapes, drop an exclusion, or set
shapeless: trueunless the parent’s clause does.
This applies at every level of the subtree: a grandchild cannot drop or narrow a restriction the grandparent established, even when the intermediate parent added none.
Committer identity
Section titled “Committer identity”A token can carry a default committer identity with --committer-identity. Writes made with the token are attributed to that identity unless a per-command --committer overrides it:
wh token create --name ci-bot --committer-identity wh:warmhub/users/<id>Use a full wref for your public user identity or a custom identity, such as wh:myorg/identities/ci-bot. A custom identity must be an active thing with a shape in a repo you can read. If that repo is private, include its repo:read scope on the token so writes can use the identity. See Create an agent identity for the complete setup.
You cannot bind another user’s public warmhub/users identity. Binding an identity controls attribution; the token’s scopes still control access. Without --committer-identity, writes are attributed to your public identity on a best-effort basis — if your public identity has not been created yet (for example, just after a first sign-in), the write lands with no committer recorded.
List tokens
Section titled “List tokens”wh token listwh token list --jsonShows your active tokens with their status and scopes. To include expired and revoked tokens, add the --all flag:
wh token list --allwh token list --all --jsonView a token
Section titled “View a token”wh token get --name ci-botShows token details: name, description, scopes, status, creation date, and expiry.
Revoke a token
Section titled “Revoke a token”wh token revoke --name ci-botRevocation is immediate. The token is rejected on the next API request.
Using PATs with the HTTP API
Section titled “Using PATs with the HTTP API”Pass the token in the Authorization header:
curl -H "Authorization: Bearer eyJhbGciOi..." \ https://api.warmhub.ai/api/repos/myorg/myrepo/headOr with the CLI via WH_TOKEN:
export WH_TOKEN=eyJhbGciOi...wh commit submit --add cave --shape Location \ --data '{"x":3,"y":7}' -m "Add cave"WH_TOKEN takes priority over any stored CLI profile. See Environment Variables for the full precedence rules.
Using PATs with MCP Clients
Section titled “Using PATs with MCP Clients”MCP clients like Claude and Cursor normally authenticate via OAuth automatically. However, PATs are useful when:
- Your organization restricts custom MCP connectors
- You’re on WSL2, where the OAuth callback can’t reach the browser
- You want to authenticate a dev instance without configuring OAuth
Use mcp-remote as a stdio bridge with your PAT:
{ "mcpServers": { "warmhub": { "command": "npx", "args": [ "-y", "mcp-remote@latest", "https://mcp.warmhub.ai/mcp", "--header", "Authorization:${WH_TOKEN}", "--transport", "http-only" ], "env": { "WH_TOKEN": "Bearer eyJhbGciOi..." } } }}This works in both Claude Desktop (claude_desktop_config.json) and Claude Code (.mcp.json). The --transport http-only flag ensures HTTP Streamable transport.
See the Quickstart for standard OAuth-based setup.
Security Model
Section titled “Security Model”- No escalation. A PAT can manage only the tokens in its own descendant subtree — the tokens it created, and theirs. It cannot list, read, or revoke tokens outside that subtree, so a leaked PAT can’t reach the rest of your tokens. An interactive session manages all your tokens.
- Scope enforcement. Each API endpoint checks the token’s scopes. A
repo:readtoken cannot push commits. - Immediate revocation. Revoking a token takes effect on the next request — there is no grace period.
- Name restrictions. Token names must be alphanumeric, hyphens, and underscores only (URL-safe).
Limitations
Section titled “Limitations”- No token rotation. To rotate, revoke the old token and create a new one.
- Subtree-scoped under a PAT.
wh tokenandclient.token.*accept an interactive session (all your tokens) or a PAT (only its own descendant subtree); component (action) tokens are rejected. To manage tokens outside a PAT’s subtree, sign in withwh auth login(CLI) or use an interactive session before calling the SDK methods.
Next Steps
Section titled “Next Steps”- Getting Access — interactive login and account creation
- HTTP API Authentication — using PATs as Bearer tokens
- CLI Commands — full
tokencommand reference