# LocalVault Encrypted local secrets vault with MCP server for AI agents. Zero infrastructure, zero cloud dependency. > **[Try the interactive demo](https://inventlist.com/tools/localvault/cli)** — explore every command in your browser. Part of [InventList Tools](https://inventlist.com/tools/localvault) — free, open-source developer utilities for indie builders. --- ## Install ### Homebrew (macOS / Linux) ```bash brew install inventlist/tap/localvault ``` ### Install script (no Homebrew) ```bash curl -sSL https://inventlist.com/tools/localvault/install.sh | sh ``` Installs into an isolated prefix (`~/.localvault/runtime`) and writes a wrapper to your PATH that pins its own Ruby. It never adds a version-manager shim and never touches your project gems, so `localvault` keeps working when you switch Ruby versions. Override with `LOCALVAULT_BIN_DIR`, `LOCALVAULT_PREFIX`, `LOCALVAULT_VERSION`, or `LOCALVAULT_RUBY`. ### RubyGems ```bash gem install localvault ``` A plain `gem install` under asdf/rbenv creates a shim that can shadow your Homebrew build and silently pin you to an old version. If `localvault version` disagrees with what you installed, run `localvault doctor` — it lists every copy on your PATH. **Requires libsodium:** ```bash # macOS brew install libsodium # Ubuntu/Debian sudo apt-get install libsodium-dev # Fedora sudo dnf install libsodium-devel ``` ## Quick Start ```bash # Create a vault (prompts for passphrase) localvault init # Store secrets without putting values in shell history / process args printf '%s' "$OPENAI_API_KEY" | localvault set OPENAI_API_KEY --stdin printf '%s' "$STRIPE_SECRET_KEY" | localvault set STRIPE_SECRET_KEY --stdin printf '%s' "$DATABASE_URL" | localvault set DATABASE_URL --stdin # Retrieve a secret (raw, pipeable) localvault get OPENAI_API_KEY # View all secrets (masked by default) localvault show # Reveal values localvault show --reveal # Export as shell variables eval $(localvault env) # Run a command with secrets injected localvault exec -- rails server ``` ## Commands ### Secrets | Command | Description | |---------|-------------| | `init [NAME]` | Create a vault (Argon2id key derivation) | | `set KEY --stdin` | Store a secret from stdin without argv/history exposure | | `set KEY [VALUE]` | Store a secret (positional value kept for compatibility) | | `set --group GROUP KEY --stdin` | Store a secret in a named group from stdin (`-g`) | | `get KEY` | Retrieve a secret (raw, pipeable) | | `show` | Display all secrets in a table (masked by default) | | `show --reveal` | Display with values visible (`-r`) | | `show --group` | Group by dot-notation prefix, one table per project (`-g`) | | `show --group QUERY` | Show one exact or uniquely matching group | | `groups [QUERY]` | List/search group names and key counts without values | | `list` | List key names only | | `delete KEY` | Remove a secret | | `rename OLD NEW` | Rename a secret key | | `copy KEY --to VAULT` | Copy a secret to another vault | | `import FILE` | Bulk-import from .env / .json / .yml | | `env` | Export as `export KEY="value"` lines | | `exec -- CMD` | Run a command with secrets injected as env vars | ### Vault Management | Command | Description | |---------|-------------| | `vaults` | List all vaults with secret counts | | `switch [VAULT]` | Switch default vault | | `unlock` | Cache passphrase for the session | | `lock [NAME]` | Clear cached passphrase | | `rekey [NAME]` | Change vault passphrase (re-encrypts all secrets) | | `reset [NAME]` | Destroy and reinitialize a vault | ### Sync & Login | Command | Description | |---------|-------------| | `login [TOKEN]` | Log in to InventList — auto-generates X25519 keypair + publishes public key | | `login --status` | Show current login status | | `logout` | Clear stored credentials | | `sync` | Sync all vaults bidirectionally (push local, pull remote, detect conflicts) | | `sync --dry-run` | Preview what sync would do without making changes | | `sync push [NAME]` | Push one vault to cloud | | `sync pull [NAME]` | Pull one vault from cloud (auto-unlocks if you have a key slot) | | `sync diff [NAME]` | Show which keys differ between local and cloud (names only, never values) | | `sync merge [NAME]` | Three-way merge local and cloud key by key, then push (`--prefer local\|remote`, `--local KEY`, `--remote KEY`) | | `sync status` | Show sync state for all vaults | ### Team Sharing (v1.3.0) Vault-level operations live under `team`. Person operations (the `@handle` already signals a person) are top-level. | Command | Description | |---------|-------------| | `team init` | Convert vault to team vault (sets you as owner, SyncBundle v3) | | `team list` | List vault members | | `team rotate` | Re-key vault with new passphrase, keep all members | | `verify @handle` | Check if a user has a published public key (dry-run) | | `add @handle` | Add teammate with full vault access | | `add @handle --scope KEY...` | Add teammate with access to specific keys only | | `remove @handle` | Remove teammate's access | | `remove @handle --scope KEY` | Remove one scoped key (keeps other scopes) | | `remove @handle --rotate` | Full revocation + re-encrypt with new passphrase | The `team add`, `team remove`, and `team verify` aliases still work for backward compatibility but the top-level forms are preferred. ### Keys | Command | Description | |---------|-------------| | `keys generate` | Generate X25519 identity keypair | | `keys show` | Display your public key | | `keys publish` | Upload public key to InventList (required before others can add you) | ### AI / MCP | Command | Description | |---------|-------------| | `install-mcp [CLIENT]` | Configure MCP server in claude-code, cursor, or windsurf | | `mcp` | Start MCP server (stdio transport) | | `doctor` | Check install and PATH readiness, including brew/asdf shadowing | | `guard install` | Install Claude Code hooks that block secrets in agent commands (v1.9.0) | | `guard status` | Show guard hook installation state | | `guard hook` | Hook entrypoint (reads hook JSON on stdin; not run by hand) | All commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Default vault is `default`. ## Personal Sync Sync your vaults between machines — same passphrase, no team features needed: ```bash # Machine A: push all your vaults at once localvault sync # Machine B: install, login, sync brew install inventlist/tap/localvault localvault login YOUR_TOKEN localvault sync # pulls everything, pushes local-only vaults localvault show # enter your passphrase — same secrets ``` Or push/pull individual vaults: ```bash localvault sync push production # push one vault localvault sync pull production # pull one vault localvault sync status # check what's synced vs local-only ``` Preview before syncing: ```bash localvault sync --dry-run # Vault Action Reason # default skip up to date # production push local changes # staging pull remote changes ``` ### Resolving a conflict When a vault changed on both machines since the last sync, `sync` stops and shows which keys differ. Values are never printed. ```bash localvault sync # devops CONFLICT both local and remote changed since last sync # cloud added REMOTE_ONLY will take cloud # local added LOCAL_ONLY will keep local # CONFLICT changed on both sides SHARED needs a choice localvault sync merge devops # clean merge: keeps every change from both sides localvault sync merge devops --prefer remote # conflicting keys: take cloud localvault sync merge devops --local SHARED # or decide per key (--local / --remote, repeatable) localvault sync diff devops # just look, change nothing ``` The merge is three-way: each sync records an encrypted snapshot of the last-synced state, so a key edited on only one side applies automatically and only keys edited differently on both sides need a choice. Deleting a key on one side while the other side edits it is also a conflict. `sync push` and `sync pull --force` still take one side wholesale. ## Team Sharing Share vault access with teammates using X25519 asymmetric encryption. The server never sees plaintext. ```bash # 1. Convert to team vault (required first) localvault team init -v production # 2. Verify teammate has a published key localvault verify @alice # 3. Add with full access localvault add @alice -v production # 4. Or scoped — they only see specific keys localvault add @bob -v production --scope STRIPE_KEY WEBHOOK_SECRET # 5. When Alice pulls, auto-unlocks via her identity key # (on Alice's machine) localvault sync pull production # => Unlocked via your identity key. # 6. Scoped members can't push # (on Bob's machine) localvault sync push production # => Error: You have scoped access. Only the owner can push. # 7. Rotate without removing anyone localvault team rotate -v production # 8. Full revocation + re-key localvault remove @alice -v production --rotate ``` **Prerequisites:** Teammates must have a published public key. `localvault login` does this automatically, or: `localvault keys generate && localvault keys publish`. ## MCP Server (AI Agents) Give AI agents controlled secret access without hardcoding credentials in MCP config. The default workflow keeps values out of model context: discover names, build a `localvault exec` command, then run it with process-scoped injection. ```bash # One-command install for Claude Code localvault install-mcp claude-code # Also supports: cursor, windsurf # Unlock your vault for the session localvault unlock # Verify setup without starting the blocking stdio server localvault mcp --check # Diagnose brew/asdf PATH shadowing after upgrades localvault doctor # MCP tools available to the agent: # localvault_whoami — diagnose active vault/session state # list_secrets(vault?, prefix?, query?) — list/search key names # localvault_build_exec(command, ...) — build safe injection (does not execute) # get_secret(key, allow_plaintext: true, vault?) — explicit plaintext reveal # set_secret(key, value, vault?) — store a secret # delete_secret(key, vault?) — remove a secret ``` Agents should prefer `localvault_build_exec` for commands, evaluation, API calls, and configuration checks. It returns both argv and a shell-safe command without opening a vault or reading a value. `get_secret` rejects calls unless `allow_plaintext: true` is explicit. Use selectors, mappings, and profiles to keep subprocess envs scoped: ```bash localvault exec --profile aws -- aws sts get-caller-identity localvault exec --only AWS_IAM.*,AWS_SES.* --except AWS_SES.smtp_password -- your-script localvault env --map AWS_IAM.access_key_id=AWS_ACCESS_KEY_ID ``` ## Multi-Project Vaults One vault, many projects. Dot-notation keeps secrets organized: ```bash # Store with project prefix localvault set myapp.DATABASE_URL postgres://localhost/myapp -v work localvault set api.DATABASE_URL postgres://localhost/api -v work # Or use the guided group form localvault set --group myapp DATABASE_URL postgres://localhost/myapp -v work # Search groups without revealing values localvault groups app -v work # View grouped by project localvault show --group -v work # Show one group by exact or unique prefix localvault show --group my -v work # Filter to one project localvault show -p myapp -v work # Export one project eval $(localvault env -p myapp -v work) # Export all projects with project.key transformed to PROJECT__key localvault env -v work # → MYAPP__DATABASE_URL, API__DATABASE_URL # Bulk import localvault import .env --prefix myapp -v work ``` ## Session Caching Avoid typing your passphrase repeatedly: ```bash eval $(localvault unlock) # All subsequent commands skip the passphrase prompt localvault get API_KEY localvault exec -- rails server ``` Unlocking writes a derived key to `LOCALVAULT_SESSION` and also caches it with an 8-hour TTL in Keychain or LocalVault's file fallback so MCP and new terminals can reuse it until `localvault lock`. ## Security ### Agent Plaintext Containment (v1.9.0) Two layers keep secrets out of AI agent context and transcripts: **1. Plaintext refuses captured streams.** `get`, `env`, and `show --reveal` print values to an interactive terminal as always. When stdout is captured (a pipe or an agent's shell), a human confirms with one keypress on `/dev/tty`; an agent's shell has no `/dev/tty`, so it is refused and pointed at injection: ```bash localvault get STRIPE_KEY # human at a terminal: prints localvault get STRIPE_KEY | pbcopy # human piping: "Print plaintext? [y/N]" # agent shell: refused → use localvault exec --map STRIPE_KEY=STRIPE_KEY -- CMD ``` There is deliberately no flag or environment variable to bypass this — the only override is a keypress on a real terminal. Headless automation uses `localvault exec` injection. **2. Guard hooks block secrets in agent commands.** `localvault guard install` wires Claude Code hooks that scan every Bash tool call against your unlocked vaults. A command containing a stored secret value is blocked before it runs, naming the key by fingerprint — never by value: ```text LocalVault guard: blocked — this tool input contains the plaintext value of default/STRIPE.private_key (sha256:1a2b3c4d5e6f). Inject it instead: localvault exec --map STRIPE.private_key=PRIVATE_KEY -- your-command ``` The installed hook fails open: locked vaults, an old binary, or a missing install allow the call rather than breaking your session. ### Crypto Stack | Layer | Algorithm | Purpose | |-------|-----------|---------| | Key derivation | **Argon2id** (64 MB, 3 iterations) | Passphrase → master key | | Encryption | **XSalsa20-Poly1305** | Authenticated encryption | | Key exchange | **X25519** | Team key slots + vault sharing | - Random 24-byte nonce per encryption - Poly1305 authentication prevents tampering - Argon2id is memory-hard (GPU/ASIC resistant) - All crypto via [libsodium](https://doc.libsodium.org/) (RbNaCl bindings) - SyncBundle v3 for team vaults (owner field + per-member key slots) ### Storage Layout ``` ~/.localvault/ ├── config.yml # Default vault, server URL, token ├── identity.key # X25519 private key (encrypted at rest) ├── identity.pub # X25519 public key (safe to share) ├── vaults/ │ ├── default/ │ │ ├── meta.yml # Salt, creation date, version │ │ └── secrets.enc # Encrypted JSON blob │ └── production/ │ ├── meta.yml │ └── secrets.enc ``` ## Server Independence LocalVault is server-agnostic. It ships configured for `inventlist.com` but works with any host that implements the protocol (4 endpoints): ```bash # Use a different server localvault config set server https://vaulthost.example # Or override per-login localvault login --server https://vaulthost.example ``` ## Development ```bash git clone https://github.com/inventlist/localvault.git cd localvault bundle install bundle exec rake test ``` ## Used by Powers credentials management at [InventList](https://inventlist.com) — where indie builders ship, share, and get discovered. ## License Apache 2.0 — see [LICENSE](LICENSE). Built by the [InventList](https://inventlist.com) team.