Running your own agent
An agent of your own is the spool binary running spool agent run on
a machine you control: a server in your network, an arm64 box, a Mac, a
pod. It dials out to Hyperspool, so it needs no inbound ports, and it
takes the steps that ask for capabilities it advertises.
Agents says when
that is worth doing instead of using managed environments.
Getting one running takes two commands, one to mint a token and one to start the agent with it:
spool agent token create build-1 --scope acme/web --cap shell --cap build > ~/agent-tokenspool agent run https://api.hyperspool.dev build-1 \ --cap shell --cap build --token-file - < ~/agent-tokenMint a token
Section titled “Mint a token”An org owner issues an agent token, naming the agent id and the capabilities the token may advertise:
spool agent token create build-1 \ --name "build agent (hel1)" \ --scope acme/web \ --expires-in 90d \ --cap shell --cap polyglot --cap buildThe command prints the token once; store it now. Agent tokens start
hsat_ (csat_ on older ones, which still work).
The scope decides whose work the agent may claim, and so whose secrets it receives:
--scope <org/project>binds the token to that one project.--scope <org>binds it to every project in that org.
Either needs the owner role in that org. See
teams and projects.
The capabilities are a ceiling: an agent can advertise fewer at runtime
but never more, so a leaked token can’t claim work you didn’t grant it.
--expires-in puts a clock on it, and a token minted with a personal
access token never outlives that token.
Agent ids are unique across Hyperspool, so an id another org already
holds comes back as a conflict; pick another. Minting again for an id
your scope already holds rotates its token. To pull a leaked token
before it expires, find its id with spool agent token list and revoke
it with spool agent token delete <token-id>.
The list says who minted each token. A token keeps working after the person who minted it leaves the org or has their account disabled, because the team’s runners are often using it. Look for tokens minted by people who have gone, and rotate them.
One token for several machines
Section titled “One token for several machines”Running the same agent on several machines, or scaling a Kubernetes Deployment, doesn’t need a token each. Mint one for a pool:
spool agent token create --pool build \ --name "build agents" \ --scope acme/web \ --cap shell --cap polyglot --cap buildIt authenticates as any agent id beginning build-, so each process
still has an id of its own:
spool agent run https://api.hyperspool.dev build-$(hostname) --cap shellOn Kubernetes, name the pool after the Deployment and pass the pod name through:
env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.nameargs: ["agent", "run", "https://api.hyperspool.dev", "$(POD_NAME)", "--cap", "shell"]spool agent list shows the pool as one line with a replica count. One
credential backs every replica, so --expires-in is what bounds a leak.
A pool name and an agent id can’t overlap: minting the pool build is
refused while an agent build-1 exists, and the other way round.
Run an agent
Section titled “Run an agent”spool agent run https://api.hyperspool.dev build-1 \ --cap shell --cap polyglot --cap build \ --token-file - < ~/agent-tokenThe arguments are the server URL and the agent id. The id can come from
$HYPERSPOOL_AGENT_ID instead, which suits a scheduler that varies the
environment per replica but not the command line.
The token comes from one of two places:
--token-file -reads it from stdin, and--token-file <path>reads it from a file and then deletes the file, so a job started later can’t find it there.- Without the flag the agent reads
$HYPERSPOOL_AGENT_TOKENand removes it from its own environment at once. The variable stays visible wherever the environment is recorded (docker inspect, a pod spec), so prefer the file when you can.
Capabilities come from repeated --cap flags, else
$HYPERSPOOL_AGENT_CAPABILITIES (comma-separated), else shell,polyglot.
The token still caps whatever you pick.
The agent itself is one static binary. The languages your steps use
(bash, python, node, go, …) must be on the host’s PATH.
What a job sees
Section titled “What a job sees”Jobs run in a scrubbed environment. The agent keeps a small allowlist
(USER, LOGNAME, SHELL, LANG, TERM, TMPDIR, the TZ and
locale vars, and the TLS certificate paths SSL_CERT_FILE,
SSL_CERT_DIR and NIX_SSL_CERT_FILE), then adds PATH, PYTHONPATH,
NODE_PATH, the pipeline’s env: block and the run’s SPOOL_* vars.
Anything else in the agent’s environment stays out, so a variable you
forgot about is dropped rather than forwarded.
To give a job a variable of its own, set it in the pipeline’s env:
block. The value reaches the job’s process as $NAME. Expressions can’t
read it, since a job’s if: is decided before the job reaches an agent;
a constant a condition needs belongs in the pipeline’s vars: block.
Each job gets an empty HOME of its own, deleted when the job ends, and
XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME and XDG_STATE_HOME
point inside it. The job starts in that directory, so relative paths in
a step resolve there, and caches under ~ start cold in every job.
Choose what HOME jobs get shows how to
change that.
Hardening
Section titled “Hardening”Jobs run as the agent’s user unless you say otherwise, on a machine that also holds the agent’s token. The defaults keep the token out of their reach as far as a same-user process allows; the settings below go further. Use them on any agent that runs work other people can submit.
Keep the token away from jobs
Section titled “Keep the token away from jobs”The agent hides itself from its jobs, so they can’t read its memory or
the environment it started with through /proc. They can read any file
the agent’s user can, though. Don’t point --token-file at a file the
agent can’t delete, such as a Kubernetes Secret volume (read-only): the
token stays there for every job to read, and $HYPERSPOOL_AGENT_TOKEN
from the Secret is the better choice.
None of this holds against root, so an agent whose jobs run as root gives them its token.
Run jobs as a user of their own
Section titled “Run jobs as a user of their own”On Linux, an agent running as root can start every job as another user:
useradd --create-home spool-jobspool agent run https://api.hyperspool.dev build-1 \ --cap shell --cap build --step-user spool-job \ --token-file - < ~/agent-token$SPOOL_STEP_USER does the same, which suits a systemd unit: add
Environment=SPOOL_STEP_USER=spool-job in a drop-in for
spool-agent.service. The agent keeps the token as root, and each job
process runs as spool-job with that user’s groups, so a job can’t read
the agent’s files, memory or /proc entries. Files a job hands the agent
by path (spool artifact put, spool checkout) are read and written
with the job’s own access, and what the agent writes for a job belongs
to it.
Give the step user only what jobs need: its own home, the groups for
devices a build uses (kvm), subordinate ids if jobs run rootless
containers, and no sudo. Anything that needs root, such as installing
system packages or writing /etc, belongs in the machine’s setup rather
than in a job.
The agent refuses to start when it isn’t root, when the user doesn’t
exist or is root, and with SPOOL_JOB_HOME=agent (below), which would
hand jobs root’s HOME.
Without a step user, a job runs as the agent’s user and can read that user’s files if it knows where they are. An agent that runs jobs other people can submit should then run as a user of its own, with nothing else in its home.
Choose what HOME jobs get
Section titled “Choose what HOME jobs get”The empty HOME per job keeps spool, git and cloud CLIs in a job from
picking up your stored token or credentials just because you started the
agent from your own account. ssh finds keys through the user database
rather than HOME, so this doesn’t hide ~/.ssh.
SPOOL_JOB_HOME on the agent changes it:
SPOOL_JOB_HOME |
Jobs get | For |
|---|---|---|
| unset | an empty HOME each |
an agent you run from your own account |
agent |
the agent’s own HOME, and start there |
a machine or account that exists only to run jobs, whose HOME holds caches and tool config |
a directory, e.g. /var/lib/spool-jobs |
that directory, shared | warm caches, when every job comes from people you trust |
The agent refuses agent while a spool config lives under its HOME,
and a shared directory that is its own HOME or holds its spool config;
either way it fails the job with the reason. With --step-user, a shared
directory should belong to the step user, or jobs can’t write to it.
Every job can write to a shared or agent HOME, and the next job reads
what it left: a .gitconfig, a .npmrc, a shell profile. Use either
only on an agent whose jobs all come from people you trust.
Pass environment variables through
Section titled “Pass environment variables through”To hand jobs a variable from the agent’s own environment, set
SPOOL_ENV_PASSTHROUGH on the agent to a comma-separated list of names
or globs (MYAPP_*,REGION). Matching variables reach every job that
agent runs; the agent token is never passed through.
Every job on the agent sees what you pass through, so keep secrets out of it and use project secrets or credentials instead.
Work from untrusted events
Section titled “Work from untrusted events”An agent of yours takes work from untrusted events too: a fork’s pull
request, a comment from someone outside the repo. That work gets none
of the project’s secrets, and a step that declares a credential is
refused, but its code runs on your machine as the agent’s user (or the
step user). Keep a token whose scope receives such events on a machine
where that is acceptable, and use a step user and an empty HOME there.
Install as a systemd service
Section titled “Install as a systemd service”curl -fsSL https://hyperspool.com/install.sh | sh -s -- \ --service \ --server https://api.hyperspool.dev \ --id build-1 \ --token-file /root/agent-token \ --caps shell,polyglot,build--token-file keeps the token off the command line, where ps and
cloud-init logs would show it; --token hsat_... also works. Without
--service the script only installs the spool binary and prints the
spool agent run command.
The unit is spool-agent.service. The token is in
/etc/spool-agent.token (mode 0600), which systemd hands the agent on
stdin, and the capabilities are in /etc/spool-agent.env:
systemctl status spool-agentjournalctl -u spool-agent -fIt runs as a throwaway DynamicUser. An agent whose steps install
their own toolchain (Nix, devenv) needs a persistent home, so pass
--user root or a real account. With --user root its jobs run as root
and can read the token file, so keep such an agent to work you trust, or
give it a step user.
On macOS
Section titled “On macOS”--service needs systemd, so on macOS the script prints the
spool agent run command and exits with an error. Supervise the agent
yourself, for example with a launchd job, and set
HYPERSPOOL_AGENT_UPDATE_URL if you want it to
update itself.
Run one in a container
Section titled “Run one in a container”There is no published agent image. The agent is a single static binary, so the Dockerfile is short:
FROM docker.io/library/debian:stable-slimRUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates bash git python3 \ && rm -rf /var/lib/apt/lists/*ADD https://get.hyperspool.dev/linux/spool-amd64 /usr/local/bin/spoolRUN chmod +x /usr/local/bin/spoolENTRYPOINT ["spool", "agent", "run"]docker run -d --restart unless-stopped \ -e HYPERSPOOL_AGENT_TOKEN=hsat_... \ my-registry/spool-agent:2026-09-12 \ https://api.hyperspool.dev build-1 --cap shell --cap polyglotPut the interpreters your steps use in the image, and keep --cap to
what it can run. polyglot covers every script language, so the image
above runs a python script and fails a node one.
Pass the token at run time, never in an image layer. The agent hides
its environment from its jobs, but anyone who can run docker inspect
on the host sees the variable. In a container, leave
HYPERSPOOL_AGENT_UPDATE_URL unset: a restart re-runs the same image, so
roll the image tag instead.
Self-update
Section titled “Self-update”The systemd service fetches the published binary on every start and
checks it against a signed .sha256 manifest before launching. A running
agent with HYPERSPOOL_AGENT_UPDATE_URL set (the installer sets it)
polls that manifest every 5 minutes. When a newer build ships it
finishes its current step and exits, systemd restarts it, and the
pre-start fetch installs the new binary. An unreachable download host, a
bad signature or a checksum mismatch keeps the current binary, so a
restart always succeeds.
Under another supervisor, set HYPERSPOOL_AGENT_UPDATE_URL and make
sure something restarts the agent after it exits: exiting is the update.
Seeing your agents
Section titled “Seeing your agents”spool agent list lists your agents: id, status, capabilities, scope,
build id and when each was last seen. Managed environments show there
too while they run, and drop off a day after they finish
(--include-finished keeps them). A build id marked outdated differs
from the agent seen most recently, which during a rollout shows who
hasn’t picked up the new binary yet.
An agent that stops cleanly leaves the list at once. One that dies without a word drops off after a day of silence; a running agent reports at least every 25 seconds, so idling never costs it its entry. An agent id with no last-seen time has a token but nothing running under it.
Stopping an agent
Section titled “Stopping an agent”Stop your agent the way you stop any process: systemctl stop spool-agent, docker stop, Ctrl-C. On SIGTERM it gives the step in
flight about ten seconds to finish, then kills it and exits, and that
step goes back to the queue.
An agent that dies mid-step loses the step once its lease lapses, and
another capable agent (or a managed environment) runs it again from the
start; When an agent dies has the
timings. Completed spool step calls inside a script return their
recorded output instead of running twice.
A step on an agent of yours has a one-hour ceiling unless the job sets
timeout:, up to 24 hours. Past it, the step’s processes are killed and
the step fails. The agents inside spool run and spool dev have no
ceiling.
Starting your own environments on demand
Section titled “Starting your own environments on demand”spool agent dispatch is what starts managed environments, and you can
run one for your org. It watches for work none of your agents can pick up
and runs a launch command you supply to bring up an agent for it:
spool agent token create dispatcher-1 --scope acme --cap dispatchexport HYPERSPOOL_AGENT_TOKEN=hsat_...spool agent dispatch https://api.hyperspool.dev \ --provides shell --provides polyglot -- ./launch.shThe launch command gets a JSON document on stdin (agent id, token,
scope, capabilities, expiry) and must start an agent with that id and
token, exiting non-zero if it couldn’t. An org-scoped dispatch token
serves only that org. --provides names what your environments can run,
once per capability.