blobhub-worker login is the first-run setup command. It stores your BlobHub API key as a named
profile, pulls your user record from BlobHub to validate the key, and records the resulting
durable identity for that profile.
Synopsis
--api-key, --api-url and --realtime-url each take exactly one of their three sources — the literal
flag, its -stdin sibling, or its -env <VAR> sibling. Naming two for the same value, or claiming stdin
with more than one -stdin flag anywhere on the line, is a usage error, exit 2, checked before anything
is read. --api-key-env and its two siblings always take the variable’s name; none has a bare form that
falls back to a fixed variable. Nothing here opens a WebSocket with the realtime URL yet — it is only
stored and reported, so it is in place when something does.
--api-key still exists, and the warning is unchanged: it is visible in ps output and left behind in
shell history. It exists only because this command line is shared with blobhub-cli, whose own scripts
already pass credentials this way. A script written against this worker should reach for
--api-key-stdin or --api-key-env <VAR> instead — neither ever touches the process table or your shell
history.
What it does
- Resolves the API key, API URL, and realtime URL, each independently from its own three
sources. An empty literal or an empty pipe fails immediately as
INPUT_REQUIRED; an-envvariable that is unset or blank fails asENV_VAR_NOT_SETinstead — that check runs first, so an-envsource can never produceINPUT_REQUIRED. - Prompts for whatever the API key and API URL still lack — but only when
--no-inputis absent, no-stdinflag was used anywhere on the line, and stdin is a real terminal. Off that path, an unclaimed API URL silently takeshttps://api.blobhub.io/v1with no prompt and no error; an unclaimed API key is the one case with no such fallback (see step 4). The realtime URL is never prompted for, on any path — supply it with a flag or leave the profile without one. - When prompting is allowed, asks for the API URL on stderr first, offering that same default; press
Enter to take it. Then asks for the API key (read with
getpass, so it is not echoed to the terminal or left in shell history). - Fails with
INPUT_REQUIREDif the key is still unresolved here — no source claimed it and prompting wasn’t available — or if you were prompted for it and entered nothing. Either way, this happens before any network call. - Calls
GET /v1/users/mewith the resolved key to validate it and retrieve youruser_id,name, andemail. A rejection here isAUTH_INVALID. - Writes (or refreshes)
identity.yamlinside that profile’s own state tree, guarded against an unexpected identity change (see below): - Adds the profile to
~/.blobhub/credentials.yamlat mode0600— the file blobhub-cli also reads and writes:The realtime URL is stored only when one was actually supplied — nothing defaults it incredentials.yamlitself.
--json the payload carries profile, url, realtime_url, user (the record GET /v1/users/me
returned), and state_directory (that profile’s tree — see step 6) — printed as one JSON line carrying one
schema_version, the same envelope blobhub-cli’s --json emits. The key is not among them, here or anywhere
else. --json works both after the command (login --json, as above) and before it
(blobhub-worker --json login …, the position blobhub-cli uses) — see Authentication and
Profiles.
The first profile stored becomes the file’s default:; later logins add or replace a profile and leave
the default where it is. Move it with blobhub profile use. The command is idempotent — running it again with
the same key under the same name is a no-op refresh.
Logging in under a new name is how you run several workers on one machine. Each profile owns its own
config.yaml, cursors and lock, so --profile prod and --profile staging run side by side without
contending. See Authentication and Profiles.Identity-change guard
If the profile’sidentity.yaml already records a user_id and the new API key resolves to a different
user_id, the command does not overwrite it unannounced:
- Interactively, it warns and asks for
y/Nconfirmation before overwriting. - Non-interactively —
--no-input, a claimed stdin source, or no terminal at all — there is nobody to ask, so it refuses outright withUSER_IDENTITY_MISMATCH, naming--forceas the way past it. It never hangs waiting for an answer that cannot come, and never overwrites just because nobody was there to object. --forceskips the question either way and overwrites, interactive or not.
staging neither consults nor overwrites what prod recorded.
Note that blobhub-cli’s login has no such guard — a CLI switches identities by design — so a re-login there
under a name this worker is pinned to is caught later, at the worker’s next start, as
USER_IDENTITY_MISMATCH.
Errors
Full descriptions: Reference.
Running as a service account
For production use, create a dedicated service account in BlobHub and authenticate the worker with its key instead of a person’s. Nothing about this command changes: supply the service account’s key exactly as you would a human’s — at the prompt, or via--api-key-stdin/
--api-key-env for a scripted run — login does not know or care whose key it is, and every step above runs
exactly as written. Give it its own name (login --profile ci) and it gets its
own state tree, which is what lets a service-account worker run beside a personal one on the same machine.
From then on, everything the worker produces — thread items it posts, session objects it writes, revisions it
commits — is authored by the service account’s user_id, not by whoever minted the key. That is also what
makes self-filtering work as intended: a human posting into a thread under their own identity is no longer
mistaken for the worker’s own output, because the two user_ids are now genuinely different. See
Session Agent Harness → Reference for the filtering rule itself.
Verified against a live service account: step 5’s
GET /v1/users/me resolves its id and name exactly
like a human user’s. A service account has no email at all — the field is simply absent from the response
— and login handles that the same way it handles any other optional field, defaulting identity.yaml’s
email to "" rather than failing.SESSION_NOT_FOUND right after you switch to a service-account
key, check its memberships before suspecting the key: a bad key fails here, at login, not later at section
attach. Revoking access is symmetric — removing the account’s membership on an organization or blob revokes
the worker’s reach immediately, with no key to reissue or rotate.
What the key can’t do
This is the reason to prefer a service account on a machine you don’t fully control. The account’s own key does all of the worker’s work — attaching to sections, posting thread items, writing session objects, committing revisions — but it cannot change who can access what. Ten operations refuse it with403
whatever role it was minted with: minting or revoking API keys, storing or deleting credentials, granting or
revoking memberships, creating or retiring accounts, minting an acting token, and changing an organization’s
or blob’s visibility.
So a key lifted off that machine buys exactly the reach the account already had, and no way to extend it —
no longer-lived replacement key, no new membership, no second account to survive your rotating the leaked
one, and no way to make a private blob public. Revoke the key and the access ends in one step. Full model:
The Access Perimeter.

