Skip to content

Secrets

Rui edited this page Nov 3, 2026 · 1 revision

Secrets

Secrets never go in the repo, and preferably not in ~/.zshrc.local either: plain-text files end up in backups, screen shares and shell history. Bitwarden is the source of truth; a tool gets a secret only when it starts, so it never sits in a file or in every shell's environment.

Bitwarden: bw

bw is a wrapper (bw.zsh): the session key lives in a root-only file, so the first bw in a shell asks for Touch ID (sudo) and later ones reuse it, without the master password. It needs export BW_USER=<email> in ~/.zshrc.local; bw --regenerate-session-key starts a new session. The key reaches the CLI through BW_SESSION, never on the command line (ps would show it).

A tool that needs a token, in ~/.zshrc.local:

mytool() {
  local token
  token=$(bw get password <item>) || return
  MYTOOL_TOKEN=$token command mytool "$@"
}

Several values in one item: bw get item <item> | jq -r '.fields[] | select(.name == "<field>").value'. Never fetch secrets at the top level of .zshrc*: every new shell would block on the vault.

A token for one command: secret

secret T                 # asks without echoing it
secret T <item>          # or from Bitwarden
claude mcp add ... --header "Authorization: Bearer $T"

$T is cleared after the next command and never exported. Lines that look like they hold a token (GitHub / GitLab / Slack / OpenAI / Anthropic keys, AWS access keys, JWTs, a long Bearer, --password=...) are not saved to the history; a leading space keeps any command out of it.

SOPS age keys

The keys for SOPS-encrypted files live in one Bitwarden secure note, SOPS age keys, never on disk by default.

Command
sops-on [60m] writes them to sops's keys file for a while (90, 90m, 30s, 2h; default SOPS_ON_MINUTES, else 60), then removes them with a notice and a macOS notification
sops-off removes them now
sops-status the time left
sops-save [FILE] puts a keys file in the note
sops-login [on|off] the clean-up at login on this machine (below)
  • A keys file sops-on did not create is never overwritten or removed.
  • A log out or restart removes them: the timer dies with the session, so a LaunchAgent (.config/dotfiles/launchd) cleans up at login, whatever time was left. sops-login off turns that off on a machine where it makes no sense (macOS keeps the choice across reboots and syncs), sops-login on back on.
  • Neovim opens SOPS files decrypted and re-encrypts on :w (sops.nvim), with undo history and swap off for them.

What keeps secrets out

  • gitleaks scans every commit's staged changes (global hook), and CI scans the whole history.
  • sops-guard stops a commit of a file sops should have encrypted but that is staged in cleartext: named *.sops.* / *.enc.*, or matched by a path_regex of a .sops.yaml (read as Go regexes, as sops does). See Git.
  • The global gitignore keeps .env and .env.* out (not .env.example; git add -f overrides).
  • Claude Code is blocked from reading keys, credentials and env files (Claude Code).
  • Check by hand: git grep -nIE '(token|secret|password|api_?key)\s*='.

Clone this wiki locally