brig docs

Guides

Secrets

On this page

brig secret manages Brig's secret store: it puts credentials into the store, reads them back, and imports a login from your host.

The secret store #

How a secret reaches the sandbox A run reads only Brig’s secret store. The secrets a profile names reach the sandbox: one as an environment variable, one as a file held in memory. Your login keychain, your SSH agent and other secret managers do not reach the sandbox. Your machine Sandbox Your login keychain SSH agent Other secret managers Brig secret store the only store a run reads gh-token claude-credentials GH_TOKEN an environment variable ~/.claude/.credentials.json a file, held in memory The agent gets the secrets its profile names, and no others.
A run reads Brig's secret store and no other.

A run reads no other store. A profile declares the names it wants under secrets:. The values in the store under those names reach the sandbox. A value arrives as a file where the agent reads one, and as an environment variable otherwise.

For claude-code, log in inside the sandbox or import your host login:

brig run claude-code               # log in inside the sandbox, or:
brig secret import claude-code     # carry your host login in, once

The first command does not persist a login. The in-sandbox login is written to ~/.claude/.credentials.json on a memory-backed mount, and brig stop discards it. An imported login survives a stop. See Authentication.

For what the keychain does and does not protect, see Security model.

External secret managers #

With 1Password, Vault or pass, pipe the value in once:

op read ... | brig secret create <name>
brig secret import <profile> <name> --from-command 'op read ...'

Or keep the value in the environment. An env.<name> binding reads Brig's environment on every run. So <your secret manager's run-with-env command> -- brig run claude-code works for the variables a profile binds that way.

Commands #

Command What it does
brig secret create <name> Stores a new secret. Refuses if the name is taken.
brig secret read <name> Prints the value.
brig secret update <name> Replaces an existing value. Refuses if the name is not there.
brig secret delete <name> Removes the secret, after asking. -y answers in advance.
brig secret ls Lists names, dates and where each value came from. Never values.
brig secret import <profile> Fills that profile's secrets from your host, once.

create and update never take the value as an argument. See Value input.

rm and list are deprecated spellings of delete and ls. Both print a deprecation notice and are removed in v0.4.0. Migration lists the retired spellings Brig still accepts.

Create and update #

If create or update refuses a name, the message names the command to use:

$ printf %s "$TOKEN" | brig secret create gh-token
$ printf %s "$TOKEN" | brig secret create gh-token
brig: a secret named "gh-token" already exists. To replace it: brig secret update gh-token
$ printf %s "$TOKEN" | brig secret update gh-tokne
brig: no secret named "gh-tokne". To create it: brig secret create gh-tokne

A successful write prints nothing.

Brig reads back what it wrote and compares the bytes. A create that does not match is removed. An update that does not match only reports the mismatch, because the previous value is already gone.

List secrets #

ls prints three columns:

$ brig secret ls
NAME                UPDATED           FROM
claude-credentials  2026-08-18 12:31  keychain:Claude Code-credentials
deploy-key          2026-08-15 21:09  -
gh-token            2026-08-15 21:09  -

--json prints the same data, one object per secret. It leaves out any field a secret does not have:

$ brig secret ls --json
{
  "apiVersion": "brig.sh/v1alpha1",
  "kind": "SecretList",
  "data": []
}

ls reads keychain attributes only, never a value. It raises no access prompt.

Column Meaning When it is missing
UPDATED The item's own modification date. Text shows -, and JSON omits modified. The keychain always supplies a date. A future backend can omit it.
FROM Provenance: where brig secret import read the value. Text shows -, and JSON omits provenance. Brig did not put the value there. You created it by hand.

import records provenance in the keychain item's comment attribute, so a listing decrypts nothing. A --from-command value reads as command (a command you gave). Brig does not show the command line, because the line can hold a quote, a pipe or a credential.

An empty store is not an error:

$ brig secret ls
no secrets yet. To add one: brig secret create <name>

Expiry warnings #

If the profile declares an expiryField:, provenance also carries the expiry of the credential. A run then warns about an expired copy before boot, and decrypts nothing:

$ brig run claude-code ~/code/demo
brig: the imported credential claude-credentials (claude-code) expired 3h ago
  → renew it on the host, then:  brig secret import claude-code

A secret with no sources:, filled with --from-command, gets a different second line. The profile-wide import cannot refill it:

