Your AI agents work. Your secrets stay home.
Local-first, open-source credential firewall for AI agents that lets agents authenticate and use API keys without ever seeing the plaintext secret. Uses the "wisp token" pattern: agents receive opaque placeholders, a local proxy swaps them for real credentials at the network boundary.
# Install (once published)
brew install wispkey
# -- or --
cargo install wispkey
# Create your vault
wispkey init
# Import existing .env credentials
wispkey import .env
# Start the proxy
wispkey serveFour commands from zero to protected. The AI process never touches your real secrets.
[AI Agent] --> "Authorization: Bearer wk_openai_prod_a7x9m2k4"
|
[WispKey Proxy @ localhost:7700]
|
Decrypts real key, swaps it in
|
[OpenAI API] <-- "Authorization: Bearer sk-real-key..."
- You store credentials in an encrypted local vault (AES-256-GCM, Argon2id key derivation)
- WispKey generates wisp tokens (
wk_*) for each credential - AI agents use wisp tokens in their requests
- The proxy intercepts headers, body, and query parameters -- swaps wisp for real, forwards to the target API
- The agent never sees, stores, or can exfiltrate the real secret
- Encrypted local vault -- AES-256-GCM at rest, Argon2id master key derivation, SQLite backend, configurable session timeout (default 30 min), machine-bound encrypted session file by default
- Wisp token proxy -- HTTP forward proxy + blind HTTPS CONNECT tunneling + HTTPS reverse proxy mode (
X-Target-Urlheader) on localhost:7700 - CLI -- Credential lifecycle, project and partition management, encrypted bundle import/export, proxy serving, subprocess/template injection, instance administration, and audit export/tail
- MCP server -- Native integration with Cursor, Claude Code, Windsurf via stdio JSON-RPC, including first-class env-sideloaded credentials for locked-vault use
- Multi-instance access -- Enroll ephemeral VMs or worker instances with per-request identity, least-privilege credential scope, scoped bootstrap-token self-enrollment, cross-platform TCP, Unix, Linux AF_VSOCK, and Firecracker UDS-backed vsock listeners, plus host-approved access escalation
- Rotation-ready instance identities -- Schedule due-aware 48-character CSPRNG secret rotation with a bounded rollout grace window; the previous secret is retired as soon as the new secret first authenticates
- .env importer -- One-command migration with auto-detection of OpenAI, GitHub, Slack, AWS, and bearer token patterns
- Projects -- Top-level credential isolation by team or engagement (
project create,use,current,list,delete) - Partitions -- Logical credential grouping within projects, with encrypted
.wkbundleexport/import (partition create,list,delete,assign,export,import)
- Policy engine -- TOML-defined rules with per-credential, per-host, per-path, per-method restrictions, deny rules, time windows, and sliding-window rate limiting
- Audit log -- Every credential use and denial logged with timestamp, target host/path, method, and status; vault-backed events are queryable by credential and date range, bulk export supports JSONL/JSON for SIEM egress,
audit tail --followstreams without skipping same-timestamp events, and vault-less env sideload use writes a local fallback JSONL audit file - Host restrictions -- Glob-pattern allowlists per credential (e.g.
api.openai.com/*) - Cross-OS local file protection -- Vault directories and sensitive local files are owner-only on Linux/macOS and restricted with Windows ACLs on Windows; generated
.env.wispkeyfiles andvault.dbare written owner-only on Unix - Management API token checks -- The proxy compares management tokens in constant time
- Secret injection for subprocesses and templates --
wispkey exec,wispkey run, andwispkey injectare audited, owner-only plaintext-egress tools that resolve credentials in-process without placing plaintext in argv, parent env, WispKey stdout except explicitinject --stdout, or audit logs - Security model -- The current boundary and intentional limits are documented in
docs/security-model.md
- Browser-based Clerk login --
wispkey cloud loginopens browser, localhost callback captures session token - Tier enforcement -- Personal (free, local-only), Cloud ($1.99/mo), Enterprise (contact)
- Environment sideload -- MCP/proxy processes can receive
WISPKEY_SIDELOAD_{SLUG}env vars and expose them as deterministicwk_env_{slug}tokens. Agents still receive only opaque tokens, and no vault master password is required for this path.
WispKey stores arbitrary encrypted secret values, not only API keys from .env files. Use api_key as the generic opaque secret type for passwords, database URLs, SSH/private-key files, webhook secrets, OAuth tokens, service-account JSON, and anything else that should stay out of the agent process. The type mainly controls how the proxy injects or substitutes the value at request time.
For non-interactive adds, prefer --value-file <path> or --value-file - for stdin. --value still works, but WispKey warns on stderr because command-line arguments can be exposed through shell history and process listings.
| Type | CLI Flag | Injection |
|---|---|---|
| Bearer Token | --type bearer_token |
Authorization: Bearer <value> |
| API Key | --type api_key |
Header or body replacement |
| Basic Auth | --type basic_auth |
Authorization: Basic <base64> |
| Custom Header | --type custom_header --header-name X-Api-Key |
Named header |
| Query Param | --type query_param --param-name key |
URL query parameter |
Examples:
printf '%s' "$DB_PASSWORD" | wispkey add "db-password" --type api_key --value-file - --tags "database"
printf '%s' "$DATABASE_URL" | wispkey add "db-url" --type api_key --value-file - --tags "database"
wispkey add "ssh-private-key" --type api_key --value-file ~/.ssh/id_ed25519 --partition "ssh-keys"
wispkey add "service-account-json" --type api_key --value-file ./service-account.json --tags "gcp"
printf '%s' "$BASIC_AUTH_VALUE" | wispkey add "basic-auth-api" --type basic_auth --value-file - --hosts "api.example.com"For non-HTTP consumers such as sudo, ssh, git, database CLIs, and local tools, wispkey exec resolves a vault credential in-process and injects it only into the child process:
# Stdin channel, useful for commands such as sudo -S.
wispkey exec --credential laptop-password --stdin -- sudo -S -p "" whoami
# Child-only environment variable.
wispkey exec --credential db-password --env DB_PASSWORD -- psql "$DATABASE_URL"
# Askpass helpers for sudo/ssh/git. Use sudo -A so sudo calls SUDO_ASKPASS.
wispkey exec --credential laptop-password --askpass -- sudo -A whoami
wispkey exec --credential git-token --askpass -- git fetchAt least one channel is required: --stdin, --env <VAR>, or --askpass. Channels can be combined. The credential is resolved within the active project, or within --project <name> when provided.
--stdin writes the secret followed by one newline and closes the child's stdin. Commands that also need interactive stdin should use --env or --askpass instead.
exec is a deliberate, owner-only plaintext-egress path for tools that cannot use the WispKey proxy. It is audited with CredentialExec events, but the audit row contains only the credential name, child program name, channel summary, project, and exit status. WispKey does not put the plaintext value in argv, the parent environment, WispKey stdout/stderr, tracing logs, or audit fields. The hidden askpass helper is not a standalone secret oracle: exec --askpass creates a per-exec owner-only handoff file and passes its path through WISPKEY_ASKPASS_HANDOFF; the helper refuses to run without a valid handoff from that child launch.
For tools that need multiple child-only environment variables, wispkey run reads a TOML manifest and resolves every cred:<name> reference before spawning the child:
# wispkey.toml
[env]
OPENAI_API_KEY = "cred:openai-key"
DATABASE_URL = "cred:db-url"
APP_ENV = "development"wispkey run -- npm test
wispkey run --manifest ./secrets/wispkey.toml --project client-alpha -- sh -c 'psql "$DATABASE_URL"'Manifest values without the cred: prefix are passed through as literal child environment values. run fails closed when the manifest is missing, invalid, has no [env] entries, or any referenced credential cannot be resolved. It writes a CredentialRun audit event with the credential names, child program name, project, and exit status.
For config files or templates, wispkey inject replaces {{ cred:<name> }} references and writes the rendered plaintext to an owner-only output file:
wispkey inject -i .env.template -o .env.local
wispkey inject -i config.template --stdout--stdout is an explicit plaintext disclosure to the caller. The safer default is -o <outfile>, which uses WispKey's owner-only file writer. inject writes a CredentialInject audit event with the credential names, output destination, and project.
Configure in Cursor, Claude Code, or any MCP-compatible tool. Keep the command as wispkey so the client uses the normal installed binary from PATH; do not hardcode a user-specific absolute path. Vault-backed credentials use the current WispKey session; run wispkey unlock before starting the client, or set WISPKEY_PASSWORD only for trusted automation.
{
"mcpServers": {
"wispkey": {
"command": "wispkey",
"args": ["mcp", "serve"]
}
}
}For env-sideloaded MCP credentials, pass WISPKEY_SIDELOAD_<SLUG> to the WispKey MCP process instead of passing the vault master password. WispKey lists the env key and returns a wk_env_<slug> token; it never returns the env value. In Codex, use env_vars so Codex forwards the variable from its own environment instead of storing the secret in config:
[mcp_servers.wispkey]
command = "wispkey"
args = ["mcp", "serve"]
env_vars = ["WISPKEY_SIDELOAD_OPENAI"]Start the WispKey proxy with the same WISPKEY_SIDELOAD_<SLUG> env var if you want the proxy to substitute the wk_env_<slug> token in outbound requests. Env sideloads are limited to the trusted local workflow; identity-authenticated instances cannot use them because sideloads have no persisted credential ID that can be enrolled or approved.
For JSON-style MCP configs that do not support env_vars, set the sideload variable in the client process environment or in the MCP server's env block:
{
"mcpServers": {
"wispkey": {
"command": "wispkey",
"args": ["mcp", "serve"],
"env": { "WISPKEY_SIDELOAD_OPENAI": "..." }
}
}
}Treat MCP env blocks as plaintext client config. Prefer process environment forwarding or an OS credential manager when available.
Available MCP tools:
wispkey_list-- List credentials (filter by tag, project)wispkey_get_token-- Get wisp token for a credentialwispkey_proxy_status-- Check vault/session/proxy statewispkey_project_list-- List all projects with partition counts
WispKey supports HTTPS in two ways:
CONNECT tunneling (standard forward proxy) -- the agent sets HTTP_PROXY=http://localhost:7700 and the proxy tunnels the TLS connection. CONNECT is a blind tunnel: the proxy cannot inspect or rewrite headers, bodies, or query strings inside the TLS stream. Use CONNECT only when the request does not need wisp token substitution.
Reverse proxy mode -- use X-Target-Url for explicit HTTPS targeting:
curl -x http://localhost:7700 \
-H "X-Target-Url: https://api.openai.com/v1/chat/completions" \
-H "Authorization: Bearer wk_openai_prod_a7x9m2k4" \
-d '{"model": "gpt-4", "messages": [...]}'Reverse proxy mode substitutes wisp tokens in headers, supported text bodies, and the X-Target-Url query string before forwarding upstream.
WispKey can serve untrusted ephemeral VMs and worker instances without giving them plaintext secrets. The host enrolls each instance, gives it a one-time id and secret plus wk_* tokens, and runs the proxy on one or more listeners:
wispkey instance enroll worker-acme-001 --tag company:acme --credential openai-key
wispkey serve --listen tcp://127.0.0.1:7700 --listen unix:/run/wispkey/proxy.sockFor fleets, the host can mint a scoped bootstrap token and let each VM self-enroll for its own instance id and secret. Successful redemptions are atomic, so TTL and max-use limits are enforced under concurrent joins:
wispkey instance bootstrap create --tag company:acme --ttl 1h --uses 50
printf '%s' "$BOOTSTRAP_TOKEN" | wispkey instance join --token-file - --name worker-acme-001Remote first-contact self-enrollment can use POST /api/instances/join; that endpoint is authenticated by the bootstrap token and does not require a management token or existing instance identity.
Unix domain socket, Linux vsock, Firecracker vsock, and non-loopback TCP listeners require instance identity by default. Loopback TCP keeps the original trusted-local behavior unless --require-identity is set. For Windows or another server, keep WispKey on loopback and reach it through an SSH tunnel, or use an identity-required host-only TCP network. Credential selectors and approvals bind to the resolved credential ID rather than its project-local display name. Out-of-scope vault-token use returns 403 out_of_scope, queues an access request, and can be approved by the host; env-sideload tokens are always out of scope for authenticated instances:
wispkey instance requests --pending
wispkey instance approve req_...Instance secrets can be rotated safely from cron, systemd timers, CI, or Windows Task Scheduler. The command emits a new one-time secret only when rotation is due:
wispkey --format json instance rotate-secret worker-acme-001 \
--if-older-than 30d \
--grace 15mDeliver the JSON result through a protected deployment channel and do not log its stdout. During the grace window both secrets work; the first successful request with the new secret retires the previous secret immediately.
See docs/multi-instance-deployment.md for the deployment model, listener options, and a Firecracker microVM example.
WispKey is not a traditional secrets manager. Traditional vaults are built to deliver plaintext secrets to trusted applications; WispKey is built for agents that should never hold plaintext secrets at all.
- Versus agent credential proxies -- WispKey is a local Rust binary with a policy engine, audit trail, five injection modes, first-class MCP tooling, and env sideload support for locked-vault agent workflows.
- Versus enterprise access platforms -- WispKey does not require a cloud account, sales motion, or hosted control plane for local use. Secrets can stay on the user's machine.
- Versus
.envfiles --.envgives prompt-injectable processes direct access to plaintext. WispKey imports secrets once and gives agents scoped wisp tokens instead.
Security claims are intentionally scoped: CONNECT is a blind tunnel, loopback TCP keeps the trusted-local default, authenticated instance listeners are scoped and fail closed, text-body substitution is limited to text-like content types, and the machine-bound session store does not defend against a same-user process that can read all local WispKey files or inspect memory. See docs/security-model.md for the public security model.
Define credential access rules in ~/.wispkey/policies.toml:
[[policy]]
name = "restrict-production"
credential = "aws-prod"
allowed_methods = ["GET"]
denied_paths = ["/admin/**", "/delete/**"]
allowed_hosts = ["api.aws.com"]
rate_limit = "10/minute"
time_window = "09:00-17:00" # local machine timeManage policies via CLI:
wispkey policy init # Create starter policies.toml
wispkey policy list # Show loaded policies
wispkey policy check # Validate policy fileAgent-scoped policies fail closed when the requester agent identity is unavailable. The proxy does not currently have a trusted agent identity source, so a policy with an agent = "..." scope still applies to proxy requests.
Credentials are isolated by project. Each project contains partitions, which contain credentials.
Each project gets its own personal partition, so partition names are project-scoped.
Credential names are unique within a project, not across the whole vault. The same credential name can exist in different projects. CLI name lookups such as get, remove, and rotate resolve against the active project; API lookups can use an explicit ?project= scope. Existing vaults migrate to schema v10 automatically.
wispkey project create "client-alpha" --description "Client Alpha credentials"
wispkey project use "client-alpha"
wispkey project current
wispkey project list
wispkey project export "client-alpha" --output client-alpha.wkbundle
wispkey project import client-alpha.wkbundle
wispkey list --all-projects
wispkey serve --all-projectsOverride per-terminal with export WISPKEY_PROJECT=client-alpha.
The proxy management API also honors project scope for GET /api/credentials, GET /api/credentials/{name}, DELETE /api/credentials/{name}, GET /api/partitions, and DELETE /api/partitions/{name} by passing ?project=<name>.
| Command | Purpose |
|---|---|
wispkey init |
Create vault and master password |
wispkey unlock |
Unlock vault for the current session |
| `wispkey add [--type TYPE] [--value-file PATH | -] [--hosts H] [--tags T] [--partition P] [--project P]` |
wispkey list [--partition P] [--project P] [--all-projects] |
List credentials |
wispkey get <name> [--show-token] |
Show credential metadata and wisp token |
wispkey remove <name> |
Delete a credential |
wispkey rotate <name> |
Regenerate a wisp token |
wispkey exec --credential <name> [--project P] [--stdin] [--env VAR]... [--askpass] -- <command> [args...] |
Inject a credential into a child process through audited stdin, child-only env, or askpass channels |
wispkey run [--manifest PATH] [--project P] -- <command> [args...] |
Run a child process with manifest-defined child-only environment variables |
| `wispkey inject -i <infile | -> [-o ] [--project P] [--stdout]` |
| `wispkey serve [--port 7700] [--random-port] [--listen SPEC]... [--require-identity | --no-require-identity] [--all-projects] [--daemon]` |
wispkey import <path> [--prefix P] [--partition P] [--project P] |
Import credentials from a .env file |
wispkey status |
Show vault, session, and proxy status |
wispkey log [--last N] [--credential C] [--since DATE] |
Query audit events |
| `wispkey audit export [--since TS] [--until TS] [--credential C] [--encoding jsonl | json] [-o FILE]` |
wispkey audit tail [--follow] [--credential C] |
Stream newest audit events as JSONL; --follow uses a forward (timestamp,id) cursor |
wispkey partition create/list/delete/assign/export/import |
Manage partitions |
wispkey project create/list/delete/use/current/export/import |
Manage projects and encrypted project bundles |
wispkey credential export/import |
Export or import one encrypted credential bundle |
wispkey instance enroll <name> [--description D] [--partition P]... [--project P]... [--credential C]... [--tag T]... |
Enroll a host-managed instance identity |
wispkey instance list/show/scope/revoke/requests/approve/deny |
List, inspect, scope, revoke, and approve or deny instance access requests |
wispkey instance rotate-secret <name> [--if-older-than 30d] [--grace 10m] |
Schedule-safe instance-secret rotation with bounded overlap |
wispkey instance bootstrap create/list/revoke |
Manage scoped, atomic bootstrap tokens for fleet self-enrollment |
| `wispkey instance join [] [--token-file <path | ->] --name ` |
wispkey mcp serve |
Start the MCP server over stdio |
Export and import encrypted credential bundles for sharing or backup:
wispkey partition create "staging" --description "Staging API keys"
wispkey partition assign "my-credential" --partition "staging"
wispkey partition export "staging" --output staging.wkbundle
wispkey partition import staging.wkbundleExports are encrypted with a separate bundle passphrase, not the vault master password. The bundle file contains real secrets after decryption, so share the file and passphrase through different channels. New exports require a 12+ character bundle passphrase.
For non-interactive bundle operations, use WISPKEY_BUNDLE_PASSPHRASE or a protected passphrase file:
export WISPKEY_BUNDLE_PASSPHRASE='a-long-export-passphrase'
wispkey project export "client-alpha" --output client-alpha.wkbundle
wispkey project import client-alpha.wkbundle \
--bundle-passphrase-file ~/.wispkey/client-alpha.bundle-passphraseExport and import one encrypted credential for narrow sharing:
wispkey credential export "openai-key" --output openai-key.wkcred
wispkey credential import openai-key.wkcred --project client-alpha --partition personalSet WISPKEY_PASSWORD to skip interactive prompts:
export WISPKEY_PASSWORD='your-master-password'
wispkey init
wispkey unlock
printf '%s' "$SECRET_VALUE" | wispkey add "key" --type api_key --value-file -For non-interactive secret input, prefer wispkey add "key" --type api_key --value-file ./secret.txt or pipe the value to --value-file -. Passing secrets with --value emits a warning because the value can be captured by shell history or process listings.
WISPKEY_PASSWORD only unlocks or initializes the vault. It is intentionally not used for encrypted bundle export/import; use WISPKEY_BUNDLE_PASSPHRASE or --bundle-passphrase-file for those commands.
wispkey mcp serve does not require WISPKEY_PASSWORD when you only need env-sideloaded credentials. Set WISPKEY_SIDELOAD_<SLUG> in the MCP server environment and ask for credential name <slug> (case and separators are normalized).
Older WISPKEY_FALLBACK_<SLUG> names are not supported. Rename those variables to WISPKEY_SIDELOAD_<SLUG> before upgrading.
src/
core/ # Vault engine (encrypt/decrypt, CRUD, wisp tokens, projects, partitions)
proxy/ # HTTP/HTTPS proxy (tokio + hyper, credential injection, policy eval, env sideload)
mcp/ # MCP server (stdio JSON-RPC transport)
cli/ # CLI interface (clap subcommands)
audit/ # Audit logging (SQLite, credential + time filtering)
migrate/ # .env file importer (auto-detection heuristics)
partition/ # Encrypted bundle export/import (.wkbundle)
secure_files.rs # Cross-platform owner-only local file protection
sharing/ # Project and single-credential encrypted share bundles
cloud/ # Cloud sync client (Clerk browser login, tier enforcement)
policy/ # Policy engine (TOML rules, rate limiting, time windows)
tests/
integration.rs # CLI integration tests
plugin/ # Cursor plugin (rules, skills, hooks, agents)
- Rust 1.94+ via rustup
- SQLite is bundled via
rusqlite-- no system install needed
git clone https://github.com/rankupgames/wispkey.git
cd wispkey
cargo build # Debug build
cargo build --release # Optimized release build
cargo test # Run the test suite
cargo clippy --all-targets --all-features -- -D warnings -W clippy::suspicious -W clippy::style -W clippy::perf -W clippy::complexity
cargo fmt --check # Format check
cargo audit # Dependency advisory check (install with: cargo install cargo-audit --version 0.22.1 --locked)rustup target add x86_64-unknown-linux-gnu
rustup target add aarch64-unknown-linux-gnu
rustup target add x86_64-pc-windows-msvc
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
cargo build --release --target aarch64-apple-darwin| Component | Crate | Purpose |
|---|---|---|
| Async runtime | tokio |
Concurrent proxy connections |
| HTTP proxy | hyper + hyper-rustls |
HTTP/HTTPS request interception and CONNECT tunneling |
| Encryption | ring + argon2 |
AES-256-GCM vault, Argon2id key derivation |
| Database | rusqlite (bundled) |
Zero-config credential store + audit log |
| CLI | clap |
Subcommand parsing with shell completions |
| Serialization | serde + serde_json + toml |
Config, policy, and MCP protocol |
| HTTP client | reqwest |
Cloud API calls (rustls-tls) |
| Logging | tracing |
Structured logging with env filter |
| Patterns | glob-match + regex |
Host restriction globs, wisp token scanning |
| Browser | open |
Clerk login flow (opens default browser) |
- WispKey Cloud (private) -- Cloudflare Worker API for encrypted cloud sync, billing, and team features
- WispKey Desktop -- Tauri + SvelteKit desktop companion app
The open-source CLI works fully offline -- no account needed. WispKey Cloud is an optional companion for encrypted sync and team workflows:
| Tier | Price | What you get |
|---|---|---|
| Personal | Free | Everything in this repo -- local vault, proxy, MCP, plugin |
| Cloud | $1.99/mo ($1.49/mo annual) | Encrypted sync, up to 10 cloud partitions, 100 MB storage |
| Enterprise | Contact us | Unlimited partitions, org management, SSO, dedicated support |
See CONTRIBUTING.md for development workflow and guidelines.
Apache-2.0 -- see LICENSE for details.