Secrets from the CLI

This guide teaches you to run your applications with secrets injected at launch instead of scattered across .env files. By the end you will have authorized your machine against the zero-knowledge vault, verified the authorization the right way, wired secrets run into your daily workflow, and learned the grant lifecycle well enough to manage a whole team's machines.

Why Replace .env Files

A .env file is a plaintext copy of your most sensitive values sitting in your working tree, one careless commit away from a repository and one backup away from places you never intended. Multiply that by every developer machine and every project checkout, and "where are our secrets" stops having an answer.

The Backbuild Secrets CLI inverts the model: secrets live encrypted in the vault, your machine is explicitly authorized to read them, and values are injected into your process's environment at the moment you launch it. Nothing is written to disk, there is one authoritative copy, and removing a machine's access is one action instead of a hunt.

How the Vault Protects You

Three properties define the security model you are working with:

  • Zero-knowledge encryption. Vault contents are encrypted on your device with keys derived from your vault master password, using hybrid post-quantum cryptography. Backbuild stores only ciphertext and cannot read your secrets. Decryption happens locally, on the machine you authorized.
  • Machine identity. Each machine gets its own identity keypair with a human-readable fingerprint. Access is granted to a specific machine, scoped to specific vaults, with an expiry. You always know which machines can read what.
  • OS keystore required. The CLI stores machine keys in your operating system's secure keystore and refuses to operate without one. Keys are never dropped into a dotfile.

Step 1: Create Your Machine Identity

On the machine that needs vault access, generate its identity keypair:

backbuild secrets keygen

This creates two hybrid post-quantum keypairs for the machine: an encryption keypair (X25519 paired with ML-KEM-768) that lets the vault deliver secrets only this machine can open, and a signing keypair (Ed25519 paired with ML-DSA-65) that proves requests came from it. Both secret keys are sealed in your operating system's secure keystore, never written to a dotfile, and the command refuses to run if no keystore is available.

The command prints the machine's fingerprint, a short human-comparable string derived from the encryption public key that uniquely identifies this machine. You will compare this fingerprint during authorization, so keep the terminal open or note it. Running keygen again with --force rotates the identity, which invalidates existing authorizations for the old one. In practice you rarely run keygen yourself: login bootstraps an identity automatically the first time you use it. Check the current identity's fingerprint and key state at any time with:

backbuild secrets status

status is local only: it reads nothing from the network and prints the active profile, its fingerprint, and whether the sealed key is present. It exits non-zero if the profile has no identity yet, which is your signal to run login.

Step 2: Authorize the Machine

Authorization is a deliberate ceremony, not a silent token exchange. After this section you will know exactly what each step proves.

backbuild secrets login
Sequence diagram of the authorization ceremony across three lanes: your machine running the CLI, any browser, and Backbuild. The CLI prints a URL, user code, and machine fingerprint. You open the URL in a browser, sign in, and unlock with your vault master password. The consent page shows the requesting machine's fingerprint, which you compare to the CLI's. You approve, the grant is delivered to the machine, and the CLI prints the granted vaults, expiry, and a grantor consent fingerprint to compare back.
The ceremony verifies both directions: you confirm the machine before approving, and the machine confirms the grantor after.
  1. The CLI prints a verification URL and a short user code. Open the URL in any browser on any device. This is why the flow works from SSH sessions and headless machines: the browser does not need to be on the same computer.
  2. You authorize in the browser. Sign in to your account and review the request. The consent page shows the requesting machine's fingerprint, asks for your vault master password to unlock, and then requires a fresh second-factor confirmation before anything is granted, even inside a signed-in session.
  3. Compare the fingerprints. The fingerprint on the consent page must match the one your CLI printed. A match proves you are authorizing your machine's key, not an attacker's machine that raced you to the code.
  4. Choose what to grant, approve, and the CLI confirms. You pick the vault to grant; only an owner of that vault can approve a machine for it. The CLI then prints the vaults you were granted, the grant's expiry, and a grantor consent fingerprint. Comparing that value out-of-band closes the loop in the other direction: the grant your machine received is the one you approved.
The Authorize a machine consent page. The requesting machine's fingerprint is highlighted with a callout stating it must match the fingerprint your CLI printed. Below it are the master password field, a Deny button, and an Unlock and verify button.
The consent page. Approve only when the highlighted fingerprint matches the one in your terminal.