renew it, then store it again:  brig secret import <profile> <name> --from-command '<command>'

Delete a secret #

delete asks first. Brig keeps no copy of a deleted value, and the keychain keeps no history.

$ brig secret delete gh-token
brig: delete "gh-token"? The value cannot be recovered [y/N] y
deleted gh-token

If there is no terminal to ask on, delete refuses:

$ echo | brig secret delete gh-token
brig: deleting "gh-token" cannot be undone, and there is no terminal to ask on. Pass -y to answer in advance: brig secret delete gh-token -y

A worked example #

This example stores a GitHub token, uses it, rotates it and removes it. The output wording is illustrative, because no test pins it.

Store the token. printf %s stores no newline, and the pipe keeps the value out of your shell history:

$ printf %s 'ghp_16C7e42F292c6912E7710c838347Ae178B4a' | brig secret create gh-token
$ brig secret ls
NAME      UPDATED           FROM
gh-token  2026-08-15 21:09  -

Use it. claude-code declares gh-token, so the name in the store is the binding:

$ brig info claude-code
...
brig: forwarding to guest:
brig:   GH_TOKEN(secret)
brig: never forwarded for claude-code: ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN (they would move this sandbox onto metered billing)

(secret) means that the value came from Brig's store and not from your shell. brig info reports the forwarding, and brig run claude-code does it.

Warning The denylist covers environment forwarding only. It does not check a files: binding. It does not stop an agent that reads a credential inside the guest from sending it over the network. See Security model for why the guard has that scope.

The profile binds the name as a chain (refs: [env.GH_TOKEN, secrets.gh-token]). An exported GH_TOKEN wins, and the stored value is the fallback. See Git access.

Rotate it:

$ printf %s 'ghp_9a1FfE0d5B7c4A2e8D3b6C1a0F5e9D8c7B6a' | brig secret update gh-token

Brig re-reads what it hands the guest on every exec. The next command gets the new value, and the sandbox does not need a restart.

Remove it:

$ brig secret delete gh-token
brig: delete "gh-token"? The value cannot be recovered [y/N] y
deleted gh-token
$ brig secret ls
no secrets yet. To add one: brig secret create <name>

Import from your host #

brig secret import <profile> copies that profile's credentials from your host into Brig's store. No test pins the output wording, so this output is illustrative:

$ brig secret import claude-code
claude-code: importing 1 secret
  claude-credentials: stored from keychain:Claude Code-credentials, expires 2026-08-19 01:31
  gh-token: no source on your host, so it is one you supply: brig secret create gh-token
note: claude-desktop also declares claude-credentials, so this fills it there too

Each secret in a profile carries a sources: list, and the first source that exists wins. brig agent ls shows which names a profile can import and which it cannot. Profiles covers how to declare sources in a profile of your own.

Flag or argument What it does
--dry-run Previews the action. It reads the sources to check them, and writes nothing.
-y Replaces a value Brig did not write, without asking.
--from-command '<sh>' Takes one named secret's value from a command's stdout, not from its declared sources.
[name...] after the profile Narrows the import to the names you list.

The argument to import is a profile. If you give it a secret name, Brig names the profile to use:

$ brig secret import claude-credentials
brig: "claude-credentials" is a secret, not a profile, and import takes the profile that declares it: brig secret import claude-code claude-credentials

Import rules #

Import reads your host's credential stores once, when you run it. A later run reads only Brig's keychain item. That item carries the default ACL, so a run raises no approval dialog. Only import raises the dialog, once.

The copy does not track its source. A renewed login on the host does not update Brig's copy. A revoked login does not invalidate Brig's copy. To refresh the copy, import again. To remove it for good, use brig secret delete.

Import does not replace a value Brig did not write. A secret with no provenance is one you created by hand. Import stops, because it cannot recover the value that the import replaces:

$ brig secret import claude-code
brig: "claude-credentials" is already stored and brig did not put it there, so importing would replace a value you supplied. To replace it: brig secret import claude-code claude-credentials -y

Import skips an unchanged value. UPDATED then still shows when the value last changed, and not when an import last ran.

Exit status #

Case Exit status
A secret that has an importer is not filled: the source gave nothing, or reading it failed Non-zero
A name has no importer at all Informational. It does not fail the command.

brig secret import x && brig run x works for a profile that mixes imported and hand-created secrets.

Note On a machine that has never run the agent, there is nothing to import and the command exits non-zero. That state is normal.

