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:
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" } }Setting them
Section titled “Setting them”gh auth token | spool credential set github --kind bearer # token from stdinspool credential set posthog --kind api-key --header X-API-Key # prompts for the valuespool credential set registry --kind basic --username ci # prompts for the passwordspool credential set tg-bot --kind url-token # path secrets, Telegram-stylespool credential list # names + kinds, never valuesspool credential delete githubNo 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.
Where they’re used
Section titled “Where they’re used”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 |
On an http node
Section titled “On an http node”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).
On a connector node
Section titled “On a connector node”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.
In code and shell nodes
Section titled “In code and shell nodes”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:
spool submit agent.py --as script --credential ANTHROPIC_KEY=anthropicIn a pipeline
Section titled “In a pipeline”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.
For fetching a repo
Section titled “For fetching a repo”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.
From the GitHub App
Section titled “From the GitHub App”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/appIt 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.
When one is missing or the wrong kind
Section titled “When one is missing or the wrong kind”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 storedas api_key - the request would be sent with the wrong kind of auth andrejected by the serviceA 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.
Connected accounts
Section titled “Connected accounts”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.