Skill: clawbow/bitwarden-secrets (version 1.0.0)

--- name: bitwarden-secrets description: Safely access Bitwarden or Vaultwarden secrets via the bw CLI with redacted outputs by default. Use for vault sync, item search, metadata retrieval, and (only with explicit confirmation) revealing a single secret field. --- # Bitwarden Secrets (safe-by-default) Use this skill to interact with Bitwarden/Vaultwarden through `bw` while minimizing accidental secret leakage. ## Prerequisites - `bw` CLI installed. - Server configured (`bw config server <url>`). - Logged in (`bw login --apikey`) and unlocked (`bw unlock`) so `BW_SESSION` is available. ## Commands ```bash cd skills/bitwarden-secrets python3 scripts/vw_cli.py status python3 scripts/vw_cli.py sync python3 scripts/vw_cli.py search --query google python3 scripts/vw_cli.py get --id <item_id> ``` ### Bootstrap helper (optional) Load runtime vars + refresh session + sync in one step: ```bash source scripts/vw_bootstrap.sh # or after shell reload: vw-openclaw-ready ``` ### Reveal (explicit only) By default, do not reveal secret values in chat. Use reveal only with explicit user consent and temporary policy override: ```bash export VW_REVEAL_ALLOW=1 python3 scripts/vw_cli.py reveal --id <item_id> --field password --confirm YES_REVEAL unset VW_REVEAL_ALLOW ``` ## Safety policy - Default outputs are redacted/metadata-only. - No bulk secret dumps. - Prefer IDs + labels and retrieve only the minimum needed field. - Reveal requires BOTH `VW_REVEAL_ALLOW=1` and `--confirm YES_REVEAL`.

path: scripts/vw_bootstrap.sh
content: #!/usr/bin/env bash
# Source this script to bootstrap Vaultwarden env for OpenClaw safely.
# Usage:
#   source /root/.openclaw/workspace/skills/vaultwarden-secrets/scripts/vw_bootstrap.sh

_vw_fail() {
  echo "[vw_bootstrap] ERROR: $*" >&2
  return 1 2>/dev/null || exit 1
}

# Warn when executed (not sourced)
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  echo "This script must be sourced to persist exports."
  echo "Use: source ${BASH_SOURCE[0]}"
  exit 1
fi

command -v bw >/dev/null 2>&1 || _vw_fail "bw CLI not found"

STATUS_JSON="$(bw status 2>/dev/null || true)"
STATUS="$(printf '%s' "$STATUS_JSON" | python3 -c 'import sys,json; 
try:
 o=json.load(sys.stdin); print(o.get("status",""))
except Exception:
 print("")')"

if [[ "$STATUS" == "unauthenticated" ]]; then
  _vw_fail "bw is unauthenticated. Run: bw login --apikey"
fi

# If session missing, unlock from BW_PASSWORD if present
if [[ -z "${BW_SESSION:-}" ]]; then
  if [[ -z "${BW_PASSWORD:-}" ]]; then
    _vw_fail "BW_SESSION missing and BW_PASSWORD not set. Run: bw unlock (or set BW_PASSWORD)"
  fi
  export BW_SESSION="$(bw unlock --passwordenv BW_PASSWORD --raw)"
fi

# Pull runtime secrets from Vaultwarden items
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXPORTED="$("$SCRIPT_DIR"/vw_env_export.sh)" || _vw_fail "vw_env_export.sh failed"
# shellcheck disable=SC1090
source /dev/stdin <<< "$EXPORTED"

# Refresh session now that BW_PASSWORD is guaranteed from vault item
export BW_SESSION="$(bw unlock --passwordenv BW_PASSWORD --raw)"

# Optional quick sync check
python3 "$SCRIPT_DIR"/vw_cli.py sync >/dev/null || _vw_fail "sync failed"

echo "[vw_bootstrap] READY (BW_SESSION + BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD loaded)"


path: scripts/vw_cli.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import subprocess
import sys
from typing import Any, Dict


def run_bw(args):
    p = subprocess.run(["bw", *args], capture_output=True, text=True)
    if p.returncode != 0:
        raise RuntimeError((p.stderr or p.stdout).strip())
    return p.stdout.strip()


def parse_json(s: str) -> Any:
    try:
        return json.loads(s)
    except Exception:
        return s


def ensure_session():
    if not os.getenv("BW_SESSION"):
        raise RuntimeError("BW_SESSION missing. Run: bw unlock, then export BW_SESSION.")


def safe_item_view(item: Dict[str, Any]) -> Dict[str, Any]:
    login = item.get("login") or {}
    uris = [u.get("uri") for u in (login.get("uris") or []) if isinstance(u, dict)]
    [REDACTED_SECRET]"password")
    return {
        "id": item.get("id"),
        "name": item.get("name"),
        "username": login.get("username"),
        "uris": uris,
        "has_password": password is not None,
        "password_length": len(password) if isinstance(password, str) else None,
        "revisionDate": item.get("revisionDate"),
    }


def cmd_status():
    out = run_bw(["status"])
    print(out)


def cmd_sync():
    ensure_session()
    out = run_bw(["sync"])
    print(json.dumps({"success": True, "message": out}, ensure_ascii=False, indent=2))


def cmd_search(query: str):
    ensure_session()
    out = run_bw(["list", "items", "--search", query])
    items = parse_json(out)
    if not isinstance(items, list):
        raise RuntimeError(f"Unexpected response: {items}")
    print(json.dumps({"count": len(items), "items": [safe_item_view(i) for i in items]}, ensure_ascii=False, indent=2))


