Personal Access Tokens
Personal access tokens (PATs) let scripts, CI/CD pipelines, and integrations authenticate with WarmHub without interactive login.
Overview
Section titled “Overview”| Property | Detail |
|---|---|
| Format | Signed JWT |
| Default expiry | 30 days |
| Maximum expiry | 1 year (set via --expires) |
| Management | 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.
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:write, repo:configure, repo:admin, repo:action-callback, org:read, org:configure, org:admin, or org:action-callback. Scopes are independent — repo:write does not include repo:read — so request the specific permissions 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:write, repo:configure, repo:admin, repo:action-callback | org:read, org:configure, org:admin, org:action-callback |
role:admin | repo:read, 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.
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 90dRef-restricted tokens with --scopes-json
Section titled “Ref-restricted tokens with --scopes-json”For advanced use cases, the --scopes-json flag accepts a JSON array of scope entries with optional allowedMatches patterns that restrict which refs within a repo the token can access. Patterns match against repo-scoped refs such as Signal/temp-1 (a thing ref) or Subscription/sub-foo (a subscription ref). The same patterns are enforced across thing operations, all subscription management operations (including creating and reading subscriptions), and subscription-keyed action reads (the action live feed, run listings and stats, run attempts, and notifications). For repo:configure entries, allowedMatches also gates credential-set surfaces: bindCredentials requires both the subscription ref and the CredentialSet/<name> ref to match a positive pattern.
--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 |
allowedMatches | string[] | No | Glob patterns restricting which repo-scoped refs this entry can access. Repo-scoped entries only ("org/repo"). Ignored on org-level and global entries. |
allowedMatches behavior:
- Repo-scoped entries only.
allowedMatcheson org-level or global wildcard entries is stripped with a warning — ref restrictions require a specific repo target. - Patterns use glob syntax and match against the full ref, for example
Signal/*(things of type Signal),Config/**(all Config things), orSubscription/sub-foo(a specific subscription). - When present, the token can only access refs that match at least one positive pattern (and no carve-out). This applies to both thing refs and subscription refs.
- Shape visibility follows the same matcher.
allowedMatchesapplies to shape reads as well as thing reads:shape.listfilters results to shapes whose bare name matches, andshape.get(andclient.shape.get) rejects requests for a bare shape name that falls outside the token’s patterns. For example, a token withallowedMatches: ["GameState/**"]will not surface theGameStateschema itself — you must also include the bare name"GameState"as a positive pattern if your tooling needs to read the shape definition. - A leading
!marks a deny carve-out:["Player/**", "!Player/secret/**"]grants Player things except those underPlayer/secret/. Evaluation is two-phase — positives establish the grant, carve-outs subtract from it, and a carve-out always wins regardless of order. - Carve-outs match the full ref, not a sub-path of the positives:
["Player/**", "!secret/**"]still grants all Player things (the carve-out targets the separatesecretnamespace). - A list of only carve-outs (e.g.
["!secret/**"]) grants everything the entry’s permissions grant, minus the carved-out refs. - Extglob negation
!(...)is rejected at token creation; express denies with a leading!instead. - Omitting
allowedMatchesmeans no ref restriction for that entry. - An empty array (
[]) is valid and means deny-all for that entry.
With --scopes-json, you can assign different allowedMatches to different permissions on the same resource by using multiple entries. Each (resource, permission) pair must be unique — duplicate pairs are rejected with a validation error.
# Read Signal/* and Config/*, but only write Signal/*wh token create --name scoped-bot --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:read"],"allowedMatches":["Signal/*","Config/*"]}, {"resource":"myorg/myrepo","permissions":["repo:write"],"allowedMatches":["Signal/*"]}]'
# Simple ref restriction — read-only for Signal/*wh token create --name reader --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:read"],"allowedMatches":["Signal/*"]}]'
# Subscription-scoped — configure only a specific subscriptionwh token create --name sub-manager --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:configure"],"allowedMatches":["Subscription/sub-foo"]}]'
# No allowedMatches — same as --scope shorthandwh token create --name full --scopes-json '[ {"resource":"myorg/myrepo","permissions":["repo:read","repo:write"]}]'The token is printed once. Save it to a secret store or environment variable immediately.
Child-token carve-out inheritance
Section titled “Child-token carve-out inheritance”When a PAT creates a child token, the child’s allowedMatches list must preserve or broaden every deny carve-out from the parent. You can narrow the child’s positive matches, add new carve-outs, or widen an existing carve-out to cover more refs — but you cannot drop a carve-out that the parent already enforces.
For example, if the parent token was created with ["Player/**", "!Player/secret/**"], any child token for the same entry must include a deny carve-out that covers at least Player/secret/**. A child using !Player/secret/** (identical) or !Player/** (broader) both satisfy the contract. Attempting to create a child token whose allowedMatches omits an inherited carve-out entirely — leaving refs the parent denied now accessible — is rejected with an authorization error.
This rule applies at every level of the subtree: a grandchild token cannot drop or narrow a carve-out that the grandparent established, even if the intermediate parent did not add new ones.
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/Identity/<id>The value is an identity wref. Without it, writes are attributed to your own user identity.
The identity must be your own. --committer-identity only accepts a public Identity that belongs to you — passing another user’s public Identity is rejected. Globally readable warmhub/users identities are not forgeable committer labels.
List tokens
Section titled “List tokens”wh token listwh token list --jsonShows all your tokens with their status (active, expired, or revoked) and scopes.
View 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://api.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.
- No web UI. Tokens are managed via the CLI or SDK only.
- 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