Large credential documents #

Brig stores a credential document of several kilobytes the same way as a token. That covers Claude Code's document once plugins add their MCP OAuth state to it. It also covers codex's ~/.codex/auth.json with its two JWTs. The only limit is the 64 KiB read cap in Value input. See Storage for the layout.

Value input #

The value is never an argument. That keeps it out of ps and out of your shell history. It comes from stdin or from a file:

Source What gets stored
stdin, the default (--stdin spells it out) The bytes, less one trailing line ending
-f FILE The file's bytes, verbatim
-f - stdin, spelled out. Same stripping as stdin.

The two most common commands:

printf %s "$TOKEN" | brig secret create gh-token
brig secret create deploy-key -f ~/.ssh/id_ed25519

Trailing newlines #

echo adds a newline that is not part of the secret, and Brig strips it:

$ echo tok | brig secret create with-echo
$ brig secret read with-echo | xxd
00000000: 746f 6b                                  tok

The store holds three bytes. A trailing newline inside an Authorization: header fails in a way that reads like a bad token. CRLF counts as one line ending, because a lone \r left behind fails the same way.

Brig stores a file as it is. A PEM key's final newline belongs to the key:

$ printf 'tok\n' > tok.txt
$ brig secret create from-file -f tok.txt
$ brig secret read from-file | xxd
00000000: 746f 6b0a                                tok.

The store holds four bytes. For exact bytes from stdin, use printf %s. There is no flag for it:

$ printf %s 'tok' | brig secret create exact
$ brig secret read exact | xxd
00000000: 746f 6b                                  tok

Size cap #

Each source has a cap of 65536 bytes. create and update refuse a longer value before it reaches the store:

$ brig secret create x < /dev/zero
brig: the value on stdin is over 65536 bytes, which is larger than any secret brig can store. If that is a file or a stream rather than a credential, this is the wrong one

The cap stops streams. No credential comes near it.

Value output #

read writes the value to stdout and adds nothing, so a pipe gets the stored bytes.

On a terminal, read adds a trailing newline and prints a warning, because the value is now in the scrollback:

$ brig secret read gh-token
ghp_16C7e42F292c6912E7710c838347Ae178B4a
brig: gh-token is now in this terminal's scrollback
  → to keep it out, pipe it:  brig secret read gh-token | ...

The warning goes to stderr, so a pipe gets only the value.

Command substitution #

The shell's $(...) strips all trailing newlines. That is correct for a token, such as GH_TOKEN=$(gh auth token) brig run claude-code. It is wrong for a value whose trailing newline matters, such as a PEM key stored with -f:

$ brig secret read from-file | wc -c
       4
$ printf %s "$(brig secret read from-file)" | wc -c
       3

If the bytes matter, redirect:

(umask 077; brig secret read deploy-key > ./deploy-key)

Secret names #

Every verb enforces the same grammar:

  • letters, digits, - and _
  • starts with a letter
  • at most 128 characters
$ printf %s v | brig secret create gh.token
brig: a secret name holds letters, digits, - and _, and "gh.token" holds "."
$ printf %s v | brig secret create 1password
brig: a secret name starts with a letter, and "1password" starts with "1"

The name has three uses, and each one narrows the grammar:

Where the name is used What it rules out
The keychain account A space or a slash, which makes the item awkward to address by hand
A word in Brig's error messages A leading digit, which reads as a number, and a leading dash, which reads as a flag
The tail of ref: secrets.<name> in a profile A ., which makes that reference ambiguous

Storage #

A secret is two items in your login keychain, both under the service sh.brig.secret.

Item Holds
<name> A random 32-byte key and nothing else. Every such item is the same size.
<name>.sealed The value, encrypted with AES-256-GCM under that key. The secret's name is bound in, so a sealed item moved under another name does not open.

brig secret ls never shows the sealed item. The dot puts its name outside the grammar of a secret name, and the listing skips such names.

Brig writes the key item through security -i. That command reads one line into a 4096-byte buffer and shortens a longer line without a message. That limits a value on the line to about 3KB, which is smaller than some credential documents. So Brig writes the sealed item through the arguments of security, which have no such cap.

Other processes on the host can read a command line. This command line holds only ciphertext and the secret's name. The value is never on any command line. The key reaches security only through a pipe.

update keeps the key and replaces the sealed item. security replaces the item whole, so that replacement is the one step that changes the value.