def cmd_get(item_id: str):
    ensure_session()
    out = run_bw(["get", "item", item_id])
    item = parse_json(out)
    if not isinstance(item, dict):
        raise RuntimeError(f"Unexpected response: {item}")
    print(json.dumps(safe_item_view(item), ensure_ascii=False, indent=2))


def cmd_reveal(item_id: str, field: str, confirm: str):
    ensure_session()
    if os.getenv("VW_REVEAL_ALLOW", "0") != "1":
        raise RuntimeError("Reveal blocked by policy. Set VW_REVEAL_ALLOW=1 for this shell, then retry with explicit confirm.")
    if confirm != "YES_REVEAL":
        raise RuntimeError("Reveal blocked. Pass --confirm YES_REVEAL for explicit consent.")

    out = run_bw(["get", "item", item_id])
    item = parse_json(out)
    if not isinstance(item, dict):
        raise RuntimeError(f"Unexpected response: {item}")

    login = item.get("login") or {}
    if field == "password":
        value = login.get("password")
    elif field == "username":
        value = login.get("username")
    elif field == "totp":
        value = login.get("totp")
    else:
        raise RuntimeError("Unsupported field. Use: password|username|totp")

    print(json.dumps({"id": item_id, "field": field, "value": value}, ensure_ascii=False, indent=2))


def main():
    p = argparse.ArgumentParser(description="Vaultwarden safe CLI wrapper")
    sub = p.add_subparsers(dest="cmd", required=True)

    sub.add_parser("status")
    sub.add_parser("sync")

    s = sub.add_parser("search")
    s.add_argument("--query", required=True)

    g = sub.add_parser("get")
    g.add_argument("--id", required=True)

    r = sub.add_parser("reveal")
    r.add_argument("--id", required=True)
    r.add_argument("--field", required=True)
    r.add_argument("--confirm", required=True)

    a = p.parse_args()
    try:
        if a.cmd == "status":
            cmd_status()
        elif a.cmd == "sync":
            cmd_sync()
        elif a.cmd == "search":
            cmd_search(a.query)
        elif a.cmd == "get":
            cmd_get(a.id)
        elif a.cmd == "reveal":
            cmd_reveal(a.id, a.field, a.confirm)
    except Exception as e:
        print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, indent=2))
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/vw_env_export.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Export runtime env vars from Vaultwarden item names (secure local use)
# Requires: BW_SESSION set (bw unlock)

ITEM_CLIENT_ID="${VW_ITEM_CLIENT_ID:-oc-bw-clientid}"
ITEM_CLIENT_SECRET="${VW_ITEM_CLIENT_SECRET:-oc-bw-clientsecret}"
ITEM_PASSWORD="${VW_ITEM_PASSWORD:-oc-bw-password}"

if ! command -v bw >/dev/null 2>&1; then
  echo "ERROR: bw CLI not found" >&2
  exit 1
fi

if [ -z "${BW_SESSION:-}" ]; then
  echo "ERROR: BW_SESSION missing. Run 'bw unlock' then export BW_SESSION." >&2
  exit 1
fi

extract_password_by_name() {
  local name="$1"
  local json id value
  json="$(bw list items --search "$name")"
  id="$(printf '%s' "$json" | python3 -c 'import sys,json; a=json.load(sys.stdin); print(a[0]["id"] if a else "")')"
  if [ -z "$id" ]; then
    echo ""
    return
  fi
  value="$(bw get item "$id" | python3 -c 'import sys,json; o=json.load(sys.stdin); print((o.get("login") or {}).get("password") or "")')"
  printf '%s' "$value"
}

CID="$(extract_password_by_name "$ITEM_CLIENT_ID")"
CSEC="$(extract_password_by_name "$ITEM_CLIENT_SECRET")"
CPW="$(extract_password_by_name "$ITEM_PASSWORD")"

if [ -z "$CID" ] || [ -z "$CSEC" ] || [ -z "$CPW" ]; then
  echo "ERROR: One or more required items are missing/empty in Vaultwarden." >&2
  echo "Expected item names: $ITEM_CLIENT_ID, $ITEM_CLIENT_SECRET, $ITEM_PASSWORD" >&2
  exit 1
fi

cat <<EOF
export BW_CLIENTID='$CID'
export BW_CLIENTSECRET='$CSEC'
export BW_PASSWORD='$CPW'
EOF


path: setup-checklist.md
content: # Bitwarden/Vaultwarden setup checklist (OpenClaw)

## Done automatically
- `bw` CLI installed
- server configured
- safe wrapper skill created (`vw_cli.py`)

## Still required from Maxime (once)
1. Create these Vaultwarden items (Login type, password field used):
   - `oc-bw-clientid` -> value = BW_CLIENTID
   - `oc-bw-clientsecret` -> value = BW_CLIENTSECRET
   - `oc-bw-password` -> value = account master password
2. In shell (root):
   - `bw login --apikey`
   - `bw unlock` then export `BW_SESSION`
3. Test helper:
   - `cd skills/bitwarden-secrets`
   - `./scripts/vw_env_export.sh > /tmp/vw_exports.sh`
   - `source /tmp/vw_exports.sh`
4. Validate:
   - `python3 scripts/vw_cli.py sync`
   - `python3 scripts/vw_cli.py search --query google`
5. Lock when done:
   - `bw lock`

## Notes
- Never paste secrets/BW_SESSION into Discord.
- Reveal mode exists but requires explicit confirmation token.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)