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