Skip to content
Self-Host Vaultwarden on Mac: Let AI Assistants Fetch Secrets by Name
Operations 16 min read

Self-Host Vaultwarden on Mac: Let AI Assistants Fetch Secrets by Name

This guide shows how to self-host Vaultwarden on a Mac using Docker and Caddy, create a read-only account for an AI coding assistant, and wrap the Bitwarden CLI in a script that unlocks, retrieves exact-match passwords by name, and immediately locks the vault, keeping secrets out of chat history and environment files.

Tech Ocean
Tech Ocean
Tech Ocean

Why Self-Host a Password Vault for AI Assistants

When an AI coding assistant needs to connect to Redis or call a third-party API, its first request is often for credentials. The easiest but insecure approach is pasting passwords into the chat or writing them into a project's .env file. Over time, secrets scatter across session logs, .env files, and any files the assistant reads with cat. Rotating a password then requires hunting down every occurrence.

Vaultwarden: A Lightweight Bitwarden-Compatible Server

Vaultwarden is a Rust implementation of the Bitwarden server API, open-source under AGPL-3.0. It is not an official Bitwarden project, but the official Bitwarden web vault, browser extensions, and the bw CLI all connect to it directly. It runs as a single container, stores data in a SQLite file, and idles at only tens of megabytes of memory. Features like organizations, collections, and member permissions are built in and used here to scope the assistant's access.

Local-Only Deployment with Docker Compose and Caddy

Two containers are orchestrated via Docker Compose in ~/vaultwarden: Vaultwarden itself (no exposed ports) and Caddy as a TLS terminator bound to 127.0.0.1:8443. This ensures the service is only reachable from the Mac itself.

services:
  vaultwarden:
    image: vaultwarden/server
    restart: unless-stopped
    environment:
      DOMAIN: https://localhost:8443
      SIGNUPS_ALLOWED: ${VW_SIGNUPS_ALLOWED:-false}
      INVITATIONS_ALLOWED: "false"
    volumes:
      - ./data:/data

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./certs:/certs:ro

The Caddyfile uses a self-signed certificate and proxies to Vaultwarden:

{
  admin off
  auto_https off
}

:443 {
  tls /certs/vw.crt /certs/vw.key
  reverse_proxy vaultwarden:80
}

Generating and Trusting a Self-Signed Certificate on macOS

HTTPS is mandatory because browser crypto APIs used by the Bitwarden web vault only work over HTTPS. A self-signed certificate meeting macOS requirements (SAN with hostname, serverAuth EKU, validity ≤825 days) is generated with the system OpenSSL and added to the login keychain:

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 825 \
  -subj "/CN=localhost" -keyout certs/vw.key -out certs/vw.crt \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature,keyEncipherment" \
  -addext "extendedKeyUsage=serverAuth" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

# Trust the cert in macOS (prompts for login password)
security add-trusted-cert -r trustRoot -p ssl \
  -k ~/Library/Keychains/login.keychain-db certs/vw.crt

Controlling Sign-Ups via Environment Variable

Vaultwarden enables registration by default. The Compose file sets SIGNUPS_ALLOWED to false via a default variable. To create accounts, temporarily override it:

VW_SIGNUPS_ALLOWED=true docker compose up -d   # enable sign-up
docker compose up -d                       # disable after accounts are created

After disabling, registration attempts return Registration not allowed or user already exists.

Two Accounts: User and Read-Only Agent

Two accounts are created: one for the user, another for the AI assistant (e.g., [email protected]). The assistant's master password is a random 24-byte base64 string generated with openssl rand -base64 24 and stored in the macOS keychain:

openssl rand -base64 24   # generate random master password
security add-generic-password -a [email protected] -s vw-agent -w
# paste the password twice when prompted

The account name ( [email protected]) and service name ( vw-agent) are later used by the vw script to retrieve the password from the keychain.

Organization and Collection Permissions

Using the personal account, an organization named demo is created. Its default collection is renamed to agent. Collections act like folders with per-collection permissions. The assistant's account is invited as a "User" with access only to the agent collection and the default "View items" permission. Personal secrets (e.g., banking) stay in the user's private vault, outside the organization.

Because no email service is configured, the invitation shows "Pending confirmation." Confirming requires verifying the assistant's public-key fingerprint: run bw get fingerprint me after logging in as the assistant and match the phrase shown in the web UI.

