Architecture

Vaulted is a Bun and TypeScript monorepo with a Rust injection primitive, compiled to a standalone binary. Secrets live in an encrypted SQLite vault on the developer's machine, never on a server. They are injected into target processes through a hardened Rust runner and exposed to AI coding agents over MCP without values ever entering the model's context window.

The flagship workflows are vaulted import .env followed by vaulted run -- npm run dev, which decrypts locally and injects plaintext into the target process through a hardened Rust dynamic library, and vaulted mcp, which exposes the vault to Claude Code, Cursor, Codex, Zed, OpenCode, and Claude Desktop without secret values ever entering the model's context window.

Repository layout

apps/
cli/ TypeScript CLI on Bun, compiles to a standalone binary
commands/ init, unlock, lock, set, get, list, rm, import, export,
run, projects, envs, mcp
mcp/server.ts MCP stdio server, the agent surface
ffi/ bridge.ts + helper.ts, Bun dlopen() into the Rust runner
lib/ config.ts, password.ts, types.ts
prebuilds/ shipped Rust .dylib/.so per platform target
packages/
crypto/ all E2E cryptography (WebCrypto + hash-wasm Argon2id)
store/ SQLite vault, schema, migrations, repositories, audit log
runner/ Rust cdylib, src/lib.rs, the injection primitive
redact/ streaming output sanitizer (Aho-Corasick multi-pattern)
docs/

Packages

PackageResponsibility
apps/cliThe vaulted binary. Commander.js commands, the MCP stdio server, the FFI bridge into the Rust runner, and config and password handling. Compiles to a standalone binary.
packages/cryptoAll end to end cryptography. Argon2id master key derivation via hash-wasm, AES-256-GCM value encryption on WebCrypto, RSA-OAEP key wrapping, and service account tokens with HKDF-SHA256 key derivation.
packages/storeThe SQLite vault on bun:sqlite. Schema, migrations, repositories, and the append-only audit log. Never sees plaintext values or key material, values arrive as a branded EncryptedString type.
packages/runnerThe Rust cdylib injection primitive, loaded via Bun.dlopen(). The only code in the repository that spawns a child with decrypted secrets.
packages/redactStreaming output sanitizer. An in-package Aho-Corasick multi-pattern matcher with zero runtime dependencies, applied to everything run-with-secrets returns.

The stack is Bun workspaces, Commander.js for the CLI, WebCrypto plus hash-wasm for Argon2id, a Rust cdylib loaded over Bun.dlopen() FFI, local SQLite with ciphertext at rest, an MCP stdio server on @modelcontextprotocol/sdk, and oxlint with oxfmt. packages/redact scans everything run-with-secrets returns, so a command that echoes a secret to stdout or stderr does not leak it into the agent's context.

The single hardened path

The most important architectural rule. Every execution path that touches a decrypted secret goes through the Rust runner. No exceptions, especially not the MCP path. An MCP tool that spawned a child directly, for example Bun.spawn with the full parent environment spread under the secrets, would bypass all of that hardening and leak the parent environment into an agent chosen process. Vaulted's MCP run-with-secrets calls the exact same FFI function the CLI does.

rust
#[no_mangle]
pub extern "C" fn run_with_secrets(
command_json: *const c_char, // ["program","arg1",...]
secrets_json: *const c_char, // {"KEY":"value",...}
) -> i32 // child exit code, -1 on error

Capture is a first class mode of the same primitive. A second export, run_with_secrets_captured, pipes child stdout and stderr and returns them as Rust allocated, zeroize on free buffers capped at 10 MiB per stream. Both functions share one internal implementation, the only difference is stdio disposition. Making capture part of the hardened primitive itself is what lets the MCP tool return output without ever spawning a child outside the runner.

The invariant is enforced, not aspirational. A source scan test asserts that neither Bun.spawn nor child_process appears in any secret carrying path. Everything the runner returns to an agent passes through packages/redact before truncation, with matches replaced by [REDACTED:KEY_NAME]. The full hardening checklist lives on the Security page and the tool reference on the MCP page.

Design decisions

DecisionWhy
No server, no account. Everything is an encrypted SQLite vault on the machineLocal-first by design
Every path that touches a decrypted secret goes through the Rust runnerA single hardened execution path, the MCP tool included
Aho-Corasick redaction over plain, base64, and URL-encoded forms on the MCP pathNothing returns to an agent with a secret value in it
Single user first, RSA-wrapped project keys kept sharing-readyTeam sharing can land later without re-encrypting the vault
bun:secrets keychain, macOS-first today, same API across platforms as support landsHonest parity, the limitation documented rather than hidden
PreviousSecurity