Appearance
Secrets
By default, secrets are resolved on your machine at deploy time, written to the Server as versioned files (0444 in 0700 directories owned by the deploy user), and mounted via compose secrets: at /run/secrets/<NAME>. File delivery keeps secret values out of the repo, the image, and docker inspect. Explicit secrets_as_env: true uses env_file instead: values become container environment variables visible through docker inspect.
Where values come from
.yoho/secrets: dotenv with$(...)substitution, Kamal style..yoho/secrets.<destination>is layered on top.shSECRET_KEY_BASE=$(op read op://Production/shop/secret_key_base) POSTGRES_PASSWORD=$(bw get password shop-db)secrets.providers+secrets.valuesinyoho.yml:yamlsecrets: providers: vault: { type: op } values: BACKUP_PASSWORD: { provider: vault, ref: "op://Production/shop-backups/password" }Provider types:
op(1Password),bw(Bitwarden),bws(Bitwarden Secrets Manager),command(argv with the reference appended).x-yoho.generate: created once on the Server, never in your password manager unless you back them up.
1Password approvals
Yoho reads all op references of an account in one op inject, so each Yoho command asks for 1Password approval at most once. How long that approval lasts is up to 1Password:
Desktop app integration. In a terminal, approval lasts for that terminal session: 10 minutes of inactivity, at most 12 hours, and locking the app revokes it. Coding agents (Claude Code, Codex) and scripts run every command in a new process without a terminal. On macOS, 1Password CLI 2.40 and later shares one approval across those processes, so a parent session and its sub-agents are asked once instead of once per command. Check with
op --versionand update the CLI if it is older. Older versions, and other platforms, may ask again for every Yoho command.Service account, no prompts. For unattended runs (CI, a long agent session), give a 1Password service account read access to only the vaults the App needs and export its token before running Yoho:
shexport OP_SERVICE_ACCOUNT_TOKEN="$(security find-generic-password -w -s yoho-op-sa)" # keep the token in the keychain, not in .yoho/secrets yoho deployop readandop injectwork with service accounts. Service accounts cannot read Personal or Private vaults, and their hourly and daily request limits are shared across the account, so prefer item and vault IDs in refs (op readmakes one request instead of three). Server-side jobs never call 1Password (ADR 0006), so a crash loop on the Server cannot use up those limits.
Yoho does not cache resolved values between commands and does not keep a session token on disk. Do not start an agent under op run to avoid prompts: that puts every secret into the agent's environment.
Requiring nonempty values (opt-in)
Empty secret values remain valid by default. To require particular source keys (the keys in .yoho/secrets or secrets.values, not container aliases), declare:
yaml
secrets:
nonempty: [DATABASE_PASSWORD, BACKUP_PASSWORD]Every listed key must exist and resolve locally to something other than exactly "" for the selected Destination. Whitespace-only and multiline values are valid; this check does not trim or change values. Existing provider/$(...) newline handling is unchanged. Keys must match [A-Za-z_][A-Za-z0-9_]*; duplicates are errors. A missing key, including a typo, an alias without its own source value, or a provider value excluded from this Destination, is an error, not ignored. Destination overlays can replace the list when contracts differ.
deploy, apply, and plan check resolved values before connecting to any Server or running the pre-connect Hook. Any other command that resolves local secrets checks the entire list too, including Backup Target credentials, managed tunnel up, secrets list/print/rotate, and credential-bearing schedule install. config check also resolves local secrets only when this list is nonempty; without it, config inspection still runs no secret commands. This opt-in uses the full existing resolution pipeline, not just listed keys: all applicable providers and secret-file $(...) commands run, so an unrelated resolution failure can fail config check even when every declared key has a literal value. Secret files are trusted shell code; commands can depend on any previously defined key or provider value (including indirect environment lookups), so Yoho does not infer a partial shell dependency graph. Do not opt in expecting a credential-free structural check; config show still does not resolve credentials. yoho config check --offline (or YOHO_OFFLINE=1) is the only mode that never runs a provider or $(...) line, even under secrets.nonempty; use it in CI.
Provider references overridden by secret files are still not fetched. With an active contract, all failed provider/secret-file commands report keys only: this includes undeclared keys sharing a provider call and credentials not yet known to the Store. Provider references, command diagnostics, raw/encoded output, and underlying command-error text are suppressed. Cancellation/deadline status is retained without command output. With an empty/absent list, existing diagnostic behavior is unchanged.
Server-generated source keys (x-yoho.generate) cannot appear in this list, even if a local value with that key also exists. Yoho rejects that combination when inspecting Compose, before provider calls or SSH: these keys are created or retained on the Server and cannot satisfy a local resolution contract. Commands resolving an opt-in contract therefore also need valid local Compose files. schedule install also checks this local Compose/generation contract before SSH for credentialless jobs, without resolving their unrelated local providers or secret-file commands. Generated-secret delivery and auditing are unchanged.
Commands that do not resolve local secrets do not gain a credential requirement (e.g. config show, App status/logs, Scheduled Job status/run, and rollback). Stored Scheduled Job specs and retained Releases continue using their stored credentials; current local declarations are not retroactively enforced on them. Reinstall/apply checks newly resolved credentials, but this declaration does not rotate secrets, alter empty values, or repair credentials automatically.
Rotating values
Change the value in 1Password, Bitwarden, or .yoho/secrets, then:
sh
yoho secrets rotate # every Service whose values changed
yoho secrets rotate web # fail unless only web changed
yoho secrets rotate --dry-run # list what would restart; change nothingYoho re-resolves secrets on your machine, writes a new generation, and restarts only the Services whose values changed: proxied Services through the Proxy (zero downtime), Stateful Services stop-first. Images are not rebuilt. Run it from a checkout of the deployed Version (same commit, same uncommitted changes): rotate recompiles the local compose files and refuses when they are another Version. Any other difference from the deployed project (a git-ignored .env, say) is refused too. pre-deploy and post-deploy Hooks run with YOHO_COMMAND=secrets rotate. The current Release is recorded again with the new secrets generation, so its old values are not kept: to undo, restore the old value in the provider and rotate again. Rolling back to an earlier Version still runs that Version's secrets. Output names Services, never values. With scoped access, the operator also runs yoho access secrets push so Members pick up the new generation.
Giving a Service its secrets
yaml
x-yoho:
secrets:
- DATABASE_PASSWORD:POSTGRES_PASSWORD # container name <- secret key
- SECRET_KEY_BASE
secrets_as_env: false # true: env_file instead of filesEach Service receives only what it declares. Apps read files via the *_FILE convention (POSTGRES_PASSWORD_FILE=/run/secrets/POSTGRES_PASSWORD).
Rules
- Do not use
${VAR}interpolation for secrets in compose: it leaks throughdocker compose config.yoho config checkrejects it. - Provider failures are classified without showing provider output:
secrets_approval_timeout(the approval prompt ofop,bworbwswent unanswered or was denied, or acommandprovider exited 75),secrets_not_signed_in,secrets_item_not_found. The--jsonstep_failevent carries thecodeand a distincthint; other failures keep the redacted command error. - A provider call that runs longer than 3 s prints "waiting for secrets provider approval…" (
step_waitin--json). On a terminal Yoho reruns an approval timeout once; set--secrets-retries NorYOHO_SECRETS_RETRIESto change it (default 0 without a terminal). The first provider value is read alone, so one approval covers the rest. - 1Password: all
opreferences of one account, fromprovidersand from$(op read op://…)lines in.yoho/secrets, are read with a singleop inject, so a command asks for approval at most once even whereopauthorizes each process separately (coding agents, scripts). A$(...)is batched only when it is exactlyop read [-n|--no-newline] [--account X] REFwith no$, pipes or other shell syntax, and not when the secrets files define anOP_*key. If the batch fails for a reason other than sign-in or approval, each reference is read on its own so the error names the key. yoho secrets listshows keys, sources, and lengths.yoho secrets print KEY --revealworks only on a terminal.yoho secrets rotate [service...]re-resolves values from the provider with no rebuild and no code change, shows which Services changed (names only), and does a zero-downtime rolling restart of only those Services. Stateful Services follow their normal stop-first rules. It records the current Release again, so its pre-rotation values are not kept.--dry-runlists what would restart.- A timed walkthrough of a secret change is in the 5-minute quickstart.
- Build-time secrets: list keys in
builder.secretsand useRUN --mount=type=secret. - Server-side jobs (Scheduled Backups) read stored files, not your password manager.