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.
This commit is contained in:
Sam & Claude 2026-06-21 19:59:48 +02:00
parent 6f3f44db7d
commit d3361691b6

View file

@ -0,0 +1,184 @@
---
name: bitwarden-cli-vault
description: 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)
```sh
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
```sh
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
```sh
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
```sh
bw get item "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" --session "$BW_SESSION"
```
### List collections
```sh
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.
```sh
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:
```python
item = {
'type': 2,
'name': 'item-name',
'notes': '$NOTES',
'collectionIds': ['xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx']
}
```
### Get a collection ID by name
```sh
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`:
```sh
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
```sh
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)
```sh
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
```sh
# 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`