# Bitwarden CLI + macOS Keychain — Secret Infrastructure on macOS How machines in the fleet store and use Bitwarden credentials. Original setup: ole Blu (MacBook Air, `ole-blu.local`). Replicated on Mac Studio 2022 (`mac.lan`). ## Architecture | What | Where | Notes | |------|-------|-------| | Bitwarden API key (client id/secret) | `~/.config/bw/env`, mode 0600 | Two lines, both `export`-ed. Non-master secret; file-safe. | | Bitwarden master password | macOS Keychain | Service `bw-master`, account ``. NEVER in a file. | | Service credentials (API keys, app passwords) | Bitwarden vault items | Login type (type:1); fetched per-use at runtime. | Session keys (`BW_SESSION`) are transient — never written to disk. ## Why this split - The API key only identifies/authenticates the CLI to Bitwarden's API; exposure doesn't unlock the vault. It's low-sensitivity, so a 0600 file is acceptable and enables non-interactive login. - The master password unlocks everything. Keychain storage gives ACL-protected, prompt-on-first-access, encrypted-at-rest handling with zero file copies. - Scripts fetch vault secrets per-use and unset them — no `export SECRET=` in shell profiles (readable via `ps eww`, lands in every child env). ## Setup on a new macOS machine ```zsh # 1. Install CLI brew install bitwarden-cli # 2. Copy env file from an existing machine (secret never enters chat/session logs) mkdir -p ~/.config/bw scp existing-machine:.config/bw/env ~/.config/bw/env chmod 600 ~/.config/bw/env # 3. Log the CLI in (prints "already logged in" if done — that's fine) source ~/.config/bw/env bw login --apikey # 4. Store the master password in Keychain (interactive; user types it once) security add-generic-password -s bw-master -a "$USER" -w # ^ prompts: password, then confirm. Never echoed. # 5. Verify (do not print the password itself) security find-generic-password -s bw-master -w | wc -c # non-zero ``` To get an API key if starting from scratch: https://vault.bitwarden.com → Settings → Security → Keys → View API key. ## Unlocking in scripts Paste this block at the top of any script that needs vault access: ```zsh [ -f "$HOME/.config/bw/env" ] && source "$HOME/.config/bw/env" if [ -z "${BW_PASSWORD:-}" ]; then BW_PASSWORD=$(security find-generic-password -s bw-master -w 2>/dev/null) || exit 1 fi export BW_PASSWORD bw login --apikey 2>/dev/null || true BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null) # ... do work, fetching secrets per-use (below) ... unset BW_SESSION BW_PASSWORD # clear when done ``` ## Fetching secrets Fetch the item ONCE and extract both fields (separate `bw get` calls can return mismatched values if the item changed between calls): ```zsh ITEM=$(bw get item "Porkbun API" --session "$BW_SESSION") API_KEY=$(echo "$ITEM" | jq -r '.login.username') API_SECRET=$(echo "$ITEM" | jq -r '.login.password') # Validate — empty/null means wrong name or missing field [ -z "$API_KEY" ] || [ "$API_KEY" = "null" ] && { echo "missing fields" >&2; exit 1; } ``` Password-only: `bw get password "Item Name" --session "$BW_SESSION"`. Item naming is a contract — scripts match exact names. Current conventions: | Bitwarden item | Username field | Password field | |----------------|----------------|----------------| | Porkbun API | pk1_... API key | sk1_... secret | | Bluesky App Password | handle | app password | ## Pitfalls - `source` sets shell variables, NOT environment variables. Always `export` BW_* vars, or `bw unlock --passwordenv BW_PASSWORD` silently fails (empty `--raw`). - `bw login --apikey --raw` returns nothing when already logged in. Use non-raw `bw login --apikey` and gate on `bw unlock` instead. - First Keychain read from a new process pops a macOS approval dialog ("security wants to use your confidential information") — one-time per item per session. - `bw get` matches the FIRST duplicate item name. After creating: check `bw list items --search "Name" --session "$BW_SESSION" | jq 'length'` → 1. - CLI caches vault data locally. After editing the vault elsewhere, `bw sync`. - Never paste live secret values into AI agent sessions or shell history — they persist. Reference the item name instead. Rotate + purge anything that leaked. - First Keychain read from a new process may pop a macOS dialog — expected. ## Fleet status | Machine | Status | |---------|--------| | ole Blu (MacBook Air, ole-blu.local) | Original — full setup | | Mac Studio 2022 (mac.lan) | Complete 2026-09-11: env via scp, login verified, Keychain item added, unlock + sync verified (664 items) |