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. The design deliberately mirrors Relic, the MIT licensed manager at heycupola/relic analyzed source-true, on every axis where Relic is strong, replaces its server dependency with a local encrypted store, and closes the containment gaps its MCP surface shipped with.

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 the same shape as Relic's, minus the SaaS layer. 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 instead of Convex, an MCP stdio server on @modelcontextprotocol/sdk, and oxlint with oxfmt. packages/redact has no Relic equivalent. It exists because of Relic's confirmed gap, their MCP tool returns child stdout and stderr verbatim, so any command that echoes a secret leaks it straight into the agent's context.

The single hardened path

The most important architectural rule, learned from Relic's confirmed mistake. Every execution path that touches a decrypted secret goes through the Rust runner. No exceptions, especially not the MCP path. Relic's MCP tool calls Bun.spawn with the full parent environment spread under the secrets, bypassing all of its own hardening and leaking 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. Relic's MCP tool bypassed its runner precisely because it needed captured output, so Vaulted closes that hole by making capture part of the hardened primitive itself.

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.

Deliberate divergences from Relic

RelicVaultedWhy
Convex server, accounts, billingNo server, no account, SQLiteLocal-first is the thesis
MCP run tool bypasses the Rust runnerSingle hardened execution pathTheir confirmed containment gap
No output redactionAho-Corasick redaction on the MCP pathTheir confirmed leak channel
Team sharing, per-member key wrappingSingle user first, hierarchy kept sharing-readyScope control for v0.3
bun:secrets macOS-first keychainSame API, same limitation, documentedHonest parity, cross-platform later
PreviousSecurity