After confirmation, the user sees all items (personal vault + organization), while the assistant sees only the two items in the agent collection. Attempts to access 个人/网银 (personal/banking) return Not found.; write attempts on demo/redis return You do not have permission to edit this item.

Wrapping the Bitwarden CLI for Exact-Match, Auto-Locking Retrieval

The official bw CLI ( npm install -g @bitwarden/cli) is used. Two problems arise when handing it directly to an assistant: bw get password searches by substring, not exact match. bw get password demo returns "Multiple items found" if multiple entries contain "demo".

After bw unlock, the session key remains valid in the environment until bw lock is called, exposing all secrets to any process that reads the variable.

A wrapper script vw solves both: it unlocks, fetches only from the designated collection, enforces exact name match via jq, and locks on exit via a trap. The master password is read from the keychain into an environment variable, never appearing in command arguments.

#!/bin/bash
# vw: fetch password from Vaultwarden by exact name. Usage: vw get <name> | vw ls
set -euo pipefail
export BITWARDENCLI_APPDATA_DIR="$HOME/.config/vw-agent"   # separate from personal bw
export NODE_EXTRA_CA_CERTS="$HOME/vaultwarden/certs/vw.crt"
export BW_NOINTERACTION=true
COLLECTION_ID="<agent collection id>"

master() { security find-generic-password -a [email protected] -s vw-agent -w; }

trap 'bw lock >/dev/null 2>&1 || true' EXIT
export BW_SESSION="$(BW_PASSWORD="$(master)" bw unlock --passwordenv BW_PASSWORD --raw)"
bw sync >/dev/null
items="$(bw list items --collectionid "$COLLECTION_ID")"

case "${1:-}" in
  ls) printf '%s' "$items" | jq -r '.[].name' | sort ;;
  get) printf '%s' "$items" | jq -er --arg n "${2:?missing item name}" '
    [.[] | select(.name == $n)]
    | if length == 1 then .[0].login.password
      else error("No entry or more than one named \($n)") end' ;;
  *) echo "Usage: vw get <name> | vw ls" >&2; exit 1 ;;
esac

The collection ID is obtained once with bw list collections and hard-coded in the script.

Usage Examples

$ vw ls
demo/openai-key
demo/redis
$ vw get demo/redis
demo-redis-0000
$ vw get 个人/网银
jq: error (at <stdin>:0): 没有或不止一条叫 个人/网银 的条目

After the command finishes, bw status shows the vault locked; the previous session key yields Vault is locked..

Performance Trade-Off

Each retrieval takes roughly 10 seconds because bw must start, unlock, and sync. This is acceptable for an assistant that fetches one or two secrets per task, but too slow for shell startup scripts.

Rules for AI Assistants

The assistant's behavior is governed by rules in its persistent instruction file ( CLAUDE.md for Claude Code, AGENTS.md for Codex):

- Passwords, tokens, API keys fetched via `vw get <project>/<service>`
- Retrieved values never written to files, command arguments, or replies
- Documentation only references entry names, not values
- Inject into environment variables for commands: `export REDISCLI_AUTH=$(vw get demo/redis)`

Entry names follow a "project/service" convention (e.g., demo/redis), so the assistant knows exactly which item to request without exposing the secret in documentation.

Backup Strategy

Vaultwarden includes a backup command that exports an encrypted SQLite dump inside the container:

docker exec <vaultwarden container> /vaultwarden backup
# Backup to 'data/db_20261109_085102.sqlite3' was successful

The data/rsa_key.pem file is also archived; losing it logs out all clients. A daily backup is saved to iCloud Drive with a two-week retention. No additional encryption is applied because item contents are already encrypted with the user's master password on the client side before reaching the server.

Limitations

This vault is only accessible on this Mac while the user is logged in and Docker is running. If the Mac is off or Docker is down, vw fails immediately. This fits a local AI assistant workflow; sharing across a team or multiple machines would require a different deployment architecture.

Vaultwarden architecture diagram
Vaultwarden architecture diagram
Invite assistant account with read-only access to agent collection
Invite assistant account with read-only access to agent collection
Pending invitation confirmation
Pending invitation confirmation
Fingerprint verification dialog
Fingerprint verification dialog
User account view showing personal vault and demo organization
User account view showing personal vault and demo organization
Assistant account view showing only agent collection items
Assistant account view showing only agent collection items
Vault locked status after vw command
Vault locked status after vw command
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CLIDockermacOSAI assistantBitwardenpassword managementCaddyVaultwarden
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.