Only approve a code that you initiated moments ago on a machine you control. If a code or authorization request ever arrives unprompted, by chat, email, or a colleague "just needing a quick approve", decline it. Unsolicited approval requests are how device-authorization phishing works, and declining is always the correct answer.

Profiles: One Machine, Several Environments

A profile is an isolated slot that holds one machine identity and one authorization. Every secrets subcommand accepts --profile NAME, and the profile in effect is resolved in this order: the --profile flag, then the BACKBUILD_PROFILE environment variable, then a profile literally named default. If you never mention profiles, everything happens under default and a single-account machine never has to think about them.

Profiles matter the moment one machine needs to reach more than one place. A profile is bound to a single control plane and vault, and that binding is fixed at login time from the environment you selected (an explicit BACKBUILD_API_BASE, otherwise BACKBUILD_ENV), then frozen into the grant. So a profile pins both where your secrets live and which vault you consented to. Give each destination its own profile, and one workstation or CI runner can hold independent grants side by side: for example a production profile and a staging profile, or one profile per project vault.

# Authorize this machine against your production deployment
BACKBUILD_ENV=production backbuild secrets login --profile production

# Authorize the same machine against a separate staging deployment
BACKBUILD_ENV=staging backbuild secrets login --profile staging

# Now run each app against the right vault, no ambiguity
backbuild secrets run --profile production -- ./deploy.sh
backbuild secrets run --profile staging -- npm run dev

# Or make one profile the default for a whole shell session
export BACKBUILD_PROFILE=staging
backbuild secrets get

Because a profile owns its own identity and grant, revoking or rotating one never touches the others, and backbuild secrets status --profile production reports that profile's fingerprint independently. Profile names may contain letters, digits, hyphens, and underscores, must start with a letter or digit, and are limited to 64 characters; dots and slashes are rejected so a profile name is always safe to use in a path.

Everyday Use

Run your app with secrets injected

This is the command that replaces the .env file. It launches any command with your vault secrets projected into that process's environment:

# Instead of copying and editing a .env file
backbuild secrets run -- npm run dev

# Any command works
backbuild secrets run -- python manage.py runserver

# Scope to one vault, then run
backbuild secrets run --vault VAULT_ID -- ./deploy.sh

Put a bare -- between the CLI's own options and the command you want to run. Everything after -- is treated as the child command and its arguments verbatim, so flags meant for your program (a --vault or --profile your app happens to define, for instance) are never mistaken for the CLI's. The separator is optional for a simple command, but making it a habit keeps the boundary unambiguous.

Secrets exist only in the launched process's environment, only for its lifetime. Nothing is written to disk, nothing lingers in your shell environment after the process exits, and there is no file to accidentally commit. Before the command starts, the CLI prints how many secrets it injected, for example Injected 3 secret(s) into the environment.

Refuse to start without the secrets you need

Add --require NAME, once per name, to make the run refuse to start when a variable your program depends on would be missing:

# Start only if both variables will be set
backbuild secrets run --require DATABASE_URL --require STRIPE_API_KEY -- npm start

Each name is compared after the same normalization the projection uses (upper case, non-alphanumerics become underscores; see How fields become environment variables below). If any required name is absent, the CLI prints required secret(s) missing: with the names, exits 1, and never starts the command, so a deploy fails at once instead of running half-configured. A required name that is empty or is one of the refused loader names is a usage error (exit 2).

On Windows, secrets run also refuses a command that resolves to the WSL launcher (for example a bare bash that Windows finds in its system folder), because an injected Windows environment does not cross into WSL and the secrets would be silently dropped. Run Git Bash by its full path, or call the interpreter (node, python) directly. The check runs before the vault is fetched.

How fields become environment variables

Each field in the vault becomes one environment variable. The name is built from the item's title and the field's label, joined as TITLE_LABEL: uppercased, with every character that is not a letter or digit turned into an underscore, and a leading underscore added if the result would start with a digit. A field labelled api key on an item titled Stripe arrives as STRIPE_API_KEY. When the title and the label are the same, the name is used once: a field labelled STRIPE_KEY on an item titled STRIPE_KEY arrives as STRIPE_KEY. When two fields would produce the same name, the later one wins and the CLI says so.

