API keys and scopes
fleet:read, fleet:run, fleet:admin; what a key can and cannot do; rotation.
Creating a key
Settings → API keys (admins and owners): give the key a name ("Claude Code on my laptop") and an access level. The key is shown once, looks like shc_…, and is stored as a peppered HMAC-SHA256 hash: nobody, including the Skillhook team, can read it back. Keys can carry an expiry date; past it, a key is refused exactly like a revoked one. Revoking a key takes effect immediately for anything that uses it. Each key belongs to the organisation it was created in and acts only there.
The three scopes
A key has one scope, and each scope maps onto an organisation role: the key may do exactly what a person with that role may do, decided by the same tables (see Teams and roles and the command table under Machines and pairing). No scope acts as owner. The machine's own mode and policy apply to a key as they do to a person: a key cannot act on a machine paired in observe mode.
What keys never do
Whatever its scope, a key never manages access: it cannot create or revoke API keys, invite or remove people, change roles, pair a machine, or add a notification channel. Those are done by a signed-in person on the dashboard, so nothing a key does outlives it: a leaked key can neither widen nor outlive its access, and revoking it ends what it could do. A key can read the team and the channels and test a channel. One more thing stays out of its reach by design: a skill's secret. skillhook cloud secret has the machine generate it sealed to a key pair made for that one request, and the cloud only forwards the sealed value, so a model behind the hosted MCP server could never open it.
The Authorization header
Send the key as a bearer token on every request to /api/v1 and /api/mcp, never in a URL or a query string. Errors are RFC 9457 problems with a code and a request_id; see the error contract.
401 unauthorized: no bearer token in the header;401 invalid_key: the key is unknown, revoked or expired.403 forbidden: the operation needs a wider scope; the message names it.429 rate_limited: more than 600 requests a minute from this key, on the REST API and the hosted MCP server together;Retry-Aftersays when.503 unavailable: our trouble, worth a retry; never a 401 or a 404 when we could not look the key up.
Rotation
- Create a new key with the same scope.
- Put it where the old one was:
skillhook cloud login --url https://skillhook.devagain on each machine that uses the CLI or the plugin, the secret in CI, the header in each MCP client. - Revoke the old key. Anything still using it gets
401 invalid_keyat once.
The audit log records who created and revoked each key, by name, so a key can be traced without ever being shown.
Where the CLI keeps it
skillhook cloud login asks for the key at the terminal without echoing it (or reads it from stdin with --key -), checks it against the cloud (GET /api/v1/me), and keeps it in ~/.skillhook/.env as SKILLHOOK_CLOUD_API_KEY next to SKILLHOOK_CLOUD_API_URL, the cloud it belongs to. It is never printed again and never reaches a skill's run, even when a skill lists it in env:; the machine's own pairing token is a different credential and is never used for the API. skillhook cloud status says whether a key is kept and for which cloud; skillhook cloud logout forgets it. The local MCP server refuses to set or move these values, so an agent never handles the key.
In CI and scripts
The same two variables in the environment do what login does, without touching a file: set SKILLHOOK_CLOUD_API_KEY and SKILLHOOK_CLOUD_API_URL and every skillhook cloud command works. Store the key as a secret of your CI system and give it the narrowest scope the job needs.