diff --git a/dot_claude/skills/vaultwarden.md b/dot_claude/skills/vaultwarden.md new file mode 100644 index 0000000..04e6b2c --- /dev/null +++ b/dot_claude/skills/vaultwarden.md @@ -0,0 +1,49 @@ +name: vaultwarden +description: Manage passwords in Vaultwarden. Use when needing credentials + for services, databases, APIs, or when storing new secrets. +--- + +## CRITICAL: Credential Blindness +NEVER use commands that would print passwords to stdout. +Passwords must NEVER appear in tool output sent to Anthropic's API. + +## Available commands (each triggers Touch ID) + +### Search (safe — returns metadata only) +```bash +bw-vault search +``` +Returns: item name, username, URL, id — NO passwords + +### Inject password into a command (safe — password never in output) +```bash +bw-vault inject --as -- +``` +Example: `bw-vault inject "prod-db" --as PGPASSWORD -- psql -h db.local -U admin` + +### Copy to clipboard (safe — only "Copied" message returned) +```bash +bw-vault copy [field] +``` +field defaults to "password", can be "username", "totp", "uri" + +### Write to temp file (safe — only file path returned) +```bash +bw-vault file /tmp/secret-XXXX +``` + +### Create new item (password auto-generated) +```bash +bw-vault create +``` + +### Edit existing item +```bash +bw-vault edit +``` + +## NEVER DO +- `bw get password ` — would leak to API +- `cat /tmp/secret-XXXX` — would leak file contents to API +- `echo $PGPASSWORD` — would leak env var to API +- Any command that prints a secret value to stdout diff --git a/dot_local/bin/bw-vault-unlock b/dot_local/bin/bw-vault-unlock new file mode 100644 index 0000000..200e4d4 --- /dev/null +++ b/dot_local/bin/bw-vault-unlock @@ -0,0 +1,274 @@ +#!/bin/bash +# bw-vault-unlock — Privileged Bitwarden CLI wrapper +# Installed to /usr/local/bin/bw-vault-unlock (root:wheel 0755) +# Called via sudo from ~/.local/bin/bw-vault +# +# Security: passwords never printed to stdout. Only metadata, exit codes, +# and "Copied to clipboard" messages are returned. +set -euo pipefail +export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin" + +BW_BIN="/opt/homebrew/bin/bw" +BW_HASH_FILE="/var/root/.bw-hash" + +# Verify bw binary integrity +if [[ -f "$BW_HASH_FILE" ]]; then + EXPECTED_HASH=$(/usr/bin/head -1 "$BW_HASH_FILE") + ACTUAL_HASH=$(/usr/bin/shasum -a 256 "$BW_BIN" | /usr/bin/awk '{print $1}') + if [[ "$EXPECTED_HASH" != "$ACTUAL_HASH" ]]; then + echo "ERROR: bw binary integrity check failed" >&2 + echo "Expected: $EXPECTED_HASH" >&2 + echo "Actual: $ACTUAL_HASH" >&2 + echo "Run: sudo shasum -a 256 /opt/homebrew/bin/bw | awk '{print \$1}' > /var/root/.bw-hash" >&2 + exit 1 + fi +fi + +export BITWARDENCLI_APPDATA_DIR="/var/root/.bw-data" + +# Subcommand allowlist +SUBCMD="${1:-}" +case "$SUBCMD" in + search|inject|copy|file|create|edit) ;; + *) + echo "Usage: bw-vault {search|inject|copy|file|create|edit} [args...]" >&2 + echo "" >&2 + echo "Commands:" >&2 + echo " search Search vault (metadata only)" >&2 + echo " inject --as -- Run command with secret as env var" >&2 + echo " copy [field] Copy secret to clipboard" >&2 + echo " file Write secret to file (0600)" >&2 + echo " create Create new vault item" >&2 + echo " edit Edit existing vault item" >&2 + exit 1 + ;; +esac + +# Read credentials using awk (not source — prevents env leakage) +CRED_FILE="/var/root/.bw-credentials" +if [[ ! -f "$CRED_FILE" ]]; then + echo "ERROR: credentials file not found at $CRED_FILE" >&2 + echo "Run the setup script first: sudo bw-vault-setup" >&2 + exit 1 +fi + +BW_CLIENTID=$(/usr/bin/awk -F= '/^BW_CLIENTID=/{print $2}' "$CRED_FILE") +BW_CLIENTSECRET=$(/usr/bin/awk -F= '/^BW_CLIENTSECRET=/{print $2}' "$CRED_FILE") +BW_PASSWORD=$(/usr/bin/awk -F= '/^BW_PASSWORD=/{print $2}' "$CRED_FILE") + +cleanup() { + # Logout on exit (best-effort) + env -i PATH="$PATH" BITWARDENCLI_APPDATA_DIR="$BITWARDENCLI_APPDATA_DIR" \ + "$BW_BIN" logout --nointeraction 2>/dev/null || true +} +trap cleanup EXIT + +# Login with API key (clean env, idempotent) +env -i PATH="$PATH" BITWARDENCLI_APPDATA_DIR="$BITWARDENCLI_APPDATA_DIR" \ + BW_CLIENTID="$BW_CLIENTID" BW_CLIENTSECRET="$BW_CLIENTSECRET" \ + "$BW_BIN" login --apikey --nointeraction 2>/dev/null || true + +# Unlock vault — password via stdin +SESSION=$(echo "$BW_PASSWORD" | env -i PATH="$PATH" \ + BITWARDENCLI_APPDATA_DIR="$BITWARDENCLI_APPDATA_DIR" \ + "$BW_BIN" unlock --passwordfile /dev/stdin --raw 2>/dev/null) + +# Clear credentials from memory ASAP +unset BW_CLIENTID BW_CLIENTSECRET BW_PASSWORD + +if [[ -z "$SESSION" ]]; then + echo "ERROR: failed to unlock vault" >&2 + exit 1 +fi + +# Helper to run bw commands with session +bw_cmd() { + env -i PATH="$PATH" BITWARDENCLI_APPDATA_DIR="$BITWARDENCLI_APPDATA_DIR" \ + "$BW_BIN" "$@" --session "$SESSION" 2>/dev/null +} + +case "$SUBCMD" in + search) + shift + if [[ $# -eq 0 ]]; then + echo "ERROR: search requires a query" >&2 + exit 1 + fi + QUERY="$*" + # Return metadata only — strip password, notes, totp, fields + bw_cmd list items --search "$QUERY" | \ + /usr/bin/python3 -c " +import sys, json +try: + items = json.load(sys.stdin) +except json.JSONDecodeError: + print('[]') + sys.exit(0) +for i in items: + i.pop('notes', None) + i.pop('fields', None) + i.pop('passwordHistory', None) + if 'login' in i and i['login']: + i['login'].pop('password', None) + i['login'].pop('totp', None) + if 'card' in i and i['card']: + i['card'].pop('number', None) + i['card'].pop('code', None) + if 'identity' in i and i['identity']: + i['identity'].pop('ssn', None) +json.dump(items, sys.stdout, indent=2) +" + ;; + inject) + shift + if [[ $# -lt 4 ]]; then + echo "ERROR: usage: inject --as -- " >&2 + exit 1 + fi + ITEM_ID="$1"; shift + if [[ "$1" != "--as" ]]; then + echo "ERROR: expected --as, got '$1'" >&2 + exit 1 + fi + shift + VAR_NAME="$1"; shift + # Validate var name (alphanumeric + underscore only) + if [[ ! "$VAR_NAME" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then + echo "ERROR: invalid env var name '$VAR_NAME'" >&2 + exit 1 + fi + if [[ "$1" != "--" ]]; then + echo "ERROR: expected --, got '$1'" >&2 + exit 1 + fi + shift + if [[ $# -eq 0 ]]; then + echo "ERROR: no command specified after --" >&2 + exit 1 + fi + SECRET=$(bw_cmd get password "$ITEM_ID") + if [[ -z "$SECRET" ]]; then + echo "ERROR: failed to retrieve password for '$ITEM_ID'" >&2 + exit 1 + fi + # Run the target command with the secret as an env var + export "$VAR_NAME=$SECRET" + unset SECRET + "$@" + ;; + copy) + shift + if [[ $# -eq 0 ]]; then + echo "ERROR: copy requires an item id/name" >&2 + exit 1 + fi + ITEM_ID="$1" + FIELD="${2:-password}" + bw_cmd get "$FIELD" "$ITEM_ID" | /usr/bin/pbcopy + echo "Copied $FIELD to clipboard" + ;; + file) + shift + if [[ $# -lt 2 ]]; then + echo "ERROR: usage: file " >&2 + exit 1 + fi + ITEM_ID="$1" + OUTPATH="$2" + umask 077 + bw_cmd get password "$ITEM_ID" > "$OUTPATH" + chmod 600 "$OUTPATH" + echo "Written to $OUTPATH (mode 0600)" + ;; + create) + shift + echo "Interactive item creation:" + read -rp "Item name: " ITEM_NAME + read -rp "Username: " ITEM_USER + read -rp "URL (optional): " ITEM_URL + read -rp "Generate password? [Y/n]: " GEN_PASS + if [[ "${GEN_PASS,,}" != "n" ]]; then + ITEM_PASS=$(bw_cmd generate -ulns --length 32) + else + read -rsp "Password: " ITEM_PASS + echo + fi + # Build the item JSON + ITEM_JSON=$(/usr/bin/python3 -c " +import json, sys +item = { + 'organizationId': None, + 'folderId': None, + 'type': 1, + 'name': sys.argv[1], + 'notes': None, + 'favorite': False, + 'login': { + 'uris': [{'match': None, 'uri': sys.argv[3]}] if sys.argv[3] else [], + 'username': sys.argv[2], + 'password': sys.argv[4], + 'totp': None + }, + 'fields': [], + 'reprompt': 0 +} +print(json.dumps(item)) +" "$ITEM_NAME" "$ITEM_USER" "$ITEM_URL" "$ITEM_PASS") + unset ITEM_PASS + RESULT=$(echo "$ITEM_JSON" | bw_cmd encode | bw_cmd create item) + CREATED_ID=$(echo "$RESULT" | /usr/bin/python3 -c "import sys,json; print(json.load(sys.stdin).get('id','unknown'))" 2>/dev/null || echo "unknown") + echo "Created item: $CREATED_ID (use 'bw-vault copy $CREATED_ID' to retrieve password)" + ;; + edit) + shift + if [[ $# -eq 0 ]]; then + echo "ERROR: edit requires an item id/name" >&2 + exit 1 + fi + ITEM_ID="$1" + # Get current item + CURRENT=$(bw_cmd get item "$ITEM_ID") + if [[ -z "$CURRENT" ]]; then + echo "ERROR: item '$ITEM_ID' not found" >&2 + exit 1 + fi + CURRENT_NAME=$(echo "$CURRENT" | /usr/bin/python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))") + CURRENT_USER=$(echo "$CURRENT" | /usr/bin/python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('login',{}).get('username','') if d.get('login') else '')") + echo "Editing: $CURRENT_NAME (user: $CURRENT_USER)" + read -rp "New username (enter to keep): " NEW_USER + read -rp "New password? [y/N]: " CHANGE_PASS + if [[ "${CHANGE_PASS,,}" == "y" ]]; then + read -rp "Generate password? [Y/n]: " GEN_PASS + if [[ "${GEN_PASS,,}" != "n" ]]; then + NEW_PASS=$(bw_cmd generate -ulns --length 32) + else + read -rsp "New password: " NEW_PASS + echo + fi + UPDATED=$(echo "$CURRENT" | /usr/bin/python3 -c " +import sys, json +item = json.load(sys.stdin) +new_user = sys.argv[1] if sys.argv[1] else None +new_pass = sys.argv[2] if sys.argv[2] else None +if new_user and item.get('login'): + item['login']['username'] = new_user +if new_pass and item.get('login'): + item['login']['password'] = new_pass +print(json.dumps(item)) +" "$NEW_USER" "$NEW_PASS") + unset NEW_PASS + else + UPDATED=$(echo "$CURRENT" | /usr/bin/python3 -c " +import sys, json +item = json.load(sys.stdin) +new_user = sys.argv[1] if sys.argv[1] else None +if new_user and item.get('login'): + item['login']['username'] = new_user +print(json.dumps(item)) +" "$NEW_USER") + fi + ITEM_REAL_ID=$(echo "$CURRENT" | /usr/bin/python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") + echo "$UPDATED" | bw_cmd encode | bw_cmd edit item "$ITEM_REAL_ID" >/dev/null + echo "Updated item: $ITEM_REAL_ID" + ;; +esac diff --git a/dot_local/bin/executable_bw-vault b/dot_local/bin/executable_bw-vault new file mode 100644 index 0000000..64a2ecf --- /dev/null +++ b/dot_local/bin/executable_bw-vault @@ -0,0 +1,3 @@ +#!/bin/bash +# User-facing wrapper — triggers Touch ID via sudo → runs privileged bw operation +sudo /usr/local/bin/bw-vault-unlock "$@" diff --git a/dot_local/bin/executable_bw-vault-setup b/dot_local/bin/executable_bw-vault-setup new file mode 100644 index 0000000..fae17a9 --- /dev/null +++ b/dot_local/bin/executable_bw-vault-setup @@ -0,0 +1,192 @@ +#!/bin/bash +# bw-vault-setup — One-time setup for bw-vault (Vaultwarden CLI integration) +# Run this script manually: bash ~/.local/bin/bw-vault-setup +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { echo -e "${RED}[✗]${NC} $*" >&2; } + +# Check we're running as the user (not root) +if [[ $EUID -eq 0 ]]; then + error "Run this script as your user (not sudo). It will prompt for sudo when needed." + exit 1 +fi + +echo "============================================" +echo " bw-vault Setup — Vaultwarden CLI for Claude Code" +echo "============================================" +echo "" + +# Step 1: Fix Homebrew permissions if needed +info "Checking Homebrew..." +if ! brew --version >/dev/null 2>&1; then + error "Homebrew not found. Install it first: https://brew.sh" + exit 1 +fi + +# Check if brew dirs are writable +if [[ ! -w /opt/homebrew/Cellar ]]; then + warn "Homebrew directories not writable. Fixing ownership..." + sudo chown -R "$(whoami)" /opt/homebrew +fi + +# Step 2: Install dependencies +info "Installing bitwarden-cli and pam-reattach..." +brew install bitwarden-cli pam-reattach 2>/dev/null || { + # May already be installed + brew upgrade bitwarden-cli pam-reattach 2>/dev/null || true +} + +# Verify installations +if ! command -v bw >/dev/null; then + error "bitwarden-cli installation failed" + exit 1 +fi +info "bw CLI version: $(bw --version)" + +if [[ ! -f /opt/homebrew/lib/pam/pam_reattach.so ]]; then + error "pam-reattach installation failed" + exit 1 +fi +info "pam-reattach installed" + +# Step 3: Configure Touch ID for sudo (with tmux support) +info "Configuring Touch ID for sudo..." +SUDO_LOCAL="/etc/pam.d/sudo_local" +if [[ -f "$SUDO_LOCAL" ]] && grep -q pam_tid "$SUDO_LOCAL"; then + info "Touch ID for sudo already configured" +else + sudo bash -c "cat > $SUDO_LOCAL << 'PAMEOF' +# pam-reattach: required for Touch ID to work in tmux/screen +auth optional /opt/homebrew/lib/pam/pam_reattach.so +auth sufficient pam_tid.so +PAMEOF" + info "Created $SUDO_LOCAL" +fi + +# Step 4: Configure scoped sudo for bw-vault +info "Configuring scoped sudo for bw-vault..." +SUDOERS_FILE="/etc/sudoers.d/bw-vault" +if [[ -f "$SUDOERS_FILE" ]]; then + info "Sudoers config already exists" +else + CURRENT_USER=$(whoami) + sudo bash -c "cat > $SUDOERS_FILE << SUDOEOF +Cmnd_Alias BW_VAULT = /usr/local/bin/bw-vault-unlock +Defaults!BW_VAULT timestamp_timeout=0 +$CURRENT_USER ALL=(root) BW_VAULT +SUDOEOF +chmod 0440 $SUDOERS_FILE" + # Validate sudoers syntax + if sudo visudo -cf "$SUDOERS_FILE" >/dev/null 2>&1; then + info "Sudoers config validated and installed" + else + error "Sudoers config has syntax errors — removing" + sudo rm -f "$SUDOERS_FILE" + exit 1 + fi +fi + +# Step 5: Create root bw data directory +info "Creating root bw data directory..." +sudo bash -c 'mkdir -p /var/root/.bw-data && chmod 700 /var/root/.bw-data' + +# Step 6: Configure bw CLI server URL (as root) +info "Configuring Bitwarden CLI server URL..." +sudo BITWARDENCLI_APPDATA_DIR=/var/root/.bw-data /opt/homebrew/bin/bw config server https://vaultwarden.viktorbarzin.me + +# Step 7: Store API credentials +info "Setting up Vaultwarden API credentials..." +echo "" +echo "You need your Vaultwarden Personal API Key." +echo "Get it from: https://vaultwarden.viktorbarzin.me/#/settings/security/security-keys" +echo " → API Key section → View API Key" +echo "" + +if sudo test -f /var/root/.bw-credentials; then + read -rp "Credentials file already exists. Overwrite? [y/N]: " OVERWRITE + if [[ "${OVERWRITE,,}" != "y" ]]; then + info "Keeping existing credentials" + else + _store_creds=true + fi +else + _store_creds=true +fi + +if [[ "${_store_creds:-}" == "true" ]]; then + read -rp "BW_CLIENTID (e.g. user.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx): " _clientid + read -rp "BW_CLIENTSECRET: " _clientsecret + read -rsp "Master password: " _password + echo "" + + sudo bash -c "umask 077; cat > /var/root/.bw-credentials << CREDEOF +BW_CLIENTID=$_clientid +BW_CLIENTSECRET=$_clientsecret +BW_PASSWORD=$_password +CREDEOF" + unset _clientid _clientsecret _password + info "Credentials stored in /var/root/.bw-credentials (root:wheel 0600)" +fi + +# Step 8: Install the privileged unlock script +info "Installing bw-vault-unlock to /usr/local/bin/..." +sudo cp ~/.local/bin/bw-vault-unlock /usr/local/bin/bw-vault-unlock +sudo chown root:wheel /usr/local/bin/bw-vault-unlock +sudo chmod 0755 /usr/local/bin/bw-vault-unlock + +# Step 9: Pin bw binary hash +info "Pinning bw binary hash..." +BW_HASH=$(shasum -a 256 /opt/homebrew/bin/bw | awk '{print $1}') +sudo bash -c "echo '$BW_HASH' > /var/root/.bw-hash && chmod 600 /var/root/.bw-hash" +info "Hash: $BW_HASH" + +# Step 10: Verify +echo "" +echo "============================================" +echo " Verification" +echo "============================================" +echo "" + +# Test sudo Touch ID +info "Testing sudo (Touch ID should appear)..." +if sudo echo "sudo works"; then + info "sudo + Touch ID: OK" +else + warn "sudo test failed" +fi + +# Test credential file is protected +if cat /var/root/.bw-credentials 2>/dev/null; then + error "SECURITY: credentials file is readable by user!" +else + info "Credential file protected: OK" +fi + +# Test bw-vault search +info "Testing bw-vault search (Touch ID will appear)..." +if bw-vault search "test" >/dev/null 2>&1; then + info "bw-vault search: OK" +else + warn "bw-vault search test failed (may be normal if vault is empty)" +fi + +echo "" +info "Setup complete!" +echo "" +echo "Usage:" +echo " bw-vault search # Search (metadata only)" +echo " bw-vault inject --as -- # Inject secret into command" +echo " bw-vault copy [field] # Copy to clipboard" +echo " bw-vault file # Write to file (0600)" +echo " bw-vault create # Create new item" +echo " bw-vault edit # Edit item" +echo "" +echo "After brew upgrade bitwarden-cli, update the hash:" +echo " sudo bash -c \"\$(shasum -a 256 /opt/homebrew/bin/bw | awk '{print \\\$1}') > /var/root/.bw-hash\""