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

> Store credentials, inspect the identity a command runs as, and switch between profiles

Five commands decide who the CLI is. `login` stores a credential, `whoami` reports the identity a command would
run as right now, `logout` removes one, and `profile ls` / `profile use` list and switch between the credentials
you have stored.

A credential is a **profile** — an API key, the API URL it belongs to, and optionally a realtime (WebSocket)
URL — kept in `~/.blobhub/credentials.yaml`, written atomically at mode `0600` and refused on load if any group
or other permission bit is set. The CLI never repairs the mode for you, and it never prints a key: not in
command output, not in an error message, not in the `--json` payload.

<Note>
  **That file is shared with [`blobhub-worker`](/worker/auth).** One `login` on a machine serves both binaries,
  and both read the same `default:` and `profiles:` map. Three consequences worth knowing before you run the
  commands below: `logout --all` removes the worker's credential too; `profile use` moves which profile a bare
  `blobhub-worker start` picks up; and **any write to this file, by either binary** — this CLI's `login`,
  `logout` and `profile use` alike, the worker's own `login`, or a one-time pre-profiles migration on the
  worker's `start` — rewrites the whole file from what it read. From an **older release of either binary**, that
  rewrite silently drops `realtime_url` from *every* stored profile, not only the one being touched — worth
  knowing if the two binaries are not upgraded together on one machine. The worker also keeps a separate body of
  local state per profile, which is why it — unlike this CLI — cares which name you log in under.
</Note>

In CI you need none of these commands. `BLOBHUB_API_KEY` resolves on its own, with no credentials file present at
all and with no warning — see [Configuration](/cli/configuration) for the full precedence ladder. The other
option is to script `login` itself: `--api-key-stdin` (or `--api-key-env`) with `--no-input` stores a validated
profile with no terminal involved, which is worth the extra step when the automation needs a **named, stored**
identity rather than a key that lives only in one process's environment.

## `blobhub login`

Authenticate and store the result as a named profile — interactively, or scripted with no terminal at all.

### Synopsis

```bash theme={null}
blobhub 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]
```

| Flag                   | Default                           | Description                                                               |
| ---------------------- | --------------------------------- | ------------------------------------------------------------------------- |
| `--profile`            | `BLOBHUB_PROFILE`, else `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.                                     |

`--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 — and the three are mutually exclusive: 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 in this workspace opens a WebSocket with the
realtime URL yet — it is only stored and reported, so it is in place when something does.

### What it does

1. Resolves the API key, API URL, and realtime URL, each independently from its own three sources. A source
   that resolves to an empty value — an empty literal, an empty pipe, or a `-env` variable that is unset or
   blank — fails immediately, as `INPUT_REQUIRED` (or `ENV_VAR_NOT_SET` for the variable case).
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 without echoing it, so it never appears on screen or in your
   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. Validates the key against `GET /v1/users/me`. A rejection there is `AUTH_INVALID`.
6. Writes the profile into `~/.blobhub/credentials.yaml` at `0600` and prints the identity it resolved:

```text theme={null}
Logged in as Ada Lovelace (<user id>), profile 'default'
```

Under `--json` the payload carries `profile`, `url`, `realtime_url`, and `user` — the record `GET
/v1/users/me` returned — plus the envelope's `schema_version`. The key is not among them, here or anywhere
else. `realtime_url` here always reports a concrete value, falling back the same way the flag does; the
**stored** profile only gets a `realtime_url` entry when one was actually supplied — nothing defaults it in
`credentials.yaml` itself.

The **first** profile you store becomes the default. Later logins add or replace a profile but leave the default
where it is; move it with [`blobhub profile use`](#blobhub-profile-use).

Unlike `blobhub-worker`, `login` has **no identity-change guard**. Logging in again — under the same profile name
or a new one — simply replaces or adds a profile, whoever the new key belongs to. A CLI switches identities by
design; a worker bound to one identity for the life of a process does not.

Because the store is shared, that has a consequence outside this binary: re-logging into a name a worker is
pinned to is caught at *its* next `start`, as `USER_IDENTITY_MISMATCH`. Nothing is lost — that is the right
place for it to surface, since the worker's stored cursors belong to the old identity.

### Errors

| Code                               | When                                                                                                                                                                                                                             |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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` instead. |
| `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`                     | `GET /v1/users/me` rejected the key.                                                                                                                                                                                             |
| `INSECURE_CREDENTIALS_PERMISSIONS` | An existing `credentials.yaml` is readable by group or other. `chmod 600` it.                                                                                                                                                    |
| `CREDENTIALS_NOT_FOUND`            | An existing `credentials.yaml` is not valid YAML, or a stored profile is malformed.                                                                                                                                              |

That last row is worth reading twice: `CREDENTIALS_NOT_FOUND` covers a file that is present but unusable, not only
one that is absent.

### Using a service-account key

`login` doesn't distinguish a human's key from a [service account](/general/service-accounts)'s — store one
the same way, under whatever `--profile` name fits: `blobhub login --profile ci`, pasting the account's key
at the prompt. The identity line it prints resolves to the account's own name and id, and every command you
later run under that profile — `--profile ci`, or `BLOBHUB_PROFILE=ci` — acts as the account, not as whoever
minted the key. See [`blobhub whoami`](#blobhub-whoami) below for what else is different about a key scoped
this way.

## `blobhub logout`

Remove a stored profile, or every profile with `--all`.

### Synopsis

```bash theme={null}
blobhub logout [--profile <name>] [--all]
```

| Flag        | Default             | Description                  |
| ----------- | ------------------- | ---------------------------- |
| `--profile` | the default profile | Profile to remove.           |
| `--all`     | off                 | Remove every stored profile. |

### What it does

Removes the named profile — or the default one, when you name none — and rewrites the credentials file. With
`--all`, every profile goes and the default is cleared. One line is printed per profile removed:

```text theme={null}
Removed profile 'staging'
```

Under `--json` the payload carries `removed`, the list of profile names that went — empty when there was nothing
to remove.

`logout --all` also removes any credential `blobhub-worker` was using, since there is one store. Its next
`start` then fails `CREDENTIALS_NOT_FOUND` — for a reason recorded in this binary's history, not its own.

Removing the **default** profile leaves the installation with no default at all, and nothing chooses a new one:
a later `login` sets the default only when no other profile exists. Until you run `blobhub profile use`, every
command must name its profile with `--profile` or `BLOBHUB_PROFILE`, or supply a key some other way.

### Errors

| Code                               | When                                                                            |
| ---------------------------------- | ------------------------------------------------------------------------------- |
| `PROFILE_NOT_FOUND`                | No profile by that name — or no default profile to remove, when you named none. |
| `INSECURE_CREDENTIALS_PERMISSIONS` | `credentials.yaml` is readable by group or other.                               |
| `CREDENTIALS_NOT_FOUND`            | `credentials.yaml` is present but unparseable or malformed.                     |

`logout --all` against no credentials file at all is not an error: nothing is removed, nothing is printed, and the
exit code is `0`.

## `blobhub whoami`

Show the resolved identity, profile, and key scope.

### Synopsis

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

| Flag        | Default                                                                  | Description                                  |
| ----------- | ------------------------------------------------------------------------ | -------------------------------------------- |
| `--profile` | the default profile                                                      | 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. |

### What it does

Resolves a key, a URL and a realtime URL exactly as every other command does, calls `GET /v1/users/me` with
them, then probes `GET /v1/users/me/orgs` to characterise the key's scope, and prints five lines:

```text theme={null}
Profile: default
URL: https://api.blobhub.io/v1
Realtime: wss://realtime.blobhub.io/v1
User: Ada Lovelace (<user id>)
Scope: full
```

Under `--json` the payload carries `profile` (`null` when no profile was involved), `url`, `realtime_url`,
`user` and `scope`, plus the envelope's `schema_version`.

This is the command to run when you are unsure which credential a command will pick up. `Profile: (none)` means
no profile was involved at all — the key came from `--api-key` or `BLOBHUB_API_KEY`.

### The scope line

| Scope        | What it means                                                                   |
| ------------ | ------------------------------------------------------------------------------- |
| `full`       | The probe succeeded: the key can list the user's orgs.                          |
| `restricted` | The probe returned `403`. The key is org- or blob-scoped.                       |
| `unknown`    | The probe failed for some other reason. Raises the `SCOPE_UNVERIFIED` advisory. |

**A `restricted` key is a valid key.** By the time the probe runs, `GET /v1/users/me` has already accepted it —
the probe is enrichment, not authentication. `restricted` is the expected reading for exactly the org- and
blob-scoped keys the platform recommends for automation, and such a key drives every command here except the
`admin`-scoped reads.

`unknown` says nothing about the key either. It means the probe itself failed — a network error, or a server
error on that one route — so the CLI reports what it observed rather than guessing `full`.

A [service account](/general/service-accounts)'s key is a **user-scoped** key — the kind [Create API
Key](/rest-api/shared/create-api-key) mints when you target it directly — and it reports `full` here too, for
the same reason a personal key does: the probe only needs the key to authenticate as a user, not to carry any
particular role. Don't read `full` as "more powerful" than `restricted`; it means "scoped to a user," nothing
about capability.

What that scope still can't do is change **who can access what**. Ten operations — minting and revoking API
keys, storing and deleting credentials, granting and revoking memberships, creating and retiring accounts,
minting an acting token, and changing an org's or blob's `visibility` — require a human behind the request,
and a service account is never one. The same key that reports `full` here gets `403` from every one of them,
regardless of the role it was minted with. Everything else is unaffected: `blobhub workflow deploy`,
`scheduler deploy`, every push, pull and execute in this binary is ordinary product work and runs exactly as
it does under a personal key. See
[The Access Perimeter](/general/service-accounts#the-access-perimeter) for the full model and for
impersonation, the way across it.

### Errors

| Code                               | When                                                                  |
| ---------------------------------- | --------------------------------------------------------------------- |
| `AUTH_INVALID`                     | `GET /v1/users/me` rejected the key.                                  |
| `CREDENTIALS_NOT_FOUND`            | No key from flags, environment, or credentials file.                  |
| `PROFILE_NOT_FOUND`                | `--profile` or `BLOBHUB_PROFILE` names a profile that is not stored.  |
| `INSECURE_CREDENTIALS_PERMISSIONS` | `credentials.yaml` is readable by group or other.                     |
| `SCOPE_UNVERIFIED`                 | Advisory, on stderr: the scope probe failed. The exit code stays `0`. |

## `blobhub profile ls`

List stored profiles, marking the default.

### Synopsis

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

Takes no flags.

### What it does

Reads the credentials file — no network call — and prints one line per profile in name order, with `*` against
the default and a `[realtime: …]` suffix on any profile that has one stored:

```text theme={null}
* default (https://api.blobhub.io/v1)
  staging (https://api.staging.example.com/v1) [realtime: wss://realtime.staging.example.com/v1]
```

Under `--json` the payload carries `profiles` — one entry per profile holding `name`, `url`, `realtime_url`
(`null` when that profile never stored one) and a boolean `default` — and `default`, the default profile's name
or `null`. No key appears in either.

No profiles, or no credentials file at all, prints nothing and exits `0`.

### Errors

| Code                               | When                                                        |
| ---------------------------------- | ----------------------------------------------------------- |
| `INSECURE_CREDENTIALS_PERMISSIONS` | `credentials.yaml` is readable by group or other.           |
| `CREDENTIALS_NOT_FOUND`            | `credentials.yaml` is present but unparseable or malformed. |

## `blobhub profile use`

Set the default profile.

### Synopsis

```bash theme={null}
blobhub profile use <name>
```

| Argument | Default  | Description                       |
| -------- | -------- | --------------------------------- |
| `name`   | required | Profile name to make the default. |

### What it does

Points the credentials file's `default` at an already-stored profile, and prints:

```text theme={null}
Default profile set to 'staging'
```

Under `--json` the payload carries `default`, the profile name now set as the default.

It stores nothing new: the profile must exist. This is how you recover a default after `logout` removed it, and
how you switch between environments without re-entering a key.

It also moves which profile a bare `blobhub-worker start` resolves — and with it which configuration, sessions
and cursors that worker picks up. The worker announces the profile it resolved on its first log line so the
change is visible rather than inferred, but if a worker runs on this machine, prefer naming its profile
explicitly in its service definition.

### Errors

| Code                               | When                                                                     |
| ---------------------------------- | ------------------------------------------------------------------------ |
| `PROFILE_NOT_FOUND`                | No profile by that name. Run `blobhub profile ls` to see what is stored. |
| `INSECURE_CREDENTIALS_PERMISSIONS` | `credentials.yaml` is readable by group or other.                        |
| `CREDENTIALS_NOT_FOUND`            | `credentials.yaml` is present but unparseable or malformed.              |

## See also

* [Worker authentication](/worker/auth) — the other half of the shared credential store.
* [Concepts](/cli/concepts) — what is shared across the binary and what belongs to a blob domain.
* [Configuration](/cli/configuration) — the credentials file these commands write, and the precedence ladder above
  it.
* [Service Accounts](/general/service-accounts) — what a service account is and how to create one.
* [Provisioning](/general/provisioning) — worked recipes for CI, Docker, Kubernetes, systemd, and Ansible.
* [Create API Key](/rest-api/shared/create-api-key) — where an org- or blob-scoped key comes from.
* [Utilities](/cli/utilities) — `doctor` diagnoses a credential these commands cannot explain.
* [Error codes](/cli/error-codes) — `AUTH_INVALID`, `PROFILE_NOT_FOUND` and every other code above, with its
  remediation.
