Skip to main content
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:
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:
login, logout and 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 chmoded 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. 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:

The flag rung exists on two commands

--api-key, --api-url and --profile are not global. They are options on whoami and 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 and 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.
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 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 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. Shell completion uses one more, _BLOBHUB_COMPLETE, which the script from 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: 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:

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

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, 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:
The fields before schema_version differ per command and are documented on that command’s page. On failure, on stderr, with stdout left empty:
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 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

A 2 is printed as a bare error: <message> with no code, because no command ran to raise one:
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:
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 — the commands that write and inspect the credentials file this page describes.
  • Utilitiesdoctor reports the resolution above as seven rows, without running a command.
  • blobhub blob limits — the allowlist cache under ~/.blobhub/cache, and what refreshes it.
  • Error codes — the catalog of codes the envelopes and exit codes above carry.
  • Create API Key — where a key comes from, and the scopes it can carry.