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:
parent
6f3f44db7d
commit
d3361691b6
1 changed files with 184 additions and 0 deletions
184
skills/bitwarden-cli-vault/SKILL.md
Normal file
184
skills/bitwarden-cli-vault/SKILL.md
Normal 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`
|
||||
Loading…
Add table
Reference in a new issue