cvault
by shahrozakbar
README.md
# π cvault
**A local, encrypted, multi-tenant password manager for Claude Code.** Claude can *use* your credentials (run commands, write `.env` files, call APIs) without the values ever entering the chat, and you get a fast terminal UI to browse, copy and edit them.
```
tenant (client / workspace)
ββ environment (optional) develop Β· staging Β· prod β¦
ββ project β linked to a directory on disk
ββ service (postgres, stripe, admin-panel, β¦)
ββ item secret Β· credential (multi-field) Β· file
ββ role + default ("log in as admin")
ββ versions v1, v2, β¦ (never deleted)
path: tenant/env/project/service/key e.g. rezilens/staging/digrc-api/admin-panel/systemadmin
```
<table>
<tr><td>
**For Claude (MCP server)**
- Uses secrets **without seeing them**: output is scrubbed to `***`
- **Sealed mode**: you type or receive values in a native macOS dialog or on the clipboard
- Knows the current project's credentials at **session start** (hook)
- Picks the right credential by **role**, or falls back to the default
</td><td>
**For you (CLI + interactive UI)**
- `cvault`: arrow-key explorer with tables, search, copy, edit
- Every change is a **new version**; roll back any time
- **Archive** instead of delete, restorable at any level
- Full **audit log** of every access
</td></tr>
</table>
---
## Contents
- [Quick start](#quick-start)
- [How Claude uses it](#how-claude-uses-it)
- [Interactive explorer](#interactive-explorer)
- [CLI reference](#cli-reference)
- [MCP tools](#mcp-tools)
- [Concepts](#concepts): refs & environments Β· roles & defaults Β· versioning Β· archive Β· export & import Β· directory linking
- [Security model](#security-model)
- [Development](#development)
---
## Quick start
```bash
git clone <this repo> ~/mcp/vault-mcp && cd ~/mcp/vault-mcp
npm install && npm run build
npm link # puts `cvault` on your PATH
# 1. store a random master password in the macOS Keychain and create the vault
security add-generic-password -a "$USER" -s vault-mcp -w "$(openssl rand -base64 33)"
export VAULT_MASTER_PASSWORD_CMD='security find-generic-password -s vault-mcp -w' # add to ~/.zshrc
cvault init
# 2. register the MCP server with Claude Code (user scope = every project)
claude mcp add cvault -s user \
-e VAULT_MASTER_PASSWORD_CMD='security find-generic-password -s vault-mcp -w' \
-- "$(which node)" ~/mcp/vault-mcp/dist/index.js
# 3. (recommended) SessionStart hook, so Claude knows each project's credentials
# add to ~/.claude/settings.json β hooks.SessionStart:
# { "hooks": [{ "type": "command", "timeout": 10,
# "command": "node ~/mcp/vault-mcp/dist/cli.js hook session-start" }] }
# 4. add your first project, linked to its directory
cvault project add acme/api --bind ~/work/acme-api
cvault set-cred acme/staging/api/admin-panel/superadmin -u root --role admin --default # password prompted, hidden
```
> The vault lives in `~/.vault-mcp/` (override with `VAULT_HOME`). The master password never leaves the Keychain.
---
## How Claude uses it
Start Claude inside a linked directory and the session hook tells it which items exist (names only, never values):
```
CVAULT: this working directory is bound to vault project "acme/api"
Available items (names only, no values):
- admin-panel/superadmin (credential; role: admin; DEFAULT; fields: username, password, url)
- admin-panel/auditor (credential; role: viewer; fields: username, password)
- stripe/api_key (secret; "Stripe live key")
```
Then just ask:
| You say | Claude does | Claude sees |
|---|---|---|
| "run the migrations with the DB creds" | `run_with_secrets` with `PGPASSWORD` β `postgres/app` | command output with `***` |
| "create the .env for this project" | `write_env_file` (0600, refuses if not gitignored) | the list of keys written |
| "list Stripe customers" | `http_request` with `Bearer {{secret:stripe/api_key}}` | the response, scrubbed |
| "log into the admin panel as viewer" | picks `admin-panel/auditor` by role | nothing secret |
| "log into the staging panel" *(no credential stored yet)* | **automatically** calls `request_credential`: dialogs ask you for the save path (pre-filled), then username and password | `stored β¦ as v1 (fields: username, password)` |
| "save the OpenAI key **sealed**" | `sealed_save`: path dialog, then masked value | `stored β¦ as v1` |
| "give me the DB password **sealed**" | `sealed_fetch` copies it to your clipboard (auto-clears in 30s) | `copied` |
---
## Interactive explorer
Run **`cvault`** with no arguments (or `cvault ui`). `cvault --help` lists the scriptable commands.
```
cvault 5 items in 1 project Β· ~/.vault-mcp
home βΊ acme βΊ api
ββββββββββββββββββββββββββββββββββββββββββββββββ Esc back/cancel Β· Ctrl+C quit ββ
? Pick an item
SERVICE β KEY β TYPE β ROLE β DEFAULT β FIELDS / FILE β VER β DESCRIPTION
βββββββββββββΌβββββββββββββΌβββββββββββββΌβββββββββΌββββββββββΌββββββββββββββββββββββββββΌββββββΌββββββββββββββββ
β― admin-panel β auditor β credential β viewer β - β username, password β v1 β Read-only
admin-panel β superadmin β credential β admin β yes β username, password, url β v2 β Full access
ssh β deploy-key β file β - β - β deploy.pem (412 B) β v1 β CI deploy key
stripe β api_key β secret β - β - β - β v3 β Stripe live key
ββ Actions ββββββββββββββββββββββ
+ New itemβ¦
Servicesβ¦
```
- **Navigate:** opens at the project linked to your current directory; search across all items by ref, role or description.
- **Copy** any secret or credential field. The clipboard auto-clears after 30s, even if you've already quit.
- **Export:** an encrypted bundle, `.env` or JSON for a project or the whole vault.
- **View** all fields, version history and the audit log as tables.
- **Edit** values and fields, add or remove fields, replace files. Each change saves a new version.
- **Labels:** role, default and description, without creating a new version.
- **Manage:** create tenants, projects, services and items; link directories; toggle Claude reveal; archive and restore; roll back; **export** a project or the whole vault.
- **Keys:** `ββ` move Β· `β` select Β· **`Esc`** back / cancel the current action Β· `Ctrl+C` quit.
- **Responsive:** tables drop low-priority columns on narrow terminals.
---
## CLI reference
| Command | What it does |
|---|---|
| `cvault` / `cvault ui` | interactive explorer |
| `cvault init` | create the vault |
| `cvault ls [scope] [--archived] [--json]` | tables of projects, services and **all items** (scope: `t`, `t/env`, `t/p`, `t/env/p`, β¦) |
| `cvault project add <t/p> [--bind dir]` | create a project, optionally linked to a directory |
| `cvault project bind <t/p> [dir] [--env e] [--remove]` | link or unlink a directory (optionally with a default environment) |
| `cvault project reveal <t/p> on\|off` | allow Claude to read plaintext (`reveal_secret`) |
| `cvault service <t/[env/]p/s> [--url] [--allow-host hβ¦] [--add-host hβ¦] [--clear-hosts]` | create or update a service, and restrict where its secrets may be sent |
| `cvault service-move <from> <to>` | rename a service or move it into or out of an environment |
| `cvault unlock <ref>` | unlock a credential locked after a 401 or a blocked use |
| `cvault project chat-values <t/p> on\|off` | allow Claude to store values it received in chat (off by default) |
| `cvault set <ref> [--stdin] [-r role] [--default]` | store a secret (hidden prompt) |
| `cvault set-cred <ref> -u user [-f k=v] [-r role] [--default]` | store a credential (password prompted, hidden) |
| `cvault put-file <ref> <file>` | encrypt a file into the vault |
| `cvault get <ref>[@N][#field] [--out file]` | print a value (credentials as a table) |
| `cvault tag <ref> [-r role] [--default\|--no-default]` | set labels, no new version |
| `cvault versions <ref>` Β· `cvault rollback <ref> <N>` | history and roll back |
| `cvault archive <target>` Β· `cvault restore <target>` | archive or restore a tenant, project, service or item |
| `cvault import .env --into <t/p/s>` | bulk-import a `.env` file |
| `cvault export [scope] [-f bundle\|env\|json] [-o file]` | export a tenant, project, service or everything ([details](#export--import)) |
| `cvault import-bundle <file> [--into t or t/p]` | restore an encrypted bundle (existing items get a new version) |
| `cvault context [dir]` | preview what the session hook injects |
| `cvault audit [-n N] [-r prefix]` | access log |
| `cvault backup <dir>` Β· `cvault change-password` | maintenance |
---
## MCP tools
| Group | Tools |
|---|---|
| Browse (no values) | `vault_status` Β· `resolve_context` Β· `list_tenants` Β· `list_projects` Β· `list_services` Β· `list_items` Β· `audit_log` |
| Use (values hidden) | `run_with_secrets` Β· `write_env_file` Β· `materialize_file` Β· `http_request` |
| Sealed (you β vault) | `request_credential` (missing login: dialogs for path + fields) Β· `sealed_save` (path + masked value) Β· `sealed_fetch` (clipboard or dialog) |
| Manage | `create_tenant` Β· `create_project` Β· `create_service` Β· `bind_project_path` Β· `set_secret` Β· `set_credential` Β· `generate_secret` Β· `put_file` Β· `tag_item` |
| Versions & archive | `list_versions` Β· `rollback_secret` Β· `archive` Β· `restore` |
| Reveal (opt-in) | `reveal_secret`: only when you've enabled it per project with the CLI |
---
## Concepts
### Refs & environments
`tenant/[env/]project/service/key[@version][#field]`
| Form | Example |
|---|---|
| with environment (5 levels) | `acme/staging/api/admin-panel/root#password` |
| without environment (4 levels) | `acme/api/stripe/api_key@2` |
| inside a linked directory | `staging/admin-panel/root` or `stripe/api_key` |
- The environment is optional. The same service name can exist once per environment (`staging/admin-panel`, `develop/admin-panel`), each with its own items, versions and **allowed hosts**.
- A directory link can carry a default environment: `cvault project bind acme/api --env staging`. Then `admin-panel/root` means staging.
- Listing and export scopes accept `acme/staging` (everything in staging), `acme/staging/api` or `acme/api` (all environments).
- `cvault service-move acme/api/staging-panel acme/staging/api/panel` moves an existing service into an environment (items, versions and hosts move with it).
- Credentials default to the `password` field.
### Missing credentials
When a task needs a login that isn't in the vault, the session hook, server instructions and "not found" errors all point Claude at `request_credential`. It opens native dialogs:
1. **Path:** pre-filled with Claude's suggestion and editable. Paths are corrected automatically, with no second dialog. Project and environment typed in the wrong order (`rezilens/digrc-api-service/develop/admin-panel/systemadmin`) become `rezilens/develop/digrc-api-service/admin-panel/systemadmin`; extra levels fold into the service name; messy characters are cleaned. It only asks again when the intent can't be worked out.
2. **Fields:** username (visible), password (hidden), plus any extra fields Claude asks for. Masked fields (password, token, secret, key, pin, otp) are **asked twice**, and a mismatch asks again, so a typo can't be saved silently. For an existing item, leaving a field empty keeps its current value, and a new version is saved.
If a login is rejected, Claude is told to stop after **one** attempt (accounts often lock after a few), never to try another environment's credential, and to offer `request_credential` on the same path to re-enter it.
Claude only receives the resulting ref.
### Enforced rules (code, not instructions)
The server enforces these itself, so Claude can't skip them:
| Rule | How it's enforced |
|---|---|
| **Missing secret β ask the user** | Before `run_with_secrets` / `write_env_file` / `http_request` / `sealed_fetch` runs, missing refs trigger the `request_credential` dialogs automatically, then the original call continues (using a different path if you saved it elsewhere). |
| **Allowed hosts per service** | `cvault service t/p/s --allow-host core-staging.example.com --add-host "*.staging.example.com"`. `http_request` checks the final URL; `run_with_secrets` checks the URLs, `-h/--host` flags and `user@host` in the command. Anything else is **blocked**, so a develop password can't reach staging. Only you can set this (CLI or explorer, not an MCP tool). |
| **Lock after a rejected login** | An HTTP **401** from `http_request` locks every credential used in that request. Locked items are refused for Claude until you re-enter them (a new version unlocks) or run `cvault unlock <ref>`. You can still view and copy them in the CLI and explorer. |
| **Use budget** | After **5** uses of a password/token field within **10 minutes** (any tool, including clipboard fills), a dialog asks **Allow / Block**. Allow gives 30 more minutes; Block locks the credential. Usernames don't count. |
| **No values through chat** | `set_secret` / `set_credential` are refused unless you run `cvault project chat-values t/p on`. |
### Roles & defaults
Give credentials a `role` (admin, viewer, tester, β¦) and mark one per service as the **default**. "Log in as viewer" picks the viewer item; with no role named, Claude uses the default and only asks when neither applies.
### Versioning
Every write creates version N+1. Old versions stay encrypted (and old files are kept as `<uid>.vN.bin`). `@N` reads any version; `rollback` copies an old version forward as a **new** version, so history is never rewritten.
### Archive, never delete
Archive a tenant, project, service or single item. It's hidden and unusable but fully restorable. Archiving a tenant hides everything under it.
### Export & import
| Format | Encrypted? | Contents | Typical use |
|---|---|---|---|
| `bundle` *(default)* | β
passphrase β scrypt β AES-256-GCM | every item incl. **files**, roles, defaults, descriptions | backups, moving to another machine, handing a project to a teammate |
| `env` | β plaintext | `KEY=value`; credentials become `KEY_FIELD`; files skipped | seeding a `.env` |
| `json` | β plaintext | nested `tenant β project β service β item` | scripts and other tools |
```bash
cvault export acme/api # β cvault-acme-api.cvault (asks for a passphrase)
cvault import-bundle cvault-acme-api.cvault # on the other machine
cvault import-bundle backup.cvault --into acme-staging/api # remap tenant/project
cvault export acme/api/stripe -f env --yes --stdout # STRIPE keys as KEY=value
```
`.env` names are built from the path below the export scope: exporting a project gives `STRIPE_API_KEY` and `ADMIN_PANEL_SUPERADMIN_PASSWORD`, while exporting one service gives `API_KEY` and `SUPERADMIN_PASSWORD`. Name clashes are an error, never a silent overwrite. Plaintext exports need confirmation (`--yes` in scripts), are written with mode 600, and are refused inside a git repo unless the file is gitignored. For scripts, set the bundle passphrase with `CVAULT_BUNDLE_PASSPHRASE`. Export is deliberately **not** an MCP tool, so Claude can't dump the vault. In the explorer: **Exportβ¦** on the home page and on each project page.
### Directory linking
`cvault project bind` links a directory (and its subdirectories; the most specific link wins) to a project. This drives short refs, the explorer's start page and the SessionStart hook.
---
## Security model
- **Crypto:** master password β scrypt (N=2ΒΉβ·) β key-encryption key, which wraps a random 256-bit data key. Every value and file is encrypted with **AES-256-GCM**, with the item's UUID as additional authenticated data. Metadata (names, roles, bindings) is stored unencrypted, so the session hook needs no password.
- **Master password** comes from the macOS Keychain via `VAULT_MASTER_PASSWORD_CMD`, and is removed from the server's environment at startup so child processes never inherit it.
- **Claude never sees values by default.** Injected secrets are scrubbed (raw, base64 and URL-encoded) from command and HTTP output. `reveal_secret` is off per project, and only the CLI can turn it on.
- **Sealed mode:** values travel through native macOS dialogs or the clipboard, never through tool arguments or results. They're passed to `osascript` via env, not argv.
- **Guardrails:** `.env` and file writes into a git repo are refused unless the file is gitignored, and files are written with mode 0600.
- **Audit log** of every use, reveal, copy and edit. It never records values.
> **Limit:** scrubbing keeps secrets out of the conversation and transcripts. It is not a sandbox, so a command can still misuse an injected secret on purpose. Review commands that come from untrusted content.
---
## Development
```bash
npm run build # tsc β dist/
npm test # vitest (crypto, versioning, archive, roles, export/import, injection, scrubbing)
npx @modelcontextprotocol/inspector node dist/index.js
```
```
src/
index.ts MCP server (tools, sealed mode, reveal gate)
cli.ts cvault CLI + SessionStart hook
ui.ts interactive explorer
store.ts vault: hierarchy, versioned items, archive, roles, audit
crypto.ts scrypt + AES-256-GCM
inject.ts run / .env / file / http injection + scrubbing
sealed.ts macOS dialogs + clipboard with auto-clear
export.ts encrypted bundles, .env / JSON export, bundle import
enforce.ts server-side rules: auto-prompt, allowed hosts, use budget, 401 lock
policy.ts host matching / extraction, budget constants
table.ts table rendering
db.ts SQLite schema + migrations
```
---
## License
[MIT](LICENSE) Β© 2026 shahroz Dhillon
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues