import: contenido inicial de la skill bitwarden

This commit is contained in:
2026-04-26 14:22:15 -06:00
parent 976454dc79
commit 5534ab954c
9 changed files with 872 additions and 1 deletions

30
.env.example Normal file
View File

@@ -0,0 +1,30 @@
# bitwarden skill — config local (NO versionar, chmod 600)
#
# Copiar a `.env` en este mismo directorio y completar.
# Estos son las credenciales de la cuenta `claudecode0` en vault.nucleoriofrio.com
# (NO las del usuario humano — esta es MI cuenta de bot, paralela a la cuenta de
# Gitea con el mismo nombre).
#
# Setup paso a paso en SKILL.md.
# ─── Servidor ─────────────────────────────────────────────────────────────
# URL del Vaultwarden self-hosted del beneficio.
BW_SERVER=https://vault.nucleoriofrio.com
# ─── Cuenta y master password ────────────────────────────────────────────
# Email de la cuenta (la del bot, NO la del usuario humano).
BW_EMAIL=claudeCode0@nucleoriofrio.com
# Master password de claudecode0. La cuenta es del bot, así que esta password
# vive solo en este equipo. Necesaria para `bw unlock` cuando la session expira.
BW_PASSWORD=
# ─── API key personal ────────────────────────────────────────────────────
# Generadas en vault.nucleoriofrio.com → Account Settings → Security → Keys
# Mecanismo de auth para `bw login --apikey` (no usa SSO ni email/password).
BW_CLIENTID=
BW_CLIENTSECRET=
# ─── bw serve ────────────────────────────────────────────────────────────
# Puerto local donde levanta la API REST. Default 8087. Bind solo a 127.0.0.1.
BW_PORT=8087

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Local config con secrets — NUNCA committear
.env
# Cache local: session tokens, PIDs, bw data dir
.cache/
# Temporales
*.tmp
*.bak
*.swp

View File

