Skip to main content
The worker authenticates with an API key, and it refers to that key by name. A profile — a name, an API key, the API URL it belongs to, and optionally a realtime (WebSocket) URL — is kept in ~/.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 blobhubblobhub 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 urlrealtime_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:
Each has its own 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.
  1. Profile name--profile, else BLOBHUB_PROFILE, else the default: recorded in the file. A name that is not stored is PROFILE_NOT_FOUND.
  2. Key--api-key, else BLOBHUB_API_KEY, else the resolved profile’s. None of the three is CREDENTIALS_NOT_FOUND.
  3. URL--api-url, else BLOBHUB_API_URL, else the profile’s, else https://api.blobhub.io/v1.
  4. Realtime URLBLOBHUB_REALTIME_URL, else the profile’s realtime_url, else wss://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-url flag — login has a flag of that name too, but it writes a profile rather than resolving one for use.
A container therefore needs no stored profile at all: BLOBHUB_API_KEY on its own is enough.
One difference from the CLI, on purpose. A profile name that is provided but empty is refused rather than falling through to the stored default. --profile "$WORKER_PROFILE" with the variable unset would otherwise start a worker on whatever the default happens to be — and here that selects an entire state tree, not just one command’s credential. An unset BLOBHUB_PROFILE still falls through as normal.
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 exported BLOBHUB_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:
A run whose key came from a flag or the environment reports 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 prod creates a profile this worker can use immediately. Its first start records the identity then, rather than requiring a blobhub-worker login you have effectively already done.
  • blobhub logout --all removes the worker’s credential too.
  • blobhub profile use moves which profile a bare blobhub-worker start resolves.
  • 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 next start as USER_IDENTITY_MISMATCH, which is the right place for it to land: the tree’s cursors belong to the old identity.
There is no lock around the file, so two simultaneous logins lose one profile edit. That was already true of two blobhub logins.

Errors

Full catalog: Reference.

See also