Skip to main content
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

  1. 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 -env variable that is unset or blank fails as ENV_VAR_NOT_SET instead — that check runs first, so an -env source can never produce INPUT_REQUIRED.
  2. Prompts for whatever the API key and API URL still lack — but only when --no-input is absent, no -stdin flag was used anywhere on the line, and stdin is a real terminal. Off that path, an unclaimed API URL silently takes https://api.blobhub.io/v1 with 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.
  3. 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).
  4. Fails with INPUT_REQUIRED if 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.
  5. Calls GET /v1/users/me with the resolved key to validate it and retrieve your user_id, name, and email. A rejection here is AUTH_INVALID.
  6. Writes (or refreshes) identity.yaml inside that profile’s own state tree, guarded against an unexpected identity change (see below):
  7. Adds the profile to ~/.blobhub/credentials.yaml at mode 0600the file blobhub-cli also reads and writes:
    The realtime URL is stored only when one was actually supplied — nothing defaults it in credentials.yaml itself.
Under --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’s identity.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/N confirmation before overwriting.
  • Non-interactively — --no-input, a claimed stdin source, or no terminal at all — there is nobody to ask, so it refuses outright with USER_IDENTITY_MISMATCH, naming --force as the way past it. It never hangs waiting for an answer that cannot come, and never overwrites just because nobody was there to object.
  • --force skips the question either way and overwrites, interactive or not.
This protects against silently swapping the actor identity by switching credentials. The guard is per profile: logging into 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.
A valid key only proves the account can authenticate; it says nothing about what the account can reach. Like any user, a service account sees only the organizations and blobs it has been added as a member of — owning or administering an account grants the account itself no access. If a section refuses with 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 with 403 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.

See also