Two safety rules run at this projection step, so nothing in the vault can subvert the process you launch. Names that control a program's loader or shell (such as PATH, IFS, LD_*, DYLD_*, BASH_FUNC_*, NODE_OPTIONS, PYTHONPATH, and GIT_SSH) are refused rather than injected, and a value containing a newline or other control byte is refused too, because it could smuggle extra assignments. Refusals are reported as warnings; the command still runs with the safe variables in place.

Read a secret

# List your items; every value is masked
backbuild secrets get

# Show the values in the listing (needs the vault owner's approval)
backbuild secrets get --reveal

# Print one field's value (needs the vault owner's approval)
backbuild secrets get --reveal --name DATABASE_URL

# Dump all items and values as JSON (needs the vault owner's approval)
backbuild secrets get --reveal --json

The server only ever returns ciphertext; the values are decrypted locally on your authorized machine. By default the listing masks every value, so a screen share, a pasted terminal log, or a recorded demo does not become an incident. Printing a value is a reveal, and every reveal asks the vault owner who authorized the machine: a prompt appears in their Backbuild app showing what is being asked for and by which machine, and they approve it with a fresh authenticator code or security key, or deny it. The CLI waits up to five minutes and prints the value once, after approval; a denied request exits 3, and a request nobody decides is rejected after five minutes and exits 4. Any mode that would print a plaintext value also requires --reveal: the listing with --reveal, a single field with --name (or the identical --field), and the machine-readable --json all refuse to run without it, before anything is requested.

To use a value rather than see it, use secrets run: it needs no approval.

--name matches a field's label, compared case-insensitively, not the item's title. It prints exactly that one value and nothing else, which is what a script or a pipe wants; if no field carries that label the command exits non-zero. Treat revealed output like the secret it is: it goes to your terminal, or wherever you redirect it, on purpose.

Store a secret

# The value is read from stdin, never from the command line
backbuild secrets set --name STRIPE_KEY
# (paste the value, press Enter)

# Or pipe it
cat key.txt | backbuild secrets set --name STRIPE_KEY

set deliberately refuses a value passed as an argument. Command-line arguments land in shell history and are visible to other processes on the machine; stdin is neither. If you try to pass the value positionally, you get a usage error, not a stored secret. It creates the item if the name is new and updates it in place if it already exists (an upsert by title), so re-running set is how you rotate a value. Writing requires a read-write grant, and on a multi-vault grant you name the target with --vault ID.

Know the name a set secret projects to. set --name X stores an item titled X holding a single field labelled X. Because the title and the label are the same (see How fields become environment variables above), that field arrives under secrets run as exactly X. Releases of the CLI from before September 2026 projected it as X_X; if your app finds nothing under X, update the CLI. When you need a multi-field credential, create the item in the app (or copy it with secrets copy) where you control the title and labels independently.

Export when a file is unavoidable

backbuild secrets export --format dotenv
backbuild secrets export --format shell
backbuild secrets export --format json

Some tools insist on a file. export is a reveal, so the vault owner approves it first, as above. It then writes plaintext to standard output in dotenv, shell, or json form, and from that moment the output is your responsibility: redirect it somewhere safe, never into a tracked file, and remember that shell redirection lines can end up in history and build logs. Prefer secrets run whenever the tool allows an environment variable, and treat export as the exception.

Sign a mobile release without exposing the keystore

Android signing keys are exactly the kind of long-lived, high-value secret that should never sit on a build machine. secrets sign keeps the keystore in the vault and lends it to the signer only for the length of one run:

# Sign an app bundle with the keystore held in the vault
backbuild secrets sign --app MYAPP ./app-release.aab

The keystore is materialized to a private, owner-only file for the duration of the signing command and then overwritten and removed, including if you interrupt it. The keystore password is handed to the signing tool by environment-variable name, never on the command line where other processes could read it. By default the command reads the fields KEYSTORE_B64, KEYSTORE_PASSWORD, and KEY_ALIAS, each prefixed by --app (so --app MYAPP reads MYAPP_KEYSTORE_B64 and its siblings); these are vault field labels as listed by secrets get. After signing, the CLI verifies the signature against the produced artifact and reports the certificate's SHA-256 fingerprint; if verification does not pass, it refuses and leaves your artifact untouched. The full flag set is in the CLI Reference.

