Skip to content

Credentials and connected accounts

A credential is a secret that knows how to turn itself into auth. An http node, a connector node, a job or a script references it by name, and Hyperspool builds the header or sets the environment variable when the work runs. Unlike a project secret, which every job gets, a credential reaches only the work that names it.

Like this, an http node that calls GitHub with a stored token:

Terminal window
gh auth token | spool credential set github --kind bearer
{ "id": "create_issue", "type": "http",
"config": { "url": "https://api.github.com/repos/me/app/issues",
"method": "POST", "credential": "github" } }
Terminal window
gh auth token | spool credential set github --kind bearer # token from stdin
spool credential set posthog --kind api-key --header X-API-Key # prompts for the value
spool credential set registry --kind basic --username ci # prompts for the password
spool credential set tg-bot --kind url-token # path secrets, Telegram-style
spool credential list # names + kinds, never values
spool credential delete github

No flag takes the secret part (the token, the api-key value, the password): it is read from stdin, or prompted for without echo on a terminal. In the console they live under Settings > Connections > Credentials.

Kind Flags On a request As an environment variable
bearer Authorization: Bearer <token> the token
api-key --header <name> (required) <name>: <key> the key
basic --username <user> (required) Authorization: Basic ... username:password
url-token replaces {credential} in the URL the token

Credentials are write-only, like secrets: once set, no command, API call or console page returns the value. To change one, set it again. They are sealed with the same per-org key as secrets.

Five things take a credential:

Where Field What arrives
http node credential: an auth header, or the secret spliced into the URL
A connector node (slack.post_message, anthropic.messages, …) credential: the same, on the request its action builds
code / shell node credentials: environment variables (ENV_VAR -> name)
A pipeline, at workflow or job level credentials: the same, for that job
A workflow node’s from_repo: credential: the token used to read the repo

The definition, the run history and the queue carry only the name; the value becomes an Authorization (or custom) header at the moment the request is sent. An explicit headers: entry in the node config wins over the credential’s header of the same name.

A url-token credential instead replaces a literal {credential} placeholder in the node’s url, for APIs that carry the secret in the path (api.telegram.org/bot{credential}/sendMessage, Discord webhook URLs).

A connector node names one the same way an http node does:

{ "id": "notify", "type": "slack.post_message",
"config": { "with": { "channel": "#deploys" },
"input": { "text": "shipped" },
"credential": "slack-bot" } }

The connector decides which kind it needs and whether it needs one at all. Connectors lists what each built-in expects.

A code or shell node can name credentials to receive as environment variables, for a script that talks to a database or an API by hand. The credentials map is ENV_VAR -> credential_name:

{ "id": "sync", "type": "code",
"config": {
"script": "psql \"$DB_URL\" -c 'select count(*) from orders'",
"credentials": { "DB_URL": "prod-db" }
} }

When an agent claims the step, the credential’s secret becomes $DB_URL and is masked in the log. The value is the one in the last column of the kinds table. A named credential overrides a project secret or literal env of the same variable, and a missing one fails the node; it never runs with the variable unset. The variable name follows the same rule as a project secret: SPOOL_* and names that change how a process starts are refused.

A top-level script takes the same injection with a repeatable --credential flag on spool submit:

Terminal window
spool submit agent.py --as script --credential ANTHROPIC_KEY=anthropic

A pipeline names credentials in the document:

credentials:
GITHUB_TOKEN: forge-status # every job in this pipeline
jobs:
publish:
credentials:
REGISTRY_PASS: zot-push # this job as well
steps:
- name: push
run: skopeo copy --dest-creds "ci:$REGISTRY_PASS" ...

Workflow-level entries reach every job; a job entry for the same variable wins, as env: does. The value is a credential name, and it is resolved when an agent claims the job, so ${{ ... }} there is refused at compile. --credential on the command line is for scripts only; a pipeline’s credentials belong to its jobs.

Prefer this over a project secret for anything that would do damage in the wrong job (a deploy key, a signing key, a token that can push). A project secret is in every job’s environment; a declared credential is only in the jobs that asked.

A workflow node running a pipeline stored in a repo takes one for the fetch:

{ "id": "ci", "type": "workflow",
"config": { "from_repo": { "path": ".spool/deploy.yaml",
"repo": "me/private-app",
"credential": "gh-read" } } }

Both repo and credential are required, and the credential must be a bearer or url-token. The fetch doesn’t fall back to a repo you connected with spool repo connect; name both every time.

A repo connected through the GitHub App has a credential nobody stores: github-app:<owner>/<name>, a bearer token for that one repo, made by the app when the job starts and lasting at least 45 minutes. A pipeline job asks for it like any other:

jobs:
release:
credentials:
GH_TOKEN: github-app:me/app

It resolves only in the project that connected the repo through the app. Names starting github-app: are reserved, so no credential or connected account can take one.

A missing credential fails loudly and never falls back to an unauthenticated call:

  • A saved graph whose connector node needs a credential and names none is refused when you save it.
  • Submitting a run checks every credential the graph names, and refuses one that doesn’t exist or is stored as the wrong kind.
  • A name computed at runtime that matches nothing fails the node’s request before anything is sent.

The wrong-kind refusal names the node and both kinds:

node "notify" needs credential "matrix" to be of kind bearer, but it is stored
as api_key - the request would be sent with the wrong kind of auth and
rejected by the service

A draft can be saved before its credentials exist, so you can build first and fill in later: the check happens when you submit a run.

A run from an untrusted event, such as a fork’s pull request, gets no credentials: a step that declares one is refused, with a message naming the untrusted origin. Runs from untrusted events has the rest.

A connected account is an identity on another system that a project borrows through that system’s consent screen instead of a pasted token. Like a credential, it is referenced by name, and the two share one namespace per project, so you can’t have both a credential and an account called github.

A member can connect a Google account under Settings > Connections > Connected accounts, for the Google Sheets connector, or start from the terminal with spool account connect sheets --provider google, which opens that page (--print prints its URL instead). A node references the account by the name it was connected under, the same way it names a credential. spool account list and spool account delete show and remove accounts.