> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication and Profiles

> Store a credential, see which one a start would use, and run several workers on one machine

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](/cli/auth) 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.

<Note>
  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.
</Note>

<Note>
  **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.
</Note>

## 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:

```bash theme={null}
blobhub-worker start --profile prod &
blobhub-worker start --profile staging &
```

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 key came from                | The tree is                          |
| -------------------------------- | ------------------------------------ |
| a stored profile                 | `~/.blobhub-worker/profiles/<name>/` |
| `--api-key` or `BLOBHUB_API_KEY` | `~/.blobhub-worker/keys/<digest>/`   |

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`](#blobhub-worker-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](/cli/configuration) 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 URL** — `BLOBHUB_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.

<Warning>
  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.
</Warning>

`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.

```bash theme={null}
blobhub-worker login [--profile <name>]
                     [--api-key <key> | --api-key-stdin | --api-key-env <VAR>]
                     [--api-url <url> | --api-url-stdin | --api-url-env <VAR>]
                     [--realtime-url <url> | --realtime-url-stdin | --realtime-url-env <VAR>]
                     [--no-input] [--force] [--json]
```

| Flag                   | Default                        | Description                                                               |
| ---------------------- | ------------------------------ | ------------------------------------------------------------------------- |
| `--profile`            | `default`                      | Name to store this profile under.                                         |
| `--api-key`            | none                           | API key. Visible in `ps` and in shell history — prefer `--api-key-stdin`. |
| `--api-key-stdin`      | off                            | Read the API key from stdin.                                              |
| `--api-key-env`        | none                           | Read the API key from environment variable `VAR`.                         |
| `--api-url`            | `https://api.blobhub.io/v1`    | Base API URL to log in against.                                           |
| `--api-url-stdin`      | off                            | Read the API URL from stdin.                                              |
| `--api-url-env`        | none                           | Read the API URL from environment variable `VAR`.                         |
| `--realtime-url`       | `wss://realtime.blobhub.io/v1` | Realtime (WebSocket) API URL to store on the profile.                     |
| `--realtime-url-stdin` | off                            | Read the realtime URL from stdin.                                         |
| `--realtime-url-env`   | none                           | Read the realtime URL from environment variable `VAR`.                    |
| `--no-input`           | off                            | Never prompt; fail instead of asking.                                     |
| `--force`              | off                            | Overwrite a recorded identity mismatch instead of refusing.               |
| `--json`               | off                            | Emit machine-readable JSON.                                               |

`--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`](/worker/cli/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.

```bash theme={null}
blobhub-worker whoami [--profile <name>] [--api-key <key>] [--api-url <url>] [--json]
```

| Flag        | Default                                                                  | Description                                  |
| ----------- | ------------------------------------------------------------------------ | -------------------------------------------- |
| `--profile` | the stored default                                                       | Profile to resolve.                          |
| `--api-key` | from the environment, then the profile                                   | API key, overriding profile and environment. |
| `--api-url` | from the environment, then the profile, then `https://api.blobhub.io/v1` | API URL, overriding profile and environment. |
| `--json`    | off                                                                      | Emit machine-readable JSON.                  |

It resolves exactly as `start` does, calls `GET /v1/users/me`, and prints six lines:

```text theme={null}
Profile: prod
URL: https://api.blobhub.io/v1
Realtime: wss://realtime.blobhub.io/v1
User: Ada Lovelace (<user id>)
State: /home/you/.blobhub-worker/profiles/prod
Running: no
```

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-two-forms-of-json)) 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](/cli/introduction)
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.

```bash theme={null}
blobhub-worker profile ls
```

```text theme={null}
* prod (https://api.blobhub.io/v1) [realtime: wss://realtime.blobhub.io/v1] [running]
  staging (https://api.blobhub.io/v1) [state]
  ci (https://api.blobhub.io/v1) [no state]
  (key 9f31c0a2b4d67e18) [state]
```

`*` 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 login`s.

## Errors

| Code                               | When                                                                                                                                                                                                                       |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INPUT_REQUIRED`                   | A `login` value's claimed literal or stdin source resolved to an empty value, or nothing supplied the API key and prompting was unavailable or left blank. An unset or blank `-env` variable is `ENV_VAR_NOT_SET` instead. |
| `ENV_VAR_NOT_SET`                  | A `login` `--*-env` flag names an environment variable that is unset or empty.                                                                                                                                             |
| `AUTH_INVALID`                     | `GET /v1/users/me` rejected the key.                                                                                                                                                                                       |
| `PROFILE_NOT_FOUND`                | A named profile is not stored, or the name is not a valid profile name.                                                                                                                                                    |
| `CREDENTIALS_NOT_FOUND`            | No key from flags, environment or the file — or the file is present but malformed.                                                                                                                                         |
| `INSECURE_CREDENTIALS_PERMISSIONS` | `~/.blobhub/credentials.yaml` is readable by group or other. Refused, never repaired.                                                                                                                                      |
| `USER_IDENTITY_MISMATCH`           | The server's user differs from the one recorded for this profile, and neither confirmation nor `--force` overwrote it.                                                                                                     |
| `MIGRATION_CONFLICT`               | A pre-profiles migration found a different `default` already stored.                                                                                                                                                       |

Full catalog: [Reference](/worker/reference).

## See also

* [`blobhub-worker login`](/worker/cli/login) — the command in full, including running as a service account.
* [`blobhub-worker start`](/worker/cli/start) — the selection flags and the preflight order.
* [Filesystem layout](/worker/filesystem) — what a state tree contains.
* [blobhub-cli authentication](/cli/auth) — the other half of the shared store.
* [Provisioning](/general/provisioning) — worked recipes for handing this worker a credential from CI, a
  container, Kubernetes, systemd, or Ansible.