Copy a Vault to Another Environment

secrets copy reproduces one vault's contents into another, faithfully and without ever printing a value. It reads and decrypts the source vault locally, then re-encrypts each item verbatim under the target vault's key and upserts it by title, preserving every item's title, each field's label, value, and type, and the item's category. Because the titles and labels are preserved exactly, the copied vault produces the identical secrets run environment. This is what makes it the right tool for seeding a staging deployment from your source of truth, or migrating a vault, where set (single-field, one item at a time) cannot reproduce a multi-field structured credential.

Copy works across the profiles you set up earlier: the source profile supplies a read grant, the target profile a read-write grant, and each can point at a different deployment. Always dry-run first to see exactly what would move:

# Preview: list titles, field counts, and categories; write nothing
backbuild secrets copy --from-profile production --to-profile staging --dry-run

# Do it for real
backbuild secrets copy --from-profile production --to-profile staging

A dry run prints one line per item, showing its title, how many fields it has, and its category, and never a value, then confirms that nothing was written. If either profile's grant covers more than one vault, name the ends explicitly with --from-vault ID and --to-vault ID. Copying a vault onto itself (the same deployment and the same vault) is refused, since it could only overwrite the source with itself. Secret values live only in memory for the length of the copy and are never displayed.

Working with Multiple Vaults

If your grant covers more than one vault, commands that read or write require an explicit --vault ID. The CLI never silently picks a vault for you, because "the wrong production database URL, quietly" is exactly the class of surprise a secrets tool must not produce. Your granted vault IDs are printed when the authorization completes, so note them then; omitting --vault on a multi-vault grant gets a clear error that lists the IDs your grant covers.

This is a different axis from profiles. A profile selects which control plane and authorization you are using; --vault selects which vault within that one grant when the grant happens to cover several. Use a separate profile per deployment, and --vault only when a single grant spans more than one vault.

The Grant Lifecycle

Understanding grants end to end is what makes this manageable across a team:

  • Scope: a grant covers specific vaults for one machine identity. Different machines get different grants.
  • Expiry: grants expire. When one does, commands fail with a clear message telling you to run backbuild secrets login again; nothing goes stale silently.
  • Ending access from the machine: backbuild secrets logout removes the machine's authorization (add --yes to skip the confirmation).
  • Ending access from the vault side: an owner of the vault revokes the machine's grant over the REST API, by the fingerprint secrets status prints, after listing the vault's machine grants to find it (see How a machine grant ends). This is your offboarding lever: a departed laptop loses access without touching the machine itself. The Machines with access list on the vault's Sharing & permissions tab does not show machine grants yet.
  • Endings you do not have to trigger: a grant also ends when the person who approved it is removed from the vault or is no longer an owner of it, and stops working while their account in the organization is suspended or deactivated. Removing anyone from a vault normally rotates its key, which ends every machine grant on that vault; run backbuild secrets login again on each machine that should keep access.
  • Identity health: if the machine identity is found broken or half-initialized, the CLI repairs it and warns you that the fingerprint changed. A changed fingerprint means previous grants no longer apply to this identity; authorize again and compare the new fingerprint.

CI and Shared Machines

A CI runner or build box is authorized the same way as a workstation, once, during setup:

  1. On the runner, run backbuild secrets keygen, then backbuild secrets login. Because the approval happens in a browser on any device, you can complete it from your own machine using the printed URL and code.
  2. Once authorized, jobs use backbuild secrets run non-interactively until the grant expires. A reveal (get --reveal, export) waits for the vault owner's approval, so do not build a job on one.
  3. Grants expire by design, so schedule re-authorization into your runner maintenance rather than discovering expiry in a failed deploy. The expiry is printed when the grant is issued; record it in your runner maintenance schedule.

Two constraints to plan for: the runner needs an operating system secure keystore available (the CLI refuses to store machine keys without one), and each runner should have its own identity so revoking one machine never disturbs the others. Secret values appear in output only when a job reveals them with the vault owner's approval, and secrets run masks the injected values in its command's output, which keeps CI logs clean by default.

