12 KiB
ClawVault — SPEC v0.1
A personal vault built for humans who have AI assistants.
The Problem
Existing password managers are built for human→human sharing. The AI assistant use case is different:
- Your AI needs ~30 of your 500 credentials to do its job
- Some credentials should never reach an AI regardless of trust
- Card numbers, CVV, passport data — client-side decrypt only
- Everything else — AI can read, use, act on
Bitwarden MCP server exists but is all-or-nothing. Nothing has two-tier encryption with field-level AI visibility, WebAuthn unlock, and LLM-powered field mapping.
Design Principles
- One binary — Go, cross-compiles mac/linux/windows, single port
- One file — SQLite DB, platform-independent, portable
- Entry model — same tree as inou/dealspace: everything is an entry
- Two-tier encryption — L1 (server key, AI-readable) + L2 (client-side WebAuthn only)
- LLM import — any format, no format-specific parsers
- LLM field mapping — extension fills forms intelligently
- No external dependencies — no cloud, no subscriptions
Architecture
clawvault/
├── cmd/clawvault/main.go # single entrypoint
├── api/ # REST + MCP handlers
├── lib/ # crypto, dbcore, types
├── web/ # embedded SPA (go:embed, vanilla JS)
└── extension/ # Chrome extension (no build step)
Single binary, one port (default 8765):
GET /→ embedded web UI/api/*→ REST API/mcp→ MCP endpoint (AI, L1 only)/ext/*→ extension API (full access)
Data Model
Entry table (single table for everything)
type Entry struct {
EntryID string // uuid
ParentID string // folder entry_id, or "" for root
Type string // credential|note|identity|card|ssh_key|totp|folder|any
Title string // plaintext title
TitleIdx string // HMAC-SHA256 blind index for search
Data []byte // zstd + AES-256-GCM (L1 key server-side)
// OR: zstd + AES-256-GCM (L2 key client-side only)
DataLevel int // 1=L1, 2=L2
CreatedAt int64
UpdatedAt int64
Version int // optimistic locking
}
VaultData (packed into Entry.Data)
type VaultData struct {
Title string `json:"title"`
Type string `json:"type"`
Fields []VaultField `json:"fields"`
URLs []string `json:"urls,omitempty"`
Tags []string `json:"tags,omitempty"`
Expires string `json:"expires,omitempty"` // YYYY-MM-DD
Notes string `json:"notes,omitempty"`
Files []VaultFile `json:"files,omitempty"`
}
type VaultField struct {
Label string `json:"label"` // "Username", "Password", "CVV" — anything
Value string `json:"value"` // plaintext after decrypt
Kind string `json:"kind"` // text|password|totp|url|file
Section string `json:"section,omitempty"` // visual grouping
L2 bool `json:"l2,omitempty"` // true = client-side decrypt only
}
type VaultFile struct {
Name string `json:"name"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Data []byte `json:"data"` // encrypted blob stored in SQLite
}
The type field is just a UI hint — it never constrains the fields.
A "shoe size" entry is valid. A "custom API token" entry is valid.
Example entries
Credit card:
{
"type": "card",
"title": "Amex Platinum",
"fields": [
{"label":"Cardholder","value":"Johan Jongsma","kind":"text"},
{"label":"Number","value":"3782 8224 6310 005","kind":"password","l2":true},
{"label":"CVV","value":"1234","kind":"password","l2":true},
{"label":"Expiry","value":"09/28","kind":"text"},
{"label":"Bank","value":"American Express","kind":"text"}
]
}
AI sees: Cardholder, Expiry, Bank. Never Number or CVV.
Identity:
{
"type": "identity",
"title": "Johan Jongsma",
"fields": [
{"label":"First Name","value":"Johan","section":"Personal"},
{"label":"Last Name","value":"Jongsma","section":"Personal"},
{"label":"Email","value":"johan@jongsma.me","section":"Personal"},
{"label":"Phone","value":"+17272252475","section":"Personal"},
{"label":"Address","value":"851 Brightwaters Blvd","section":"Address"},
{"label":"City","value":"St. Petersburg","section":"Address"},
{"label":"State","value":"FL","section":"Address"},
{"label":"ZIP","value":"33704","section":"Address"},
{"label":"Passport","value":"NL12345678","kind":"password","l2":true,"section":"Documents"}
]
}
Two-Tier Encryption
L1 — Server Key (AI-readable)
VAULT_KEYenv var (hex-encoded 32 bytes)- Per-entry key: HKDF-SHA256(VAULT_KEY, entry_id)
- Encryption: zstd compress → AES-256-GCM encrypt → store
- AI (MCP) can read all L1 fields
- L2 fields within an L1 entry: individual field values are further encrypted
with L2 key before being packed into VaultData — server stores ciphertext,
returns
{"label":"CVV","value":null,"l2":true}to AI
L2 — Client Key (WebAuthn PRF, never leaves device)
- Derived entirely in browser via WebAuthn PRF extension
navigator.credentials.get({extensions:{prf:{eval:{first:salt}}}})→ 32-byte secret- Secret → HKDF → L2 key → decrypt L2 field values in browser (Web Crypto API)
- Server NEVER sees L2 key or L2 plaintext
- L2 key lives in
sessionStorageonly — gone on tab close - Auto-lock after 15 minutes idle (configurable)
WebAuthn authenticators supported
- Touch ID (Mac) — native browser prompt
- Face ID (iPhone, via web UI on iOS Safari)
- Windows Hello
- Google Titan Key (USB-A/NFC, FIDO2)
- YubiKey (FIDO2)
Register multiple authenticators for redundancy. One unlock = all L2 accessible.
Recovery
- Printed 12-word BIP39 mnemonic generated at vault setup
- Mnemonic derives L2 key as last resort (entropy → HKDF)
- Store physically (safe, safety deposit box)
- NO email/SMS/server fallback — would break the security model
Import (LLM-powered)
POST /api/import — multipart, accepts any file format
Flow:
- User uploads file (Bitwarden JSON, 1Password CSV, LastPass, Chrome, plain text)
- Server sends content to Fireworks LLM (zero retention):
"Parse this password export into ClawVault entries. Return JSON array of VaultData. Guess types. Mark l2:true on fields that appear sensitive: card numbers, CVV, SSN, passport numbers, private keys, TOTP seeds."
- Server returns preview — user reviews before committing
- L2 fields highlighted in amber — "These fields will NOT be readable by your AI"
- User can toggle L2 per field in preview
- Confirm → write to DB
Best practice enforced by UI: "Review L2 markings before importing." Sensitive fields default to L2 — user explicitly unlocks them for AI if desired.
Browser Extension
Manifest V3, Chrome first.
Files
manifest.jsonbackground.js— service worker, API key, vault callscontent.js— form detection, field fillingpopup.html/js— credential picker
Extension API key
Stored in chrome.storage.local. Full access (L1 + L2 via client-side decrypt).
Extension has its own token type: ext — distinguishable in audit log.
LLM Field Mapping
- Content script detects forms (any login/fill page)
- Serializes visible inputs: label, name, placeholder, type, selector
- User opens popup → picks credential
- Background calls
POST /ext/map:{"entry_id":"...", "fields":[{"selector":"#email","label":"Email","type":"email"},...]} - Server calls LLM: maps vault fields to form selectors
- Content script fills mapped fields
- TOTP field detected → live code generated → filled automatically
L2 fill flow
- Extension popup shows 🔒 on L2 fields
- Click → WebAuthn prompt in browser (Touch ID etc.)
- L2 key derived → L2 fields decrypted in browser memory → filled
- Key discarded after fill
TOTP
Seed stored as L2 field by default (user can change to L1 per entry).
If L1 (AI-accessible):
GET /api/ext/totp/:id→{"code":"123456","expires_in":23}- MCP tool
get_totp("GitHub")→ returns live code - AI can complete 2FA flows autonomously — killer feature
If L2 (client-only):
- Seed only decryptable client-side
- Extension generates code locally after WebAuthn unlock
- AI cannot access
Default: L2 for TOTP seeds. User explicitly marks L1 to enable AI 2FA automation. This is a conscious, visible decision — not a silent default.
MCP Tools (L1 only)
get_credential(query) → VaultData, L2 fields omitted
list_credentials(filter?) → [{entry_id, title, type, urls}]
get_totp(query) → {code, expires_in} (L1 TOTP only)
search_vault(query) → [{entry_id, title, type, matched_field}]
check_expiring(days?) → [{title, type, expires, days_remaining}]
create_entry(data) → entry_id (AI can save new credentials)
MCP token: read-only by default. Write token optional (for AI to save new credentials).
Web UI
Vanilla JS, embedded in binary, no framework, no build step.
Views
/→ entry list, folder tree, search/entry/:id→ detail view/entry/new→ create (dynamic field builder, type picker)/import→ LLM import with preview/settings→ WebAuthn setup, tokens, export, audit log
L2 UX
- L2 fields render as:
🔒 Locked — Touch to unlock - Click → WebAuthn prompt → decrypt in browser → show value + copy button
- "Unlock all" button → single WebAuthn prompt → all L2 visible for session
- Lock icon in nav shows session state (locked/unlocked)
Audit Log
type AuditEvent struct {
EventID string
EntryID string
Title string // snapshot
Action string // read|fill|ai_read|create|update|delete|import|export
Actor string // web|extension|mcp
IPAddr string
Timestamp int64
}
GET /api/audit — paginated, filterable by actor/action/entry.
AI access clearly marked as actor:"mcp".
Password Generator
GET /api/generate?length=20&symbols=true
GET /api/generate?words=4 → "correct-horse-battery-staple"
Crypto/rand throughout. Built into field editor.
Config
VAULT_KEY=<hex-encoded 32 bytes> # required, L1 master key
PORT=8765
DB_PATH=./clawvault.db
FIREWORKS_API_KEY=... # for LLM import + field mapping
LLM_MODEL=accounts/fireworks/models/llama-v3p3-70b-instruct
SESSION_TTL=86400 # seconds
L2_LOCK_IDLE=900 # L2 auto-lock after 15min idle
Build Plan
Day 1 — Foundation
- Go module, entry model, L1 crypto, dbcore
- CRUD API (create/read/update/delete entries)
- Web UI: list, create, view, edit
- Password generator
- Session auth (token-based)
Day 2 — L2 + Extension
- L2 field encryption (client-side Web Crypto in browser)
- WebAuthn PRF registration + unlock flow
- Chrome extension: popup, content script, LLM fill
/ext/mapLLM field mapping endpoint
Day 3 — AI + Import
- MCP endpoint + 6 tools
- OpenClaw skill: ClawVault
- LLM import (
/api/import) with preview UI - TOTP generation
Day 4 — Complete
- Audit log
- File attachments (BLOB in SQLite)
- Expiry alerts (heartbeat + MCP)
- Export (Bitwarden-compatible JSON)
- Systemd service + deploy script
Out of Scope v0.1
- Mobile autofill (v2, OSS community)
- Firefox/Safari extension
- Multi-user/team sharing
- Browser extension for non-Chromium
Competitive Landscape
| ClawVault | Bitwarden MCP | mcp-secrets-vault | 1Password | |
|---|---|---|---|---|
| Field-level AI visibility | ✅ | ❌ | ❌ | ❌ |
| Two-tier encryption | ✅ | ❌ | ❌ | ❌ |
| WebAuthn L2 unlock | ✅ | ❌ | ❌ | ✅ (auth only) |
| LLM field mapping | ✅ | ❌ | ❌ | ❌ |
| LLM import (any format) | ✅ | ❌ | ❌ | ❌ |
| One binary | ✅ | ❌ | ✅ | ❌ |
| Self-hosted | ✅ | ✅ | ✅ | ❌ |
| Open source | ✅ | ✅ | ✅ | ❌ |
ClawVault — the vault that knows who it's talking to.