Password Space
by vishnutvm
README.md
# Password Space
**A local encrypted credential vault for developers and AI agents.**
Store secrets once. Unlock them explicitly. Let agents access credentials through MCP without routinely exposing secret values in chat transcripts.
> Encryption at rest and MCP inject handles reduce how often secrets appear in chat — they do **not** make secrets invisible to a compromised machine, a stolen unlock session, or every possible log/tool path. See [Security model](#security-model) and [SECURITY.md](./SECURITY.md).
## Why Password Space?
| Anti-pattern | What goes wrong |
| --- | --- |
| Paste secrets into chat | Persist in transcripts, screenshots, logs |
| Commit `.env` files | Leak via git and shared workspaces |
| Plaintext MCP configs | Readable by anything with config access |
Password Space keeps **ciphertext on disk**, an **explicit unlock** gate, and an **agent side channel** (MCP + short-lived inject files + Skill + optional redaction hook).
## Features
| Feature | What it does |
| --- | --- |
| Encrypted local storage | Secrets in SQLite (`vault.db`), AES-256-GCM under a passphrase-wrapped vault key |
| Explicit unlock | `vault_unlock` (MCP) or CLI `unlock` before tools can use the vault; `vault_lock` / `lock` clears the session |
| Metadata-only listing | `list` / MCP `list_secrets` return names, ids, tags — not values |
| Day-1 MCP setup | `vault_status`, `vault_init`, `vault_unlock`, `vault_lock`, `vault_set_secret` — no clone/`pnpm` required after plugin install |
| Secret injection | `inject_secret` writes a short-lived `0600` env file and returns a handle + path — not the value |
| Session TTL | Unlock session expires (default **8 hours**); expired sessions are rejected and cleared |
| Redaction hook | Optional `beforeSubmitPrompt` hook strips **known** inject values from outbound prompts |
| Bundled plugin MCP | `plugins/password-space/dist/index.js` is a self-contained esbuild bundle (vault + MCP) |
When the vault is **locked**, mutating/read tools fail until unlock again. Restarting the MCP process does not unlock the vault — it only re-reads an existing valid session file.
## Security model
**Designed to protect:** vault database on disk while locked (ciphertext + wrapped key); chat/transcript plane when agents use `inject_secret` / tool-arg secrets and follow the Skill (handles like `ps://NAME` instead of values).
**Does not protect against:** an attacker on the same unlocked user account; theft of a live session file; plaintext while present in process memory, inject `*.env` files, or CLI `get` / `resolve_secret` results; hooks that are disabled, misconfigured, or incomplete.
| Area | MVP behavior |
| --- | --- |
| KDF / encryption | scrypt (N=2¹⁵, r=8, p=1) + AES-256-GCM via Node `crypto` — see [SECURITY.md](./SECURITY.md) |
| Unlock session | `$PASSWORD_SPACE_HOME/session`, mode `0600`, default TTL 8h; lock deletes it |
| Inject files | `$PASSWORD_SPACE_HOME/inject/<token>.env`, mode `0600`, default TTL **5 minutes** (consume then delete) |
| Passphrase loss | **No recovery** — ciphertext is unrecoverable without the unlock passphrase |
| Redaction | Only **known** values from inject files / explicit secret lists — not a universal DLP |
Full threat model, trust boundaries, and reporting: **[SECURITY.md](./SECURITY.md)**. Architecture / key hierarchy: **[docs/architecture.md](./docs/architecture.md)**.
## Requirements
| Requirement | Notes |
| --- | --- |
| Node.js | **≥ 20** (uses built-in `node:sqlite`) — required to run the bundled MCP |
| Cursor plugin | Cursor with marketplace / MCP support |
| Package manager | **pnpm** 9.x — **optional**; only for CLI / developing this repo |
| OS | Developed and verified on **macOS**; should run anywhere Node 20 + `node:sqlite` works — treat Linux/Windows as **best-effort** until listed here as tested |
## Quickstart (Cursor plugin — primary)
Day-1 setup does **not** require `git clone` or `pnpm`.
1. Open **Cursor Settings → Customize** (or **Dashboard → Plugins & MCPs** for team marketplaces).
2. **+ Add Marketplace** → **Import from GitHub**.
3. Paste: `https://github.com/vishnutvm/password-space`
4. Install the **password-space** plugin.
5. Optional custom vault path: set a **real** `PASSWORD_SPACE_HOME` (absolute path) in the environment or a hand-written MCP `env` block. Leave it unset to use `~/.password-space`. Never leave an unresolved `${PASSWORD_SPACE_HOME}` (or other `${…}`) as the path — the server treats those as unset and falls back to the default.
6. Ask the agent, for example:
> Set up Password Space for me: check status, init the vault if needed, unlock it, then store my API token.
The Skill drives this MCP flow:
```text
vault_status → vault_init (if needed) → vault_unlock → vault_set_secret → inject_secret
```
Pass the **passphrase** and **secret values** only as MCP tool arguments (or UI secret prompts / `PASSWORD_SPACE_PASSPHRASE`). **Never** paste them into chat.
Official public listing (later): [cursor.com/marketplace/publish](https://cursor.com/marketplace/publish).
Layout: `.cursor-plugin/marketplace.json` → `plugins/password-space/`.
## Quickstart (CLI — optional power-user)
```bash
git clone https://github.com/vishnutvm/password-space.git
cd password-space
pnpm install && pnpm build
pnpm password-space init # interactive passphrase (min 8 chars)
pnpm password-space unlock # writes session file
pnpm password-space set GITHUB_TOKEN --tags api,github # prompts for value on a TTY
pnpm password-space list # names / tags only
pnpm password-space status
pnpm password-space lock
```
### How `set` reads the secret value
| Input | When |
| --- | --- |
| Interactive hidden prompt | TTY and no `--value` |
| stdin (one value) | Non-TTY (pipes) |
| `--value <text>` | Explicit flag (avoid in shell history for real secrets) |
Prefer piping from a protected source rather than putting a real token on the command line:
```bash
printf '%s' "$GITHUB_TOKEN" | pnpm password-space set GITHUB_TOKEN --tags api,github
```
`pnpm build` also regenerates the marketplace MCP bundle at `plugins/password-space/dist/index.js`.
### Throwaway demo
```bash
export PASSWORD_SPACE_HOME=/tmp/ps-demo
rm -rf "$PASSWORD_SPACE_HOME"
# WARNING: inline PASSWORD_SPACE_PASSPHRASE is for demos only — it can end up in shell history.
# Do not use this pattern with a real vault passphrase.
PASSWORD_SPACE_PASSPHRASE='demo-pass-ok' pnpm password-space init
PASSWORD_SPACE_PASSPHRASE='demo-pass-ok' pnpm password-space unlock
printf 'ghp_demo_secret_value' | pnpm password-space set DEMO_TOKEN --tags demo
pnpm password-space list
pnpm password-space status
pnpm password-space lock
```
Or run `pnpm demo` (throwaway vault under `/tmp/password-space-demo`).
## Manual MCP configuration
Without the marketplace, point Cursor (or another MCP client) at the **bundled** server (Node only — no monorepo install):
```json
{
"mcpServers": {
"password-space": {
"command": "node",
"args": ["/absolute/path/to/password-space/plugins/password-space/dist/index.js"]
}
}
}
```
`PASSWORD_SPACE_HOME` is optional — omit `env` for the default `~/.password-space`. If you override, use a concrete absolute path only (never an unresolved `${PASSWORD_SPACE_HOME}` placeholder).
Also see [`examples/mcp.cursor.json`](./examples/mcp.cursor.json). Skill: `plugins/password-space/skills/password-space/`. Hooks: `plugins/password-space/hooks/hooks.json`.
### MCP tools
| Tool | Returns secret value? | Use |
| --- | --- | --- |
| `vault_status` | No | Initialized? Unlocked? Next step hint |
| `vault_init` | No | Create vault (passphrase via arg or `PASSWORD_SPACE_PASSPHRASE`) |
| `vault_unlock` | No | Write unlock session |
| `vault_lock` | No | Clear unlock session |
| `vault_set_secret` | **No** (metadata only) | Store/update name + value + tags |
| `list_secrets` | No | Discover names / ids / tags |
| `get_secret_metadata` | No | One secret’s metadata |
| `inject_secret` | **No** (writes env file) | **Preferred** agent access |
| `resolve_secret` | Yes, if `confirm_redaction: true` | Last resort only |
## Secret-injection lifecycle
This is the intended agent path (conversation plane stays on refs; runtime plane reads a file).
```text
1. Human/agent: vault_status → vault_init (once) → vault_unlock
2. Agent: vault_set_secret (values as tool args — not chat)
3. Agent: list_secrets / get_secret_metadata → names only
4. Agent: inject_secret({ name })
5. MCP: decrypt in-process → write inject/<token>.env (0600)
6. Tool result: handle + env_file path + env_var + expires_at (no value)
7. Runtime: source/read env file outside chat → use credential
8. Runtime: delete env file when done
9. Human/agent: vault_lock when finished
```
| Question | Answer |
| --- | --- |
| What does `inject_secret` return? | JSON with `handle` (`ps://NAME`), `env_file` (absolute path), `env_var`, `expires_at`, `redact_in_transcript: true` — **not** the secret value |
| Where is the file? | `$PASSWORD_SPACE_HOME/inject/<random>.env` |
| Permissions? | Written with mode `0600` (best-effort `chmod`) |
| How to use without printing? | Read/source the file in a shell or tool runtime; do **not** `cat` it into chat |
| When deleted? | Caller should delete after use; TTL metadata defaults to **5 minutes** (file is not auto-swept by a daemon in MVP) |
| Vault locked / missing secret? | Tool returns an error payload (`Vault is locked…` / `Secret not found…`); no env file is written |
Avoid CLI `get` and MCP `resolve_secret` in agent workflows — both can put plaintext into the tool/CLI result stream.
## Architecture
```mermaid
flowchart TB
Human[Human + agent prompts] -->|vault_init / unlock / set| MCP[Bundled MCP]
MCP --> Vault[(Encrypted SQLite vault.db)]
MCP -->|session 0600| Session[Unlock session]
Agent[Agent + Skill] -->|list / metadata / inject| MCP
MCP -->|ciphertext decrypt| Vault
MCP -->|env file handle| Inject[inject/*.env]
Agent -->|consume outside chat| Inject
Hook[redact.mjs hook] -->|strip known values| Chat[Chat transcript]
Agent -.->|prefer handles only| Chat
CLI[Optional CLI] -.->|same vault| Vault
```
**Key hierarchy:** passphrase → scrypt(salt) → unwrap vault key → AES-256-GCM per secret.
Detail: [docs/architecture.md](./docs/architecture.md).
## Troubleshooting
| Symptom | Likely cause | What to do |
| --- | --- | --- |
| `node:sqlite` / MCP fails to start | Node < 20 | Upgrade to Node 20+ (`node -v`) |
| `Vault not initialized` | No `vault_init` yet | Ask the agent to call `vault_init`, or CLI `password-space init` |
| `Vault is locked` / `No unlock session` | Locked or expired session | Call `vault_unlock`, or CLI `password-space unlock` |
| `Unlock session expired` | TTL elapsed (default 8h) | Unlock again; use `ttl_hours` / `--ttl` if you need longer |
| `Wrong passphrase` | Typo or wrong vault | Confirm `PASSWORD_SPACE_HOME` matches the vault you meant |
| Plugin installed but MCP errors “locked” | Session missing/expired | Call `vault_unlock` (same `PASSWORD_SPACE_HOME`) |
| Secret not found | Wrong name / empty vault | `list_secrets` after unlock |
| Unexpected data dir | Env override or unresolved `${…}` | Check `PASSWORD_SPACE_HOME` is a real path or unset; `vault_status` shows `data_dir` (placeholders fall back to `~/.password-space`) |
| MCP won’t start | Bad path to `dist/index.js` | Rebuild with `pnpm build`, or reinstall the plugin |
## Development
```bash
pnpm install && pnpm build && pnpm test
```
| Path | Role |
| --- | --- |
| `packages/vault` | Crypto, store, session, inject, redact |
| `packages/cli` | Optional human CLI |
| `packages/mcp-server` | MCP server sources (bundled into the plugin) |
| `plugins/password-space` | Cursor plugin (bundled MCP + skill + hooks + logo) |
See [CONTRIBUTING.md](./CONTRIBUTING.md). License: [MIT](./LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues