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

# Configuration

> Where credentials live, how a command resolves them, and what it prints on which stream

Every command in the binary settles the same three questions before it does anything: which key, which org, and
what to print. This page is the reference for all three — the credentials file, the precedence ladder above it,
the flags that are genuinely global, and the streams, envelopes and exit codes the output goes out on.

## `~/.blobhub`

Everything the CLI stores lives under one directory:

```text theme={null}
~/.blobhub/
├── credentials.yaml            # profiles: an API key and the URL it belongs to
└── cache/
    └── allowlist/<key>.yaml    # the compiler's import-allowlist cache
```

There is no project-local configuration file and nothing is read from the current directory. A checkout is
described by its manifest, which you pass explicitly with `-f`; the only implicit state is the credential.

### `credentials.yaml`

A `default:` naming one profile, and a `profiles:` map of name → key and URL:

```yaml theme={null}
default: default
profiles:
  default:
    key: <api key>
    url: https://api.blobhub.io/v1
  staging:
    key: <api key>
    url: https://api.staging.example.com/v1
```

[`login`](/cli/auth#blobhub-login), [`logout`](/cli/auth#blobhub-logout) and
[`profile use`](/cli/auth#blobhub-profile-use) are the only writers, and each rewrites the whole file through one
mechanism: a temporary file in the same directory, renamed into place, then `chmod`ed to `0600` *after* the
rename, so the mode is reliably `0600` whatever the process `umask` was. Either the old file or the new one is
there at every instant; a half-written credentials file is not a state you can reach. The parent directory is
created `0700`.

**A loose mode is refused, never repaired.** If any group or other permission bit is set, the load fails with
`INSECURE_CREDENTIALS_PERMISSIONS` and names the `chmod 600` that fixes it. The CLI will not widen or narrow a
mode you chose; silently repairing a permission is how a tool teaches you to stop looking at them.

| The file is…                                                                                      | What happens                                                                                     |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Absent                                                                                            | Not an error. Resolution simply has no profile to draw on.                                       |
| Group- or other-readable                                                                          | `INSECURE_CREDENTIALS_PERMISSIONS`.                                                              |
| Present but not valid YAML, missing `profiles:`, or holding an entry without both `key` and `url` | `CREDENTIALS_NOT_FOUND` — the code covers a file that is unusable, not only one that is missing. |

No key is ever printed: not in command output, not in an error message, not in a `--json` payload.

## How a command resolves a credential

Three values are resolved, in this order, and the order is load-bearing.

1. **The profile name** — `--profile`, else `BLOBHUB_PROFILE`, else the file's `default:`. A name that is not
   stored is `PROFILE_NOT_FOUND`.
2. **The key** — `--api-key`, else `BLOBHUB_API_KEY`, else the resolved profile's `key`. None of the three is
   `CREDENTIALS_NOT_FOUND`.
3. **The URL** — `--api-url`, else `BLOBHUB_API_URL`, else the resolved profile's `url`, else
   `https://api.blobhub.io/v1`.

Read as one ladder, highest precedence first:

| Rung                  | Key               | URL                         | Profile           |
| --------------------- | ----------------- | --------------------------- | ----------------- |
| Flags                 | `--api-key`       | `--api-url`                 | `--profile`       |
| Environment           | `BLOBHUB_API_KEY` | `BLOBHUB_API_URL`           | `BLOBHUB_PROFILE` |
| The named profile     | its `key`         | its `url`                   | —                 |
| The `default` profile | its `key`         | its `url`                   | —                 |
| Built in              | —                 | `https://api.blobhub.io/v1` | —                 |

### The flag rung exists on two commands

`--api-key`, `--api-url` and `--profile` are **not** global. They are options on
[`whoami`](/cli/auth#blobhub-whoami) and [`doctor`](/cli/utilities#blobhub-doctor), the two commands whose job is
to report on a credential — including one that is not stored yet. Every other command in the binary starts the
ladder at the environment rung.

[`login`](/cli/auth#blobhub-login) and [`logout`](/cli/auth#blobhub-logout) also take a `--profile`, but it means
something else there: which profile to write, or which to remove. Neither overrides the ladder.

### A key from the environment needs no file at all

Set `BLOBHUB_API_KEY` and every command works with no `~/.blobhub` on the machine — no `login`, no profile, and
no warning. That is the whole CI and container story, and it is why this CLI has environment precedence where
`blobhub-worker`, which logs in interactively once per host, does not.

```bash theme={null}
# a CI job needs these two lines and nothing else
export BLOBHUB_API_KEY="$BLOBHUB_KEY"          # from your secret store
blobhub workflow deploy -f manifest.yaml
```

Add `BLOBHUB_API_URL` only if you are not pointing at `https://api.blobhub.io/v1`. `whoami` reports
`Profile: (none)` for exactly this state — no profile was involved.

### Two traps in that order

**The profile name is resolved before the key.** A `BLOBHUB_PROFILE` naming a profile that is not stored fails
with `PROFILE_NOT_FOUND` even when `BLOBHUB_API_KEY` is set: the key is never reached. So a stale
`BLOBHUB_PROFILE` left in a CI environment breaks a setup that otherwise needs no profile whatsoever.
[`blobhub doctor`](/cli/utilities#blobhub-doctor) fails its `credentials` row on this combination for that reason,
rather than reporting a green row for a setup where every real command exits `1`.

**A loose file mode blocks an environment key too.** The permission check happens as the file is loaded, which is
step 1, before any key is looked for. A group-readable `credentials.yaml` therefore exits `1` even on a command
whose key came from `BLOBHUB_API_KEY` and which would never have read a profile. The file is on the machine
either way, so the CLI reports it either way.

## `--org` is a property of the checkout

A manifest may name its blob as a bare `<name>` rather than `<org>/<name>`. The org that bare name inherits comes
from `--org`, else `BLOBHUB_ORG`, and from nowhere else — a missing org is `BLOB_REFERENCE_INVALID` rather than a
guess. The same applies to a bare reference passed to [`blobhub blob show`](/cli/blob/show) and its siblings.

**No profile carries an org, deliberately.** A profile is a credential; which org a checkout targets is a
property of the checkout, not of the key used to reach it. One key routinely addresses several orgs, and one org
is routinely reached by several keys, so storing an org alongside a key would tie together two things that vary
independently. Set `BLOBHUB_ORG` per repository, or write `<org>/<name>` into the manifest and settle it there.

## Environment variables

These four are the whole set you would ever set yourself.

| Variable          | Effect                                                                                    |
| ----------------- | ----------------------------------------------------------------------------------------- |
| `BLOBHUB_API_KEY` | The API key. Outranks every stored profile; resolves on its own with no credentials file. |
| `BLOBHUB_API_URL` | The API base URL. Outranks a profile's `url`. Defaults to `https://api.blobhub.io/v1`.    |
| `BLOBHUB_PROFILE` | Which stored profile to resolve. Outranked by `--profile` where that flag exists.         |
| `BLOBHUB_ORG`     | The org a bare blob name belongs to. Outranked by `--org`. Not a credential.              |

Shell completion uses one more, `_BLOBHUB_COMPLETE`, which the script from
[`blobhub completion`](/cli/utilities#blobhub-completion) sets for itself. You never set it by hand.

## Global flags

These options live on the root command, and they are the only genuinely global ones:

| Flag              | Effect                                                            |
| ----------------- | ----------------------------------------------------------------- |
| `--json`          | Emit one machine-readable JSON object instead of rendered output. |
| `--verbose`, `-v` | Print extra diagnostic detail on stderr.                          |
| `--quiet`, `-q`   | Suppress advisory warnings. Errors are unaffected.                |
| `--org`           | The org a manifest's bare `blob:` name belongs to.                |
| `--version`       | Print the version and exit `0`, ignoring everything else.         |

**A global flag goes before the subcommand.** They are options on the root command, so anywhere after the
subcommand they are simply not that command's options:

```bash theme={null}
# correct
blobhub --org acme --json blob show dragon-ops

# usage error, exit 2: error: No such option: --json
blobhub blob show dragon-ops --json
```

## `--dry-run` is per-command

There is no global dry run. `--dry-run` is an option on **six** commands, and its absence elsewhere is not a rule
you can infer — two commands that write local files do not have it.

| Command            | `--dry-run` | What it does                                                                                                      |
| ------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------- |
| `workflow pull`    | yes         | Reports the intended calls and performs none.                                                                     |
| `workflow push`    | yes         | Reports the intended calls and performs none.                                                                     |
| `workflow build`   | yes         | Reports what it would compile and writes no definition file.                                                      |
| `workflow deploy`  | yes         | Reports the intended calls and performs none.                                                                     |
| `workflow prune`   | yes         | Reports what would be deleted and deletes nothing.                                                                |
| `scheduler deploy` | yes         | Writes no schedule — but still resolves every target, and may create a target session.                            |
| `workflow eject`   | **no**      | Writes local files, but refuses to overwrite an existing one — `EJECT_TARGET_EXISTS` — unless you pass `--force`. |
| `scheduler pull`   | **no**      | Overwrites the manifest file you point it at, with no guard at all. Run it on a clean checkout.                   |

The six differ in ways the table cannot show:

* **`scheduler deploy --dry-run` is not free of side effects.** Resolving a `session:` target that does not exist
  yet creates it, and a dry run resolves every target. That side effect is the one thing `scheduler pull` cannot
  undo, which is why `scheduler diff` exists: it reports an absent session as *would be created* and creates
  nothing.
* **`workflow prune --dry-run` beats `--yes`.** Passing both deletes nothing. A stronger safety guard silently
  losing to a weaker one is the single outcome that could genuinely surprise somebody.
* **Every dry run's `--json` payload carries `dry_run: true`**, alongside the list of what would have happened —
  so a pipeline can gate on the plan without parsing prose.

## Output streams

| Stream | Carries                                                                    |
| ------ | -------------------------------------------------------------------------- |
| stdout | The command's result — the rendered output, or the single `--json` object. |
| stderr | `error:` and `hint:` lines, `warning:` advisories, and `-v` detail lines.  |

Nothing but the result reaches stdout, which is what makes `--json` safe to pipe: a warning, a diagnostic or a
failure can never land in the middle of the object your parser is reading.

The one exception is [`blobhub login`](/cli/auth#blobhub-login), which prompts for the API URL on stdout because
it is an interactive command. Do not pipe it — in an automated context, supply `BLOBHUB_API_KEY` instead of
logging in at all.

### The `--json` envelope

Exactly one JSON object per invocation, either way.

On success, on **stdout** — the command's own payload plus the envelope's `schema_version`. Here, `whoami`'s:

```json theme={null}
{ "profile": "default",
  "url": "https://api.blobhub.io/v1",
  "user": { "id": "<user id>", "name": "Ada Lovelace" },
  "scope": "full",
  "schema_version": "1.0" }
```

The fields before `schema_version` differ per command and are documented on that command's page.

On failure, on **stderr**, with stdout left empty:

```json theme={null}
{ "error": "no API key from flags, environment, or credentials file",
  "hint": "blobhub login",
  "code": "CREDENTIALS_NOT_FOUND",
  "schema_version": "1.0" }
```

`schema_version` is `"1.0"` and is present on both. `hint` is a command you can paste, or an empty string; it is
never prose.

Commands that would otherwise print lines as they happen — `workflow execute --watch`'s live event stream,
`workflow prune`'s summary of what it is about to delete — print **nothing** under `--json`. A second line on
stdout would make the object unparseable, and one object is the contract.

Two commands stand outside the envelope. [`blobhub completion`](/cli/utilities#blobhub-completion) writes a shell
script, since wrapping it in JSON would defeat the only use of the command, and `--version` writes a bare version
string. Passing `--json` to either changes nothing.

## Exit codes

| Code | Meaning                                                                                                                                                     |
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0`  | Success.                                                                                                                                                    |
| `1`  | The command failed. An `error: <CODE>: <message>` line, or the JSON error envelope, says why.                                                               |
| `2`  | Usage error — an unknown command or option, a missing required argument, a value outside a closed set. Caught before the command runs, so nothing happened. |

A `2` is printed as a bare `error: <message>` with no code, because no command ran to raise one:

```text theme={null}
error: Missing parameter: manifest
```

[`blobhub doctor`](/cli/utilities#blobhub-doctor) is the one command that can exit `1` without printing an
`error:` line at all. Its failure is a report — a `✗` row — rather than an operation that failed.

## Advisories

An advisory is something worth knowing that is not a failure. It prints to stderr as:

```text theme={null}
warning: SCOPE_UNVERIFIED: could not determine the key's scope: <the error the probe hit>
```

The rules are short, and the last one is the one to remember:

* **An advisory never changes the exit code.** A run that emits five of them exits exactly as it would have with
  none.
* **`-q` suppresses them.** Errors are unaffected — `-q` quiets advice, not failure.
* **They are also suppressed whenever stderr is not a TTY.** Piping stderr, redirecting it to a file, or running
  under a CI log collector silences every advisory.

That last rule keeps automated logs clean, and it has a consequence worth stating plainly: **an advisory is not
observable from automation.** `-v` does not bring it back — `-v` controls a separate channel of detail lines,
which do print to a non-TTY stderr — and advisories do not appear in the `--json` payload either. To see one, run
the command on a terminal.

Where an advisory would change what you do, there is a flag that turns the underlying condition into a hard
failure instead. `workflow build --strict-imports` is the case that matters most: it converts the
`IMPORT_UNVERIFIED` advisory, raised when the import allowlist cannot be read, into a refusal.

## See also

* [Authentication](/cli/auth) — the commands that write and inspect the credentials file this page describes.
* [Utilities](/cli/utilities) — `doctor` reports the resolution above as seven rows, without running a command.
* [`blobhub blob limits`](/cli/blob/limits) — the allowlist cache under `~/.blobhub/cache`, and what refreshes it.
* [Error codes](/cli/error-codes) — the catalog of codes the envelopes and exit codes above carry.
* [Create API Key](/rest-api/shared/create-api-key) — where a key comes from, and the scopes it can carry.
