~/.blobhub/credentials.yaml, the same file blobhub-cli reads and writes. One login on a
machine serves both tools.
Four commands cover it: login stores a profile, whoami reports which credential a start would pick up
and where its state lives, and profile ls lists what is stored. start takes the same selection flags as
the rest.
Removing a credential and moving the default belong to
blobhub — blobhub logout and blobhub profile use.
The worker deliberately ships no second writer for verbs the CLI already owns: pipx install blobhub-cli gets
you one. On a worker-only install where pulling in a second package isn’t worth it, editing
~/.blobhub/credentials.yaml by hand works too — it is a default: key and a profiles: map, nothing more.A profile written by an older release of either binary has only
key and url — realtime_url did not
exist yet — and the hazard that creates is not specific to login. Any write to this file, by either
binary, rewrites it whole: login is the only write this worker makes in ordinary use, but start writes
once too, during a pre-profiles migration — and blobhub-cli’s logout and profile use are writes as well,
alongside its own login. An old-release binary doing any of those rewrites the entire file from what it
read, silently dropping realtime_url from every other stored profile, not only the one being touched —
and nothing in the file flags that it happened. Worth knowing if the two binaries are not upgraded together
on one machine.Why a profile is more than a credential here
For the CLI a profile is only a credential: every invocation is short-lived and keeps nothing. The worker is different. Everything it persists — which sessions it watches, how far it has read them, the identity it is pinned to, its single-instance lock — is meaningful only under one identity on one deployment. So a profile here also names a state tree, and each profile gets its own. That is what lets one machine run several workers at once:config.yaml, its own cursors, its own logs, and its own lock. A second worker on the same
profile is still refused with WORKER_ALREADY_RUNNING, and the message names the profile.
Which tree a run uses
A state tree is named after wherever its key came from, not after the profile name that happened to resolve:
The digest is a one-way hash of the key, so the key itself is never a path, never logged and never printed.
This rule exists to make one situation unrepresentable. With
default: prod stored and a different
BLOBHUB_API_KEY exported, the ladder below resolves the name prod and a key from the environment — and
a tree named after prod would put two identities into one set of cursors and one lock. Naming by origin
instead means two credentials can never share a tree.
Two consequences follow. The same key supplied by two different routes gets two trees, which surfaces loudly
as INVALID_CONFIG naming a config.yaml that is not there rather than as silent misbehaviour. And
--profile alongside a supplied key runs in a key tree — the named profile then contributes only its URL,
and the worker says so in its log.
Run whoami to see the resolved path. You need it: a key-derived tree’s name cannot
be guessed, and its config.yaml has to be authored inside it before the first start.
How a credential is resolved
Four values, each highest-precedence first. This is blobhub-cli’s ladder rung for rung.- Profile name —
--profile, elseBLOBHUB_PROFILE, else thedefault:recorded in the file. A name that is not stored isPROFILE_NOT_FOUND. - Key —
--api-key, elseBLOBHUB_API_KEY, else the resolved profile’s. None of the three isCREDENTIALS_NOT_FOUND. - URL —
--api-url, elseBLOBHUB_API_URL, else the profile’s, elsehttps://api.blobhub.io/v1. - Realtime URL —
BLOBHUB_REALTIME_URL, else the profile’srealtime_url, elsewss://realtime.blobhub.io/v1. It resolves alongside the other three on every command, but nothing consumes it yet. Like the CLI, it has no flag rung on this ladder at all: no command that resolves a credential for its own use exposes a--realtime-urlflag —loginhas a flag of that name too, but it writes a profile rather than resolving one for use.
BLOBHUB_API_KEY on its own is enough.
start announces the profile and tree it resolved on its first log line. That is deliberate: blobhub profile use, typed for a CLI reason, moves which profile a bare blobhub-worker start picks up.
blobhub-worker login
Store an API key under a name, and record the identity it belongs to — interactively, or scripted with no
terminal at all.
--api-key still exists — this command line is shared with blobhub-cli — but it is visible in ps and in
shell history; prefer --api-key-stdin or --api-key-env <VAR>. Each of the three values takes exactly one
source, and prompting for whatever is still missing happens only when --no-input is absent, no -stdin
flag was used, and stdin is a real terminal — off that path an unclaimed API URL silently takes its default
and an unclaimed API key fails INPUT_REQUIRED. The first profile stored becomes the file’s default:;
later logins add or replace a profile and leave the default where it is. Full detail, including what
--force overwrites and what --json reports: blobhub-worker login.
Why this matters more for a worker
An exportedBLOBHUB_API_KEY does not stay confined to the worker process that read it. Every coding agent
this worker spawns gets its environment from codeagents/core/env.py, which starts from the worker’s own
os.environ and layers config on top — so whatever variables started the worker are, by default, handed to
every agent turn it runs. The claude_code adapter filters that inheritance, but only for CLAUDECODE and
CLAUDE_CODE_* — variables scoped to an outer Claude Code session, not to BlobHub — so BLOBHUB_API_KEY
passes through untouched. The codex adapter filters nothing at all: every parent variable reaches the
spawned agent, blocklisted or not. An exported key is therefore visible, unfiltered, inside every turn either
harness runs — a much larger blast radius than the one process that read it.
Provisioning a profile once with login and never exporting the key avoids this: the credential lives only
in ~/.blobhub/credentials.yaml, so no agent the worker spawns ever inherits it. This is the strongest
reason to prefer login over BLOBHUB_API_KEY on a worker, and it does not apply to a short-lived
blobhub-cli invocation, which spawns no agents to leak into.
blobhub-worker whoami
Show which credential a start would use, and where that run’s state lives.
It resolves exactly as
start does, calls GET /v1/users/me, and prints six lines:
Profile: key <digest> — there is no worker
equivalent of the CLI’s Profile: (none), because the name is what selects the tree. Every failure a start
would hit surfaces here identically, which is the point of running it first.
Under --json (either whoami --json or --json whoami — see below) the payload
carries profile, label, url, realtime_url, user, state_directory and running as one line.
label is what the human-readable Profile: line above actually prints; the raw profile field beside it
is null for a key-derived tree — the JSON does have a case matching the CLI’s (none), even though the
printed text above never does. It prints through the same writer login uses, which merges in one
schema_version, matching blobhub-cli’s --json rather than contrasting with it. A provisioning script that
just stored a profile can call whoami --json immediately after to confirm what it resolved, without
scraping the human-readable lines.
The two forms of --json
--json is accepted in two positions: after the subcommand (whoami --json, login --json, the form used
throughout this page) or before it (blobhub-worker --json whoami, the form blobhub-cli
takes). Both work identically, on success and on error — an error under either form still carries the
schema_version envelope. blobhub-cli accepts only the root form; the worker accepts both so a script
already written against the subcommand form keeps working. Because the error path is enveloped too, a
provisioning script parsing --json gets a parseable object whether the command it ran succeeded or failed —
no separate branch for “did this fail before it could print JSON.”
blobhub-worker profile ls
List stored profiles, annotated with what this machine knows about each.
* marks the file’s default. A [realtime: …] suffix appears on any profile that has one stored. The
bracket at the end says whether this machine has a state tree for that profile and whether a worker is live
in it — which is what blobhub profile ls structurally cannot show. Key-derived trees are listed too, by
digest, since they are state trees with no name. No network call is made.
Upgrading from a pre-profiles worker
Installs older than v0.5.0 kept everything flat under~/.blobhub-worker/. The first start after upgrading
migrates automatically and prints what it moved: state into profiles/default/, and the credential into
~/.blobhub/credentials.yaml as the profile default. The old copy of the key is removed, not left behind.
It refuses rather than overwrites in two cases — a pre-upgrade worker still running (stop it first), and a
default profile already in the shared store under a different key (MIGRATION_CONFLICT; rename yours or
move it aside).
One case migrates but needs a follow-up, and the migration says so: if the shared file’s default: already
names some other profile, it is left alone — it is not the worker’s to move — so a bare blobhub-worker start will not pick up the tree just migrated. Use --profile default, or move the pointer with blobhub profile use.
Sharing the file with blobhub-cli
Because there is one store, actions in one tool are visible in the other:blobhub login --profile prodcreates a profile this worker can use immediately. Its firststartrecords the identity then, rather than requiring ablobhub-worker loginyou have effectively already done.blobhub logout --allremoves the worker’s credential too.blobhub profile usemoves which profile a bareblobhub-worker startresolves.- blobhub-cli has no identity-change guard on
login— a CLI switches identities by design. So re-logging into a name this worker is pinned to surfaces at its nextstartasUSER_IDENTITY_MISMATCH, which is the right place for it to land: the tree’s cursors belong to the old identity.
blobhub logins.
Errors
Full catalog: Reference.
See also
blobhub-worker login— the command in full, including running as a service account.blobhub-worker start— the selection flags and the preflight order.- Filesystem layout — what a state tree contains.
- blobhub-cli authentication — the other half of the shared store.
- Provisioning — worked recipes for handing this worker a credential from CI, a container, Kubernetes, systemd, or Ansible.

