Skip to main content
Glama
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