~/.blobhub
Everything the CLI stores lives under one directory:
-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.- The profile name —
--profile, elseBLOBHUB_PROFILE, else the file’sdefault:. A name that is not stored isPROFILE_NOT_FOUND. - The key —
--api-key, elseBLOBHUB_API_KEY, else the resolved profile’skey. None of the three isCREDENTIALS_NOT_FOUND. - The URL —
--api-url, elseBLOBHUB_API_URL, else the resolved profile’surl, elsehttps://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 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
SetBLOBHUB_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.
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. ABLOBHUB_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-runis not free of side effects. Resolving asession:target that does not exist yet creates it, and a dry run resolves every target. That side effect is the one thingscheduler pullcannot undo, which is whyscheduler diffexists: it reports an absent session as would be created and creates nothing.workflow prune --dry-runbeats--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
--jsonpayload carriesdry_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:
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:- An advisory never changes the exit code. A run that emits five of them exits exactly as it would have with none.
-qsuppresses them. Errors are unaffected —-qquiets 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.
-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.
- Utilities —
doctorreports 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.