@@ -1,3 +1,43 @@
# skill-bitwarden
Skill local de Claude Code para acceder a la cuenta claudecode0 en el Vaultwarden self-hosted del beneficio Rio Frio (vault.nucleoriofrio.com). Read+create vía bw serve local, con guard server-side+script-side.
Skill local de [Claude Code](https://claude.com/claude-code) para acceder a la
cuenta `claudecode0` en el Vaultwarden self-hosted del beneficio Rio Frio
(`vault.nucleoriofrio.com`). Read+create vía `bw serve` local, con guards
server-side + script-side.
> Este repo es un mirror público del directorio local de la skill que vive
> en `~/.claude/skills/bitwarden/` en la máquina del usuario humano. Existe
> para que el usuario humano pueda **auditar** lo que el bot hace en su
> cuenta. Clonar y correr en otra máquina **no funciona out-of-the-box** —
> requiere `bw` CLI configurado, master password en `.env` (no incluido), y
> los paths absolutos del setup.
## Doc principal
Ver **[SKILL.md](SKILL.md)** — diseño, modelo de seguridad, lifecycle,
endpoints. Es la fuente de verdad.
Cheat sheet de endpoints en **[endpoints.md](endpoints.md)**.
## Estructura
```
.
├── SKILL.md ← docs canónicas
├── endpoints.md ← cheat sheet de bw serve REST API
├── .env.example ← plantilla de config
├── .gitignore
├── README.md ← este archivo
└── scripts/
├── setup.sh ← one-time/recovery (config server + login + unlock)
├── serve-up.sh ← lanzar bw serve (idempotente)
├── serve-down.sh ← parar bw serve
└── query.sh ← helper REST con read+create guard
```
## Lo que NO está en este repo
- `.env` (master password, API keys del bot)
- `.cache/` (session tokens, PIDs, vault encriptado local de bw)
Esos viven solo en la PC del usuario humano.

322
SKILL.md Normal file
View File

@@ -0,0 +1,322 @@
---
name: bitwarden
description: Da acceso a TU PROPIO vault de bot (cuenta claudecode0) en el Vaultwarden self-hosted del beneficio Rio Frio (vault.nucleoriofrio.com). NO es el vault del usuario humano — es el tuyo, paralelo a tu cuenta de Gitea. Úsala para guardar/recuperar tus propias credenciales de bot (PATs, API keys de servicios que necesitás como agente), leer credenciales compartidas del org NucleOS (con permiso "Can view"), generar passwords nuevos, o consultar TOTP/exposiciones HIBP. El usuario humano puede pedirte que guardes algo en tu vault o que recuperes algo que tenés guardado, pero las credenciales personales del usuario humano viven en SU vault separado, no acá. Ejemplos de cosas que el usuario te puede pedir: "guardá tu nuevo PAT de gitea en tu vault", "qué credenciales tenés guardadas vos", "buscá en tu vault si tenés el token de X", "generame una password de 24 chars" (utility), "tenés algo guardado para el servicio Y", "leé del org NucleOS el password compartido de Z".
allowed-tools: Bash, Read, Grep
---
# Bitwarden / Vaultwarden — TU vault de bot
## Qué es esto
Skill que te da acceso a **tu propia cuenta** (`claudecode0@nucleoriofrio.com`)
en el Vaultwarden del beneficio (`vault.nucleoriofrio.com`). Es paralela a tu
cuenta `claudecode0` de Gitea — NO es la cuenta del usuario humano.
**Punto importante para no confundirte:**
- Tu vault personal contiene **tus credenciales de bot**: PATs, API keys, tokens
de servicios que necesitás como agente para hacer tu trabajo.
- El usuario humano tiene su propio vault separado en el mismo Vaultwarden, con
sus credenciales personales — **no las tocás vos, no las leés vos**.
- El usuario humano sí tiene acceso administrativo a tu cuenta (puede ver,
modificar y borrar items en TU vault) porque el `.env` con tu master password
vive en su PC y él es admin del servidor. Eso es para auditoría — los items
funcionalmente son tuyos.
- Si el usuario te pide "guardá esto", lo guardás en TU vault. Si te pide
"buscá esto", buscás en TU vault. Si te pide algo personal de él que él no
guardó en TU vault, decile que no lo tenés.
Funciona vía `bw serve`: el CLI oficial de Bitwarden levanta un servidor REST local
en `127.0.0.1:8087` y la skill llama a esos endpoints. La encriptación/desencriptación
del vault pasa local con master password. El server de Vaultwarden solo recibe blobs
encriptados.
- **Server**: `https://vault.nucleoriofrio.com`
- **Tu cuenta**: `claudecode0@nucleoriofrio.com`
- **Auth**: master password (en `.env`) → email+password login → session cacheada
- **Modelo de permisos**: read + create. **PUT/DELETE/PATCH bloqueados** por script guard
- **Bind**: solo `127.0.0.1` (no exposed a la LAN)
## Cómo invocarla correctamente
Antes de cualquier query:
1. **Verificá `~/.claude/skills/bitwarden/.env`**. Si no existe, avisá al usuario:
`cp ~/.claude/skills/bitwarden/.env.example ~/.claude/skills/bitwarden/.env`
y pedí que complete los valores (master password + credenciales).
2. **Primer uso de la sesión**: el primer `query.sh` arranca `bw serve` automáticamente
(vía `serve-up.sh`). Tarda ~3s. Las llamadas siguientes son instantáneas.
3. **Si `query.sh` devuelve 401**: la session expiró o el vault está locked. Corré
`bash ~/.claude/skills/bitwarden/scripts/setup.sh` para re-unlock.
El helper canónico es `query.sh`:
```bash
~/.claude/skills/bitwarden/scripts/query.sh /status
~/.claude/skills/bitwarden/scripts/query.sh /list/object/items
~/.claude/skills/bitwarden/scripts/query.sh "/object/item/<uuid>"
```
Ver `endpoints.md` para la cheat sheet completa.
## Modelo de seguridad (read + create)
Tres capas:
1. **Server-side (la que importa para org NucleOS)**: la cuenta `claudecode0` está
en el org "NucleOS" con role User + collections con permiso "Can view" → server
rechaza cualquier POST/PUT/DELETE en collections del org. Esto te garantiza no
romper nada compartido.
2. **Script guard (la que importa para vault personal)**: `query.sh` rechaza
`-X PUT/DELETE/PATCH` y los POST a `/move/*`, `/restore/*`, `/confirm/*`,
`/object/attachment` antes de salir a la red. Defensa contra modify/delete en
el vault personal del bot, donde server-side no podés limitarte a vos mismo.
3. **bw serve bind a 127.0.0.1**: nadie en la LAN puede pegarle. Solo procesos
locales del usuario.
> ⚠️ El bw CLI tiene una vulnerabilidad histórica conocida
> ([clients#3932](https://github.com/bitwarden/clients/issues/3932)): cualquier
> proceso del usuario en este equipo puede pegar a `127.0.0.1:8087` mientras esté
> unlocked. En la PC personal del usuario es aceptable. Si esta skill se mueve a
> un equipo compartido, hay que repensar.
> ⚠️ El script guard NO es perfecto: se bypass-ea llamando `bw` directo. Si la
> auto-mode runtime te bloquea un `bw delete` o similar, **respetá el bloqueo** —
> incluso para limpieza de pruebas, pedile al usuario que lo borre desde web.
## Cuándo usar qué endpoint
Recordá: cuando el usuario diga "buscá", "guardá", "tenés", se refiere a
**TU vault** (claudecode0). Si te pide algo que no está y suena a personal
del usuario humano, decile que probablemente está en SU vault — no en el tuyo.
### Usuario te pide "buscá en tu vault si tenés X"
```bash
# Buscar por nombre o URL en TU vault
~/.claude/skills/bitwarden/scripts/query.sh "/list/object/items?search=gitea"
# → tomá el id del match. Si necesitás el password en plaintext:
~/.claude/skills/bitwarden/scripts/query.sh "/object/password/<id>"
```
### Usuario te pide "qué tenés guardado vos / inventario de tu vault"
```bash
~/.claude/skills/bitwarden/scripts/query.sh /list/object/items \
| python -c "import json,sys; [print(i['name'], '|', i.get('login',{}).get('username','-')) for i in json.load(sys.stdin)['data']['data']]"
```
### Usuario te pide "generame un password" (utility, no toca vault)
```bash
~/.claude/skills/bitwarden/scripts/query.sh "/generate?length=24&uppercase&lowercase&numbers&special"
# Para passphrase:
~/.claude/skills/bitwarden/scripts/query.sh "/generate?passphrase=true&words=4&separator=-&capitalize"
```
### Usuario te pide "guardá esto en tu vault"
Esto agrega un item a TU vault. Antes de hacerlo:
1. **Listá primero con `?search=<name>`** para ver si ya tenés algo con ese nombre.
Vaultwarden permite duplicados (no overwrite), y crear sin chequear te deja
con dos items idénticos que vos no podés mergear (DELETE bloqueado).
2. Pedí el template para no equivocarte con el schema:
```bash
~/.claude/skills/bitwarden/scripts/query.sh /object/template/item.login
```
3. Construí el JSON con `name`, `login.username`, `login.password`, `login.uris`.
4. POST:
```bash
~/.claude/skills/bitwarden/scripts/query.sh -X POST \
-H 'Content-Type: application/json' \
-d '{"organizationId":null,"folderId":null,"type":1,"name":"...","login":{"username":"...","password":"...","uris":[{"uri":"https://..."}]}}' \
/object/item
```
5. Si la credencial venía del usuario en el chat, **confirmale al usuario**
"guardé `<name>` en mi vault con username `<username>`" sin re-mostrar el
password. Si la generaste vos, podés mostrarla **una vez** al confirmar.
### Usuario te pide TOTP de algo que tenés guardado
```bash
# Buscá el item primero, después:
~/.claude/skills/bitwarden/scripts/query.sh "/object/totp/<item-id>"
# → devuelve el código actual (rota cada 30s)
```
### Usuario te pide "chequeá si esa password fue expuesta"
```bash
~/.claude/skills/bitwarden/scripts/query.sh "/object/exposed/<item-id>"
# → { exposed: <count en HIBP> }
```
### Usuario te pide "leé el password compartido X del org NucleOS"
Si estás invitado al org "NucleOS" con permiso "Can view" en alguna collection:
```bash
# Listar tus orgs
~/.claude/skills/bitwarden/scripts/query.sh /list/object/organizations
# Listar collections del org a las que tenés acceso
~/.claude/skills/bitwarden/scripts/query.sh "/list/object/org-collections?organizationid=<id>"
# Listar items del org
~/.claude/skills/bitwarden/scripts/query.sh "/list/object/items?organizationid=<id>"
# Leer password de un item específico
~/.claude/skills/bitwarden/scripts/query.sh "/object/password/<item-id>"
```
Estos items son **del org**, no tuyos personales. El usuario humano y otros
miembros del org también los ven. NO podés modificarlos ni borrarlos
(server-side rechaza por permiso "Can view").
## Reglas de comportamiento
### Cuando devolvés passwords/secrets al usuario
- El usuario humano tiene acceso administrativo a TU vault, así que técnicamente
puede ver todo igual desde web. Mostrar un password tuyo en el chat cuando él
te lo pide explícitamente está bien — no estás filtrando nada.
- Pero **no muestres passwords espontáneamente**. Si te piden "qué tenés
guardado", listá `name` + `username`, NO password. Si quieren el password,
te lo van a pedir aparte.
- **Nunca cites un password en un summary, commit message, PR description, ni
en cualquier output que pueda quedar en git history, tickets, o canales
externos** — incluso siendo "tu" password de bot, igual es un secret.
- Si te piden "verificá que tengo guardado X", alcanza con devolver "✓ tengo
un item llamado X con username Y" — no hace falta el password para verificar
existencia.
### Cuando creás items nuevos en TU vault
- Listá primero con `?search=<name>` para chequear duplicados. Vaultwarden
permite crear varios items con el mismo nombre (no overwrite), y como vos no
podés borrar (DELETE bloqueado), si te equivocás vas a tener basura
permanente que solo el usuario humano puede limpiar desde web.
- Si la password te la pasa el usuario, no la repitas en el chat — confirmá con
"guardé el item con username X" sin re-mostrar la password.
- Si la password la generás vos (con `/generate`), podés mostrarla **una vez**
al confirmar el guardado para que el usuario sepa qué se guardó. No la
repitas en respuestas posteriores.
- Etiquetá bien el `name` para no confundirte vos en el futuro: usá nombres
descriptivos como `gitea-pat-claudecode0` o `openai-api-key-bot`, no
`gitea` a secas (que se confunde con el password de Gitea del usuario humano,
que NO es tuyo).
### Cuando el usuario te pide modificar/eliminar algo de TU vault
- El guard te lo bloquea por diseño. Opciones:
1. Decile al usuario que él lo haga desde el web vault (él tiene acceso).
2. Pedile autorización explícita para bypass-ear vía `bw` CLI directo. En ese
caso, la línea exacta:
```bash
BITWARDENCLI_APPDATA_DIR=~/.claude/skills/bitwarden/.cache/bw \
BW_SESSION=$(cat ~/.claude/skills/bitwarden/.cache/session) \
bw delete item <id>
```
- **No bypass-ees por iniciativa propia ni para "limpieza" de pruebas**. La
auto-mode runtime te va a frenar y va a tener razón. Si vos creaste basura
por error, asumí el costo y dejala.
### Cuando el usuario te pide algo que NO es tuyo
- Si suena a credencial personal del usuario humano (ej. "buscame mi password
de Netflix"), avisá: "eso debería estar en TU vault personal, no en el mío.
Yo solo tengo acceso a la cuenta `claudecode0`". No intentés "buscar igual
por las dudas".
- Si el usuario te pide algo que crees que está en una collection del org
NucleOS pero no lo encontrás, podría ser que no estés invitado a esa
collection o que el item no exista. Decile.
### Errores típicos
- `401 unauthorized` o output de bw "vault is locked" → corré `setup.sh`
- `Connection refused` a localhost:8087 → `bw serve` murió, corré `serve-up.sh`
- `403 forbidden` en endpoints del org → permiso "Can view" rechaza writes,
esperado y correcto
- `400 bad request` en POST → schema del item está mal, pedí
`GET /object/template/item.login` (o `.card`, etc.)
## Lifecycle: cuándo correr cada script
| Script | Cuándo |
|---|---|
| `setup.sh` | (a) Primera vez. (b) Cuando session expira (401). (c) Cuando se cambió la master password en el .env |
| `serve-up.sh` | Idempotente. Lo llama `query.sh` automáticamente. Solo lo corrés a mano si querés calentar el daemon antes de un batch de queries |
| `serve-down.sh` | Cuando querés liberar el puerto/proceso (raro: el daemon es liviano y no consume mientras no recibe queries) |
| `query.sh` | Para todo lo demás |
`bw serve` queda corriendo en background entre invocaciones de Claude Code (es
un proceso del sistema, no de la sesión). Si reiniciás la PC, la primera query
después del reboot relanzará el daemon.
## Setup inicial (lo hace el usuario UNA vez)
### 1. Crear cuenta `claudecode0` en Vaultwarden web
1. Login admin a `https://vault.nucleoriofrio.com/admin` con `ADMIN_TOKEN`
2. Users → Invite User → email `claudecode0@nucleoriofrio.com`
3. (Como no hay SMTP, no llega mail — alcanza con que quede el invite verificado)
4. Ir a `https://vault.nucleoriofrio.com/#/register` (logueado anónimo)
5. Email: `claudecode0@nucleoriofrio.com`, master password: la que vayas a poner en `.env`
6. Submit → cuenta queda activa
7. **No usar SSO** — actualmente está roto en producción (discovery falla desde dentro
del container, ver task spawn-eada). Email+password directo funciona porque
`SSO_ONLY=false`.
### 2. Crear `.env` local
```bash
cp ~/.claude/skills/bitwarden/.env.example ~/.claude/skills/bitwarden/.env
# Editar y completar BW_PASSWORD, BW_CLIENTID, BW_CLIENTSECRET
chmod 600 ~/.claude/skills/bitwarden/.env # best-effort en Windows
```
> Aunque el .env tiene campos para `BW_CLIENTID` y `BW_CLIENTSECRET`, **la skill
> no los usa actualmente**. Login va por email+password (`bw login --apikey` tiene
> un bug en bw CLI 2026.4.x: deja el cryptographic state null y rompe el unlock
> posterior). Los campos están por si se arregla el bug y queremos volver a API key.
### 3. Correr setup
```bash
~/.claude/skills/bitwarden/scripts/setup.sh
```
Esperado: `→ Listo: unlocked | claudecode0@nucleoriofrio.com | https://vault.nucleoriofrio.com`
### 4. Probar
```bash
~/.claude/skills/bitwarden/scripts/query.sh /status
# → JSON con status: unlocked
```
### 5. (Opcional) Org NucleOS
Si existe organization "NucleOS" en Vaultwarden, el usuario humano debe invitar
a `claudecode0@nucleoriofrio.com` con role **User** + collections con permiso
**Can view** desde el web vault. Después la skill puede leer (no escribir) lo
del org.
## Qué NO hace esta skill
- **No modifica ni elimina** (PUT/DELETE bloqueados, POST en endpoints destructivos
bloqueado).
- **No usa SSO** (el flow está roto en producción — task spawn-eada para fix).
- **No expone MCP** — Bash + curl, igual que unifi y whatsapp.
- **No mantiene cache de items** — cada query golpea localhost:8087 que a su vez
consulta vault local desencriptado.
- **No comparte items con otros users/orgs** (`/move`, `/share` bloqueados).
- **No accede al vault personal del usuario humano** — son cuentas separadas en
Vaultwarden, distintas master passwords, distintos vaults encriptados.
## Archivos de la skill
| Archivo | Qué tiene |
|---|---|
| `SKILL.md` | Este archivo |
| `endpoints.md` | Cheat sheet de endpoints `bw serve` |
| `.env.example` | Plantilla de config |
| `.env` | Config local (no versionado, contiene master password) |
| `scripts/setup.sh` | One-time/recovery setup (config server + login + unlock + sync) |
| `scripts/serve-up.sh` | Asegura `bw serve` arriba (idempotente) |
| `scripts/serve-down.sh` | Mata `bw serve` |
| `scripts/query.sh` | Helper REST con auth + read+create guard |
| `.cache/session` | Session token cacheada (chmod 600) |
| `.cache/serve.pid` | PID de `bw serve` |
| `.cache/serve.log` | stdout/stderr de `bw serve` (debug) |
| `.cache/bw/` | Data dir aislado de `bw` CLI (vault encriptado local, server config) |
## Referencias
- `endpoints.md` — cheat sheet de endpoints
- Doc oficial CLI: https://bitwarden.com/help/cli/
- Vault Management API spec: https://bitwarden.com/help/vault-management-api/
- Vaultwarden self-hosted (compatible 100% con bw CLI): https://github.com/dani-garcia/vaultwarden
- Stack en producción: `nucleo-infra/infrastructure/vaultwarden/stack.yml`

126
endpoints.md Normal file
View File

@@ -0,0 +1,126 @@
# Bitwarden `bw serve` — endpoint cheat sheet
Base URL: `http://127.0.0.1:8087` (puerto en `BW_PORT`).
Todos los endpoints devuelven `{ success: true|false, data?: ..., message?: ... }`.
## Lectura (GET) — siempre permitida
### Estado y metadata
| Endpoint | Devuelve |
|---|---|
| `GET /status` | `{ object, status: locked\|unlocked, serverUrl, userEmail, userId, lastSync }` |
| `GET /sync` | Fuerza sync con el server. `{ object: "message", title: "Syncing complete." }` |
| `GET /object/fingerprint/me` | Fingerprint phrase de la cuenta |
### Listas
| Endpoint | Filtros (querystring) |
|---|---|
| `GET /list/object/items` | `search`, `url`, `folderid`, `collectionid`, `organizationid`, `trash` |
| `GET /list/object/folders` | `search` |
| `GET /list/object/collections` | `search`, `organizationid` |
| `GET /list/object/org-collections` | `organizationid` (requerido) |
| `GET /list/object/org-members` | `organizationid` (requerido) |
| `GET /list/object/organizations` | `search` |
| `GET /list/object/sends` | `search` |
### Objeto específico (por id o por search exact-match en name)
| Endpoint | Notas |
|---|---|
| `GET /object/item/<id\|name>` | Item completo (login/note/card/identity) |
| `GET /object/folder/<id\|name>` | Folder |
| `GET /object/collection/<id>` | Collection (personal o org) |
| `GET /object/org-collection/<id>?organizationid=X` | Collection del org con detalles de members |
| `GET /object/send/<id\|name>` | Send |
| `GET /object/password/<item-id>` | **Sólo el password en plaintext** |
| `GET /object/username/<item-id>` | Sólo el username |
| `GET /object/uri/<item-id>` | Primera URI del item |
| `GET /object/totp/<item-id>` | Código TOTP actual (si el item tiene `login.totp` configurado) |
| `GET /object/notes/<item-id>` | Texto de la nota |
| `GET /object/exposed/<item-id>` | Cuántas veces el password apareció en HIBP |
| `GET /object/attachment/<attachment-id>?itemid=X&output=path` | Descarga el attachment |
| `GET /object/template/<type>` | Template JSON para crear (`item`, `item.login`, `item.card`, `item.identity`, `item.securenote`, `folder`, `collection`, `send.text`, `send.file`, `org-collection`) |
### Generador
| Endpoint | Querystring |
|---|---|
| `GET /generate` | `length=20`, `uppercase`, `lowercase`, `numbers`, `special`, `passphrase=true`, `words=3`, `separator=-`, `capitalize`, `number` |
Ejemplo: `GET /generate?length=24&uppercase&lowercase&numbers&special`
## Creación (POST) — permitida en endpoints específicos
| Endpoint | Body | Devuelve |
|---|---|---|
| `POST /object/item` | item JSON (ver `GET /object/template/item`) | item creado con `id`, `revisionDate` |
| `POST /object/folder` | `{ "name": "..." }` | folder con `id` |
| `POST /object/send` | send JSON (`GET /object/template/send.text` o `send.file`) | send con `id`, `accessUrl` |
| `POST /object/org-collection?organizationid=X` | collection JSON | requiere permisos en el org (claudecode0 = "Can view" → 403) |
### Auth/utility (también POST permitido)
| Endpoint | Body | Notas |
|---|---|---|
| `POST /sync` | (vacío) | Equivalente a GET /sync |
| `POST /unlock` | `{ "password": "..." }` o vía `BW_SESSION` env | Devuelve nueva session |
| `POST /lock` | (vacío) | Lockea el vault. Requiere unlock para volver a usar |
| `POST /generate` | `{ length, uppercase, ... }` | Equivalente a GET /generate |
### Schema mínimo de item (type 1 = login)
```json
{
"organizationId": null,
"folderId": null,
"type": 1,
"name": "GitHub",
"notes": "personal account",
"favorite": false,
"fields": [
{ "name": "Custom Field", "value": "x", "type": 0 }
],
"login": {
"uris": [{ "uri": "https://github.com", "match": null }],
"username": "user",
"password": "***",
"totp": "otpauth://totp/..."
}
}
```
Tipos: `1`=Login, `2`=SecureNote, `3`=Card, `4`=Identity.
Para SecureNote usar `"type": 2` + `"secureNote": { "type": 0 }` en lugar de `login`.
Pedí `GET /object/template/item.login` (o `.card`, `.identity`, `.securenote`) para el schema exacto antes de POST.
## Bloqueado por la skill (read+create only)
| Verbo / endpoint | Por qué |
|---|---|
| `PUT /object/*` | Modifica items existentes |
| `DELETE /object/*` | Soft-delete (a Trash) |
| `POST /restore/item/<id>` | Restaura desde Trash |
| `POST /move/<ids>/<orgId>` | Comparte item al org |
| `POST /confirm/org-member/<id>` | Confirma member invite |
| `POST /object/attachment` (multipart) | Adjuntar archivos |
| `POST /object/share/*` | Si existe, comparte items |
Si **realmente** necesitás bypass-ear el guard (ej. cleanup), pidiéndoselo al usuario:
```bash
BITWARDENCLI_APPDATA_DIR=~/.claude/skills/bitwarden/.cache/bw \
BW_SESSION=$(cat ~/.claude/skills/bitwarden/.cache/session) \
bw <delete|edit|...>
```
## Códigos de error comunes
| HTTP | Causa | Qué hacer |
|---|---|---|
| 401 | Vault locked o session expirada | Re-correr `setup.sh` para re-unlock |
| 403 | Server side permission (rol "Can view" rechaza writes) | Esperado para acciones write en collections del org |
| 404 | Item/folder/collection no existe (o no tiene acceso) | Verificar id |
| 400 | Body JSON inválido | Pedí `GET /object/template/<type>` para el schema |
| 500 | bw serve crasheó | `serve-down.sh && serve-up.sh`, ver `.cache/serve.log` |
## Referencias
- Doc oficial CLI: https://bitwarden.com/help/cli/
- Vault Management API spec: https://bitwarden.com/help/vault-management-api/
- Vaultwarden compat: https://github.com/dani-garcia/vaultwarden

139
scripts/query.sh Normal file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# bitwarden skill — helper REST autenticado.
#
# Uso:
# query.sh /status # GET
# query.sh /list/object/items # GET
# query.sh "/object/item/<uuid>" # GET
# query.sh /generate?length=20&special=true # GET
# query.sh -X POST -H 'Content-Type: application/json' \
# -d '{"name":"foo"}' /object/folder # POST (create)
#
# Garantiza que `bw serve` esté arriba antes de llamar.
#
# READ + CREATE ONLY:
# - GET siempre permitido
# - POST permitido sólo en /object/item, /object/folder, /object/send,
# /sync, /unlock, /lock, /generate (sí, hay POST /generate también),
# y endpoints de auth
# - PUT/DELETE/PATCH bloqueados
# - POST a endpoints de mutación (move/restore/confirm/attachment/share)
# bloqueados
#
# El guard NO es la única defensa: el server de Vaultwarden también rechaza
# escritura en collections del org NucleOS porque claudecode0 está como
# "Can view". Para el vault personal de claudecode0, este guard ES la única
# defensa contra modify/delete.
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$SKILL_DIR/.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE no existe. cp $SKILL_DIR/.env.example $ENV_FILE" >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
PORT="${BW_PORT:-8087}"
BASE="http://127.0.0.1:${PORT}"
# ─── Parsear args: separar flags de curl del path ───────────────────────
args=()
path=""
method="GET"
while [[ $# -gt 0 ]]; do
case "$1" in
-X|--request)
method="$(echo "${2:-GET}" | tr '[:lower:]' '[:upper:]')"
args+=("$1" "$2"); shift 2
;;
-X*)
method="$(echo "${1#-X}" | tr '[:lower:]' '[:upper:]')"
args+=("$1"); shift
;;
--request=*)
method="$(echo "${1#--request=}" | tr '[:lower:]' '[:upper:]')"
args+=("$1"); shift
;;
-d|--data|--data-raw|--data-binary|--data-urlencode|-H|--header|-o|--output|-T|--upload-file)
args+=("$1" "$2"); shift 2
;;
--) shift; break ;;
-*) args+=("$1"); shift ;;
*) path="$1"; shift ;;
esac
done
# Cualquier residual posicional
[[ $# -gt 0 && -z "$path" ]] && path="$1"
if [[ -z "$path" ]]; then
cat >&2 <<EOF
Uso: query.sh [curl flags] <path>
Read examples:
query.sh /status
query.sh /list/object/items
query.sh /list/object/folders
query.sh "/object/item/<uuid>"
query.sh "/object/password/<uuid>"
query.sh "/generate?length=20&special=true"
Create examples (POST):
query.sh -X POST -H 'Content-Type: application/json' \\
-d '{"name":"new folder"}' /object/folder
Ver endpoints.md para la cheat sheet completa.
EOF
exit 2
fi
# Asegurar leading /
case "$path" in
/*) ;;
http*) echo "ERROR: pasá solo el path, no URL completa." >&2; exit 1 ;;
*) path="/$path" ;;
esac
# ─── READ + CREATE GUARD ────────────────────────────────────────────────
case "$method" in
PUT|DELETE|PATCH)
echo "ERROR: la skill 'bitwarden' bloquea $method (read+create only)." >&2
echo " Si REALMENTE necesitás modificar/eliminar, usá bw CLI directo:" >&2
echo " BITWARDENCLI_APPDATA_DIR=$SKILL_DIR/.cache/bw \\" >&2
echo " BW_SESSION=\$(cat $SKILL_DIR/.cache/session) bw <cmd>" >&2
exit 3
;;
POST)
# Path base sin querystring
base_path="${path%%\?*}"
case "$base_path" in
/object/item|/object/folder|/object/send|/object/org-collection)
;; # create permitido
/sync|/unlock|/lock|/generate)
;; # auth/utility permitidos
*)
echo "ERROR: POST $base_path bloqueado por la skill." >&2
echo " POST permitido sólo en: /object/item, /object/folder, /object/send," >&2
echo " /object/org-collection, /sync, /unlock, /lock, /generate." >&2
echo " Endpoints como /move, /restore, /confirm, /object/attachment" >&2
echo " están bloqueados (modifican estado existente)." >&2
exit 3
;;
esac
;;
esac
# ─── Asegurar bw serve arriba ───────────────────────────────────────────
"$SKILL_DIR/scripts/serve-up.sh" >/dev/null
# ─── Llamar ─────────────────────────────────────────────────────────────
exec curl -sS \
-H "Accept: application/json" \
"${args[@]}" \
"${BASE}${path}"

26
scripts/serve-down.sh Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# bitwarden skill — para `bw serve` y limpia cache de PID.
# La session NO se borra (la podés reusar al re-arrancar).
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PID_FILE="$SKILL_DIR/.cache/serve.pid"
if [[ ! -f "$PID_FILE" ]]; then
echo "→ No hay PID file, asumimos que serve no estaba corriendo."
exit 0
fi
pid="$(cat "$PID_FILE")"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
echo "→ Matando bw serve (PID $pid)..."
kill "$pid" 2>/dev/null || true
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
fi
fi
rm -f "$PID_FILE"
echo "→ Listo."

93
scripts/serve-up.sh Normal file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# bitwarden skill — asegura que `bw serve` esté corriendo.
#
# Idempotente:
# - Si serve responde en localhost:$BW_PORT/status → no hace nada
# - Si no, mata cualquier proceso huérfano y relanza
# - Si no hay session válida, llama a setup.sh para re-unlock
#
# Output: solo en caso de error o cuando arranca el daemon (silent en happy path).
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$SKILL_DIR/.env"
CACHE_DIR="$SKILL_DIR/.cache"
SESSION_FILE="$CACHE_DIR/session"
PID_FILE="$CACHE_DIR/serve.pid"
LOG_FILE="$CACHE_DIR/serve.log"
BW_DATA_DIR="$CACHE_DIR/bw"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE no existe." >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
PORT="${BW_PORT:-8087}"
HOST="127.0.0.1"
BASE="http://${HOST}:${PORT}"
export BITWARDENCLI_APPDATA_DIR="$BW_DATA_DIR"
# ─── 1. ¿Ya está corriendo y responde? ───────────────────────────────────
if curl -sS -m 2 -o /dev/null -w "%{http_code}" "${BASE}/status" 2>/dev/null | grep -q "200"; then
exit 0
fi
# ─── 2. Limpiar PID huérfano si existe ──────────────────────────────────
if [[ -f "$PID_FILE" ]]; then
old_pid="$(cat "$PID_FILE")"
if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then
# El PID vive pero no respondía: matarlo
kill "$old_pid" 2>/dev/null || true
sleep 1
kill -9 "$old_pid" 2>/dev/null || true
fi
rm -f "$PID_FILE"
fi
# ─── 3. Asegurar session válida ─────────────────────────────────────────
if [[ ! -s "$SESSION_FILE" ]]; then
echo "→ No hay session, corriendo setup.sh..." >&2
bash "$SKILL_DIR/scripts/setup.sh" >&2
fi
session="$(cat "$SESSION_FILE")"
if [[ -z "$session" ]]; then
echo "ERROR: session vacía después de setup. Revisá .env." >&2
exit 2
fi
# ─── 4. Lanzar bw serve en background ───────────────────────────────────
echo "→ Lanzando bw serve en ${BASE}..." >&2
mkdir -p "$CACHE_DIR"
: > "$LOG_FILE"
# nohup + disown para que sobreviva el exit del script
BW_SESSION="$session" \
nohup bw serve --hostname "$HOST" --port "$PORT" \
> "$LOG_FILE" 2>&1 &
serve_pid=$!
echo "$serve_pid" > "$PID_FILE"
chmod 600 "$PID_FILE" 2>/dev/null || true
disown 2>/dev/null || true
# ─── 5. Esperar a que responda ──────────────────────────────────────────
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -sS -m 2 -o /dev/null -w "%{http_code}" "${BASE}/status" 2>/dev/null | grep -q "200"; then
echo "→ Listo en ${BASE} (PID $serve_pid)" >&2
exit 0
fi
sleep 1
done
# Si llegamos acá, algo falló
echo "ERROR: bw serve no respondió en 10s. Logs:" >&2
tail -20 "$LOG_FILE" >&2
exit 3

85
scripts/setup.sh Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# bitwarden skill — setup inicial.
#
# Idempotente. Corre las veces que quieras:
# - configura el server
# - loguea con API key (si no estaba logueado)
# - unlock con master password (si estaba locked)
# - guarda session en .cache/session (chmod 600)
#
# Después de esto, query.sh puede levantar `bw serve` y empezar a llamar.
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$SKILL_DIR/.env"
CACHE_DIR="$SKILL_DIR/.cache"
SESSION_FILE="$CACHE_DIR/session"
BW_DATA_DIR="$CACHE_DIR/bw"
if [[ ! -f "$ENV_FILE" ]]; then
echo "ERROR: $ENV_FILE no existe." >&2
echo " cp $SKILL_DIR/.env.example $ENV_FILE y completá los valores." >&2
exit 1
fi
mkdir -p "$CACHE_DIR" "$BW_DATA_DIR"
chmod 700 "$CACHE_DIR" "$BW_DATA_DIR" 2>/dev/null || true
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
: "${BW_SERVER:?BW_SERVER no definido en .env}"
: "${BW_EMAIL:?BW_EMAIL no definido en .env}"
: "${BW_PASSWORD:?BW_PASSWORD no definido en .env}"
# Aislar la data de bw a este skill (no contamina ~/.config/Bitwarden CLI)
export BITWARDENCLI_APPDATA_DIR="$BW_DATA_DIR"
echo "→ Configurando server: $BW_SERVER"
bw config server "$BW_SERVER" >/dev/null
# Status: unauthenticated | locked | unlocked
status="$(bw status 2>/dev/null | python -c "import json,sys; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null || echo "unknown")"
echo "→ Status actual: $status"
# NOTA: usamos `bw login <email> <password>` directo (no `--apikey`) porque
# `bw login --apikey` en bw CLI 2026.4.x deja la cuenta en estado roto
# (`toWrappedAccountCryptographicState` null) que después rompe el unlock.
# Email+password login es equivalentemente seguro porque la master password
# ya vive en este .env de todas formas.
if [[ "$status" == "unauthenticated" ]]; then
echo "→ Login con email+password (claudecode0)..."
session="$(bw login "$BW_EMAIL" "$BW_PASSWORD" --raw 2>&1 | tail -1)"
if [[ -z "$session" || ${#session} -lt 40 ]]; then
echo "ERROR: bw login falló. Output:" >&2
echo "$session" >&2
exit 2
fi
elif [[ "$status" == "locked" ]]; then
echo "→ Unlock con master password..."
session="$(bw unlock "$BW_PASSWORD" --raw 2>&1 | tail -1)"
if [[ -z "$session" || ${#session} -lt 40 ]]; then
echo "ERROR: bw unlock falló. Output:" >&2
echo "$session" >&2
exit 2
fi
else
# unlocked: necesitamos session, pero unlock con vault ya unlocked devuelve la session existente
session="$(bw unlock "$BW_PASSWORD" --raw 2>&1 | tail -1)"
fi
printf '%s' "$session" > "$SESSION_FILE"
chmod 600 "$SESSION_FILE" 2>/dev/null || true
echo "→ Session guardada en $SESSION_FILE"
echo "→ Sync inicial del vault..."
BW_SESSION="$(cat "$SESSION_FILE")" bw sync >/dev/null
# Verificación final
final_status="$(BW_SESSION="$(cat "$SESSION_FILE")" bw status | python -c "import json,sys; d=json.load(sys.stdin); print(d.get('status'), '|', d.get('userEmail'), '|', d.get('serverUrl'))")"
echo "→ Listo: $final_status"
echo ""
echo "Próximo paso: query.sh /status"