# 🤖 The one way scripts talk to the Bitwarden CLI. Sourced (not run) by # release-server.sh, doctor.sh, ops-dev.sh — POSIX sh. # # Two rules, learned the hard way: # # 1. **bw never prompts — we do.** bw's inquirer prompt writes to stdout, so # inside `$(...)` it sits invisible and the script "hangs" until a stray # Enter redraws it (the doctor lesson, then the release-server lesson). # Instead, OUR prompt (stty -echo + a plain `read -r`, POSIX — dash has # no `read -s`) reads /dev/tty with the label on stderr — # always visible, capture-proof — and the password reaches bw through # --passwordenv: never argv (ps would show it), never disk, unset the # moment the unlock returns. bw is a non-interactive backend, full stop. # (The one exception is `bw login` — 2FA is genuinely interactive and # rare; scripts detect "unauthenticated" and hand the human that ONE # command to run themselves.) # # 2. **Every bw call gets a watchdog.** bw's API calls carry no timeout and # can stall for good on a bad connection or stale session (a promote sat # 14 silent minutes in `bw get` 2026-07-21; another hung indefinitely in # `bw unlock` 2026-07-24). perl's alarm is the never-a-hang promise — # the timer survives exec, and macOS ships perl. # Rule 1, enforced globally: with BW_NOINTERACTION set, a locked-vault # `bw get`/`bw list` fails immediately instead of prompting — without it, bw's # invisible in-pipe prompt eats a stray Enter before falling through to OUR # unlock prompt (the "I still had to press Enter" papercut, 2026-07-25). # Exported at source time so every bw call in the sourcing script is covered. BW_NOINTERACTION=true export BW_NOINTERACTION # bw_run — run one bw command under a watchdog. # Exit status is bw's, or SIGALRM's (142) on a stall. stdin is pinned to # /dev/null — none of our calls (get/list/sync/status) legitimately read it, # so nothing bw does can ever wait on the operator's keyboard. bw_run() { _bw_t="$1"; shift perl -e "alarm $_bw_t; exec @ARGV" bw "$@" # # Fails (rather than blocking) without a tty: headless contexts export # BW_SESSION themselves or use the explicit env-key overrides. bw_unlock_session() { if [ ! -e /dev/tty ]; then echo "bw: no tty to prompt on — export BW_SESSION (bw unlock --raw) first" >&2 return 1 fi printf "Bitwarden master password (typing is hidden): " >&2 # `read -s` is a bashism — Debian's /bin/sh (dash) rejects it with # "Illegal option -s" (bit an operator mid-publish, 2026-08-06). Hide the # typing the POSIX way: drop tty echo around a plain `read -r`. Restore on # every exit from the read, including ^C at the prompt (restore, then # re-raise INT so the script still dies) — a password prompt that leaves # the terminal echo-less is its own papercut. The `|| true` tails keep # `set -e` sourcing scripts alive when there's no stty to save (odd ttys). _bw_stty="$(stty -g /dev/null || true)" _bw_restore_tty() { [ -n "$_bw_stty" ] && stty "$_bw_stty" /dev/null || true; } trap '_bw_restore_tty; trap - INT; kill -INT $$' INT [ -n "$_bw_stty" ] && stty -echo /dev/null || true IFS= read -r LCHAT_BW_PASSWORD &2 unset LCHAT_BW_PASSWORD return 1 } _bw_restore_tty trap - INT echo >&2 # 90s: the KDF is seconds, but bw's first server round-trip gets headroom. _bw_sess="$(LCHAT_BW_PASSWORD="$LCHAT_BW_PASSWORD" perl -e 'alarm 90; exec @ARGV' \ bw unlock --raw --passwordenv LCHAT_BW_PASSWORD &2 return 1 fi printf '%s\n' "$_bw_sess" }