delete removes the key item first, then the sealed item. Once the key is gone the sealed item is unreadable, so a failure between the two steps leaves nothing usable behind.

Anything that can read Brig's keychain items as you can read the key and open the sealed item. Anything that cannot open the keychain gets ciphertext. Security model says what the keychain item's ACL does and does not stop.

Brig stores into a Secret Service keyring on your D-Bus session bus. gnome-keyring and KWallet both speak that API.

Brig keeps one item per secret in your default collection. The value is in the keyring item, because a Secret Service keyring has no size ceiling like the one on macOS. Each item is tagged as Brig's, and Brig lists and touches nothing else in the keyring. If the collection is locked, Brig opens it through your keyring UI.

If the D-Bus session bus or the keyring is missing, brig secret says which one. It does not fall back to a file:

$ brig secret create gh-token
brig: no secret store on this platform: a D-Bus session bus is running but no Secret Service answers on it. Install a keyring (gnome-keyring or KWallet, both speak the Secret Service API) and log in to a session that starts it, or read the secret once from a command's output with `brig secret import <profile> --from-command '<sh>'`, which stores it like any other import

The check happens before Brig reads a value from stdin.

--from-command runs the command once and stores its stdout like any other import. It still needs a keyring to write into.

Secrets stored by older versions on macOS

A secret stored by an older Brig holds its value in the keychain item itself. read returns it. The next update or re-import moves it into the two-item layout. You do not migrate anything by hand.

An older Brig that reads a key item refuses it as one it did not write. It does not hand the key bytes to a guest.

Errors #

What Brig says What happened
a secret named "x" already exists. To replace it: brig secret update x create will not overwrite.
no secret named "x". To create it: brig secret create x update will not create.
no secret named "x". `brig secret ls` lists them read or delete on a name that is not there.
create x was given an empty value, and brig skips empty variables when it forwards them, so it would never reach a sandbox The source was empty.
no value on stdin. Pipe one in, or pass -f <file>: … (and prints two examples) create at a prompt with nothing piped in. It does not wait, because a value typed there goes into your scrollback.
-f was given an empty path. Leave it out to read stdin, or pass `-f -` to say so -f "$KEYFILE" with the variable unset. Brig does not fall through to stdin, which can hold other data from the script.
--stdin and -f name two different sources; pass one Both were given.
the value on stdin is over 65536 bytes, which is larger than any secret brig can store. If that is a file or a stream rather than a credential, this is the wrong one create or update read more than 65536 bytes before reaching the store. With -f FILE, the message names the file in place of stdin.
"x" secret is damaged: the key is in the keychain, but its sealed value is missing. Store it again: … The x.sealed item was removed and the key item was not. See Storage.
"x" secret is damaged: the sealed item does not open with the key stored for it, so one of them was changed outside brig. Store it again: … One of the two items was replaced by something other than Brig. Storing the value again replaces both.
"x" secret is damaged: the sealed item is not a brig sealed value, so something other than brig put it there. Store it again: … The x.sealed item holds something that is not Brig's format. Storing the value again replaces both.
deleting "x" cannot be undone, and there is no terminal to ask on. Pass -y to answer in advance: … delete ran with no terminal, such as in a cron job or a unit file.
a secret name holds letters, digits, - and _, ... See Secret names.
no secret store on this platform: … no Secret Service answers on it … Linux with no keyring on the D-Bus session bus. Install gnome-keyring or KWallet and log in to a desktop session that starts it.
no secret store on this platform: … a keyring is running but has no default collection … Linux with a keyring but no default collection, such as a headless or freshly provisioned session that has not opened one. Open your keyring once from a desktop session, which creates it.
"x" is a secret, not a profile, and import takes the profile that declares it: … import's first argument is a profile. The message names the profile that declares the secret you typed.
nothing to import for "x": … held no value The profile's sources exist and none of them had anything. Usually: run the agent on the host once to log in.
"x" is already stored and brig did not put it there, so importing would replace a value you supplied You created it by hand. Pass -y if you mean to replace it.
--from-command fills one secret, so it needs one name It supplies a value, and nothing in the command says which secret it is for.
the imported credential x (y) expired N ago, followed by renew it on the host, then: brig secret import y A run found a stored, imported credential past its expiryField:. Log in on the host again and import again.

Type a command, a flag or an error message.