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

# blobhub-worker login

> Store the API key and validate identity

`blobhub-worker login` is the first-run setup command. It stores your BlobHub API key as a named
[profile](/worker/auth), pulls your user record from BlobHub to validate the key, and records the resulting
durable identity for that profile.

## Synopsis

```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`, `--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):
   ```yaml theme={null}
   user:
     user_id: usr_01J...
     name: Your Name
     email: you@example.com
     recorded_at: 2026-05-27T12:34:56Z
   ```
7. Adds the profile to `~/.blobhub/credentials.yaml` at mode `0600` — **the file
   [blobhub-cli](/cli/auth) also reads and writes**:
   ```yaml theme={null}
   default: prod
   profiles:
     prod:
       key: bhk_...
       url: https://api.blobhub.io/v1
   ```
   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](/worker/auth#the-two-forms-of-json).

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.

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

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

| Code                               | Meaning                                                                                                                                                                                                                                    |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `INPUT_REQUIRED`                   | A claimed literal or stdin source resolved to an empty value, for any of the three; or nothing supplied the API key and prompting was unavailable or left blank. An unset or blank `-env` variable is `ENV_VAR_NOT_SET` below, never this. |
| `ENV_VAR_NOT_SET`                  | An `--api-key-env`, `--api-url-env`, or `--realtime-url-env` flag names a variable that is unset or empty.                                                                                                                                 |
| `AUTH_INVALID`                     | The server rejected the API key (401/403 from `/v1/users/me`).                                                                                                                                                                             |
| `USER_IDENTITY_MISMATCH`           | The server's `user_id` differs from the previously recorded value, and confirmation was declined or unavailable. Re-run with `--force` to overwrite it deliberately.                                                                       |
| `INSECURE_CREDENTIALS_PERMISSIONS` | `~/.blobhub/credentials.yaml` is readable by group or other (refused, never repaired).                                                                                                                                                     |
| `PROFILE_NOT_FOUND`                | `--profile` is not a valid profile name (`[A-Za-z0-9._-]`, 1–64 chars, no `.`/`..`).                                                                                                                                                       |

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

## Running as a service account

For production use, create a dedicated [service account](/general/service-accounts) 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_id`s are now genuinely different. See
[Session Agent Harness → Reference](/worker/session-agent-harness/reference) for the filtering rule itself.

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

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](/rest-api/shared/add-member) — 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](/general/service-accounts#the-access-perimeter).

## See also

* [Authentication and Profiles](/worker/auth)
* [Service Accounts](/general/service-accounts)
* [`blobhub-worker start`](/worker/cli/start)
* [Filesystem layout](/worker/filesystem)