Troubleshooting

  • "Grant expired" (or commands suddenly demand authorization): run backbuild secrets login again and re-compare fingerprints. This is routine, not a fault.
  • The CLI refuses to run, citing no secure keystore: the machine has no OS keystore available. This is a protection, not a bug; enable your platform's keystore rather than looking for a way around it.
  • Warning that the machine identity was repaired and the fingerprint changed: the old identity was unusable. Re-run secrets login and verify the new fingerprint on the consent page.
  • "Multiple vaults" error: pass --vault ID explicitly. The CLI will not choose for you.
  • "required secret(s) missing": a --require name was not produced by the vault's projection. Check the item title and field label with secrets get, or that you are on the right profile and vault.
  • A machine that worked yesterday is refused today: its grant expired, the person who approved it is no longer an owner of the vault, or the vault key was rotated when someone was removed. Run backbuild secrets login again.
  • On Windows, secrets run refuses bash as the WSL launcher: run Git Bash by its full path, or call the interpreter directly.
  • A command printed *** where a value was: secrets run masks the injected values in its command's output on purpose. Use the value inside the command; to see it, reveal it with get --reveal --name, which the vault owner approves.
  • A reveal ended "denied" or "timed out": the vault owner denied it, or nobody decided within five minutes. Ask the owner, then run it again; nothing was printed.
  • "update the backbuild CLI": the CLI is older than the service accepts. Install the current release.
  • Your app cannot find a variable you stored with set: a set --name X secret projects as X. A CLI release from before September 2026 projected it as X_X; update the CLI.

Frequently Asked Questions

Can Backbuild read my secrets?
No. Vault contents are encrypted on your device before upload and decrypted only on machines you have authorized. The platform stores ciphertext. This is also why nobody can email you a reset for your vault master password.

What does the fingerprint comparison actually prove?
That the key being authorized is the one on your machine. The consent page shows the requester's fingerprint; your CLI printed yours. If they match, you authorized your machine. The grantor consent fingerprint the CLI prints afterwards proves the reverse direction: the grant your machine holds came from the approval you performed.

My laptop was lost or stolen. What is the blast radius?
Have an owner of the vault revoke that machine's grant over the REST API, by its fingerprint, and the grant is dead; a key rotation, which removing anyone from the vault normally triggers, ends every machine grant on it. The vault itself remains encrypted, and the thief does not have your vault master password. Rotate any secrets the machine had read, then get back to work.

Is secrets run really safer than a .env file?
Yes, categorically. A .env file is at-rest plaintext that every backup, sync tool, and git add . can pick up. secrets run puts values in one process's environment for one lifetime, with a single revocable grant behind it and one authoritative copy in the vault. It also replaces each injected value of four or more characters, and its base64, URL-encoded and hex forms, with *** in the command's output, including a value split across writes. Masking is best effort: a value the command transforms another way, writes to a file or sends over the network is not masked, and programs see pipes rather than a terminal.

Does the approval stop someone who controls the machine?
No. A machine you authorized holds a grant that can decrypt the vault, so someone who controls that machine, or runs a modified client on it, can read what the grant covers without asking. The approval governs the official CLI and the service's own interfaces; it is not a substitute for revoking a machine you no longer trust.

Does export defeat the purpose?
Used casually, it can. It exists for tools that cannot read environment variables. Keep its output out of tracked files and logs, and reach for it only when run cannot do the job.

Can one machine hold access to more than one environment at once?
Yes, with profiles. Authorize each destination under its own profile (backbuild secrets login --profile production, then again with --profile staging) and select one per command with --profile or the BACKBUILD_PROFILE environment variable. Each profile carries its own identity and grant, bound at login to the control plane you chose, so they never interfere and revoking one leaves the others intact.

How do I seed a staging vault from production without retyping every secret?
Use backbuild secrets copy --from-profile production --to-profile staging. It reproduces every item faithfully, including multi-field credentials and categories that set cannot express, so the copied vault yields the same environment. Dry-run it first with --dry-run to see exactly what would move, and note it never prints a value.

Where is the vault's master password in all this?
You enter it in the browser during the authorization ceremony, never into the CLI. See the master password explainer for what it is and why it is unrecoverable.

See Also