layered-soul/skills/bitwarden-cli-vault/SKILL.md
Sam & Claude d3361691b6 feat(skills): add bitwarden-cli-vault — bw CLI read/write/update/delete
Session-based operations with no interactive prompts. Covers:
- Session setup from provider.env
- Read (list, get by name, get by ID)
- Create (base64-encoded JSON, with collection)
- Update (get → modify → pipe to edit)
- Delete
- Upsert pattern (create if absent, update if exists)
- Rebuild authorized_keys from vault items

Proven working: full round-trip of key creation → vault
publish → read back → delete on OSA 2026-06-21.
2026-06-21 19:59:48 +02:00

5.3 KiB

name description
bitwarden-cli-vault Use the Bitwarden CLI (bw) to read, write, update, and delete vault items — session-based, no interactive prompts.

Bitwarden CLI (bw) — Vault Operations

Use the bw CLI to automate vault reads and writes. Session-based auth: no interactive master password prompts after initial unlock.

Prerequisites

provider.env must contain the 3 bootstrap values:

BW_CLIENTID=user.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
BW_CLIENTSECRET=***
BW_PASSWORD=***

Session setup (once per script)

source <(grep -E '^BW_' /usr/local/etc/colibri/provider.env)
bw logout >/dev/null 2>&1
bw login --apikey >/dev/null 2>&1
BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD 2>&1 | grep 'export' | sed 's/.*BW_SESSION="\(.*\)".*/\1/')
export BW_SESSION

All subsequent commands use --session "$BW_SESSION". No password prompts.

Read

List all items

bw list items --session "$BW_SESSION" | python3.12 -c "import sys,json; [print(i['name'],i['id']) for i in json.load(sys.stdin)]"

Get one item by name

ITEM_ID=$(bw list items --search "item-name" --session "$BW_SESSION" | python3.12 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
bw get item "$ITEM_ID" --session "$BW_SESSION" | python3.12 -c "import sys,json; d=json.load(sys.stdin); print(d['notes'])"

Get one item by ID

bw get item "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" --session "$BW_SESSION"

List collections

bw list collections --session "$BW_SESSION" | python3.12 -c "import sys,json; [print(c['name'],c['id']) for c in json.load(sys.stdin)]"

Create

Create a secure note with base64-encoded JSON. Pipe it in — no interactive prompts.

NOTES="your content here"
JSON=$(python3.12 -c "
import json, base64
item = {
    'type': 2,           # 2 = secure note
    'name': 'item-name',
    'notes': '$NOTES'
}
print(base64.b64encode(json.dumps(item).encode()).decode())
")
echo "$JSON" | bw create item --session "$BW_SESSION"

Create in a specific collection

Add collectionIds to the JSON:

item = {
    'type': 2,
    'name': 'item-name',
    'notes': '$NOTES',
    'collectionIds': ['xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx']
}

Get a collection ID by name

COLLECTION_ID=$(bw list collections --session "$BW_SESSION" | python3.12 -c "
import sys,json
for c in json.load(sys.stdin):
    if c['name'] == 'hive-pubkeys':
        print(c['id'])
")

If the collection doesn't exist, create it via the Vaultwarden web UI (no CLI for org-collections without organization admin API).

Update

Get the item, modify the JSON, re-encode, pipe to bw edit:

ITEM_ID=$(bw list items --search "item-name" --session "$BW_SESSION" | python3.12 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
EXISTING=$(bw get item "$ITEM_ID" --session "$BW_SESSION")
UPDATED=$(echo "$EXISTING" | python3.12 -c "
import json, base64, sys
d = json.load(sys.stdin)
d['notes'] = 'new content'
print(base64.b64encode(json.dumps(d).encode()).decode())
")
echo "$UPDATED" | bw edit item "$ITEM_ID" --session "$BW_SESSION"

Delete

bw delete item "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" --session "$BW_SESSION"

Note: this moves to trash. Use Vaultwarden web UI for permanent deletion.

Common patterns

Upsert (create if absent, update if exists)

ITEM_NAME="$(hostname)"
NOTES="$PUBKEY"

EXISTING_ID=$(bw list items --search "$ITEM_NAME" --session "$BW_SESSION" 2>/dev/null | python3.12 -c "
import sys,json
data = json.load(sys.stdin)
for i in data:
    if i.get('name') == '$ITEM_NAME':
        print(i['id'])
        break
" 2>/dev/null)

if [ -n "$EXISTING_ID" ]; then
    # Update
    CURRENT=$(bw get item "$EXISTING_ID" --session "$BW_SESSION")
    echo "$CURRENT" | python3.12 -c "
import json, base64, sys
d = json.load(sys.stdin)
d['notes'] = '$NOTES'
print(base64.b64encode(json.dumps(d).encode()).decode())
" | bw edit item "$EXISTING_ID" --session "$BW_SESSION"
else
    # Create
    python3.12 -c "
import json, base64
print(base64.b64encode(json.dumps({
    'type': 2,
    'name': '$ITEM_NAME',
    'notes': '$NOTES'
}).encode()).decode())
" | bw create item --session "$BW_SESSION"
fi

Rebuild authorized_keys from vault items

# For mother-sync-hive-keys: pull all pubkeys, wrap with SSH restrictions
bw list items --search "colibri@" --session "$BW_SESSION" | python3.12 -c "
import json, sys
for i in json.load(sys.stdin):
    pubkey = i.get('notes','')
    hostname = i.get('name','')
    if pubkey.startswith('ssh-'):
        print(f'command=\"colibri-mcp\",restrict,no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding {pubkey} colibri@{hostname}')
" > /var/db/colibri/.ssh/authorized_keys.hive
chmod 600 /var/db/colibri/.ssh/authorized_keys.hive

Pitfalls

  • bw create/edit need base64-encoded JSON piped to stdin; --session must be a flag, not env-only
  • bw get/list/delete accept --session directly
  • bw list items --search does substring match — filter by exact .name if multiple results
  • Session expires after a few hours; re-unlock for long-running scripts
  • bw create org-collection requires organization admin (not available via personal API key) — create collections in web UI
  • Delete moves to trash; permanent delete requires web UI or bw delete item <id> --permanent