keywarden

keywarden

Enables AI agents to make authenticated API calls and run commands with secrets injected, while keeping credentials completely hidden from the model, with policy enforcement, grants, and audit logging.

Category
访问服务器

README

keywarden

Your AI agent can use your API keys. It can never read them.

keywarden is a local, encrypted credential vault that speaks MCP. Claude Code, Claude Desktop, Cursor, or any MCP client connects to it and gets two capabilities: make an authenticated API call, and run a command with credentials in its environment. Neither one ever puts the credential itself into the model's context.

There is no get_secret tool. That absence is the whole product.

Just want to use it? docs/USING.md is the short version: install, the four ways in, and how to wire it into Claude Code, Claude Desktop or your own app. This README is the full reference.

   agent                keywarden                    upstream
     |                     |                          |
     |  "POST /v1/chat     |                          |
     |   using openai/prod"|                          |
     |-------------------->|                          |
     |                     | check policy             |
     |                     | decrypt key              |
     |                     | attach Authorization     |
     |                     |------------------------->|
     |                     |<-------------------------|
     |  response only      | scrub any key from body  |
     |<--------------------| append to audit log      |

Why

Right now the normal way to let an agent use your OpenAI key is to put the key in a .env file and let the agent read it. The moment it does, the key is in a model's context window. From there it is in a provider's logs, possibly in a training set, possibly in a crash report, and definitely in your own transcript history that you will paste into a bug report six months from now.

Rotating a key is annoying. Not knowing whether it leaked is worse.

keywarden removes the step where the model sees the key at all.

Install

npm install -g keywarden-mcp

That gives you two commands: keywarden (the CLI and console) and keywarden-mcp (the MCP server your agent client spawns). The npm package is keywarden-mcp because keywarden was already taken on npm by an unrelated project; the command you type is still keywarden.

Node 20.10 or newer. Two runtime dependencies: the MCP SDK and zod. No native modules, no compiler, no daemon.

Quickstart

keywarden init --passphrase
keywarden add openai/prod --provider openai
keywarden mcp-config

init creates ~/.keywarden/ with an encrypted vault and a deny-by-default policy. add prompts for each field, so nothing lands in your shell history. mcp-config prints the block to paste into your MCP client.

Then, in Claude Code:

Call the OpenAI models endpoint with my prod key and tell me which ones I have access to.

The model calls http_request with ref: "openai/prod". keywarden attaches the key, makes the call, returns the response. Ask it to print the key and it will tell you it cannot.

The tools an agent gets

Tool What it does
list_secrets Metadata only: refs, providers, field names, last used. Never values.
describe_secret One credential plus how it may be used, which hosts, which env vars.
list_providers Built-in presets and what each one expects.
http_request Authenticated HTTPS call. keywarden attaches the credential.
run Spawn a local process with credentials injected as env vars.
audit_tail Recent entries from the tamper-evident log.

Set KEYWARDEN_DISABLE_EXEC=1 to drop run entirely and expose only the HTTP proxy.

Drop it in front of code you already have

The adoption problem with a credential proxy is that using it usually means rewriting how your code calls the API, and nobody rewrites working code for a security property they cannot see. So keywarden presents the provider's shape at a keywarden URL. Point an existing SDK's base_url here, put a keywarden key where the provider key went, change nothing else:

from anthropic import Anthropic

client = Anthropic(
    base_url="http://127.0.0.1:8787/x/anthropic/prod",
    api_key="kw_live_...",          # a keywarden key, not an Anthropic one
)
from openai import OpenAI

client = OpenAI(
    base_url="http://127.0.0.1:8787/x/openai/prod/v1",
    api_key="kw_live_...",
)

Or set the variables most frameworks already read, and point a whole app at keywarden without touching its source:

export ANTHROPIC_BASE_URL=http://127.0.0.1:8787/x/anthropic/prod
export ANTHROPIC_API_KEY=kw_live_...

keywarden shim <ref> prints the exact snippets for a credential, and the console has the same under Drop-in SDK.

What the app gets by doing that: it no longer holds a provider credential, every call is policy-checked and attributed to an agent identity, spend lands in a budget, and the whole thing is revocable in one command.

Streaming works, and streamed calls are still metered. keywarden reads token counts out of the event stream as it passes, so stream=True does not become a hole in your accounting. Redaction is applied over a sliding window, so a secret that straddles a chunk boundary is still caught — that correctness problem was the reason streaming did not exist before.

The shim is a different doorway to the same pipeline, not a bypass. A key without the http capability, a ref outside the key's scope, a path outside policy, and an exhausted budget are all refused exactly as they are on /v1/proxy.

About naming, and environments

A ref is just a name. Environments only matter if you hold more than one key from the same provider

  • a real one and a throwaway for testing. If you hold one key each, skip them and call it anthropic.
keywarden list --env dev     # filters on the part after the slash

That is a view, not a boundary. It changes what you are shown, never what a key may reach. The control that actually restrains something is a scoped key, and that is for agents, which run unattended:

keywarden group set dev-only anthropic/dev openai/dev
keywarden apikey create nightly-triage --ref @dev-only --http   # cannot touch prod, ever

A person gets one key and the console filters what it shows. Making someone hold two keys and remember which is which is how they end up using the broad one for everything.

Least privilege, and the key you lose

The failure that actually happens is not a broken cipher. It is one broad key, handed out because it was convenient, turning up somewhere it should not be.

Name the set, do not glob it

openai/** is easy to write and quietly grows every time you add a credential. If what you meant was "the two an intern may touch", say that:

keywarden group set interns openai/dev anthropic/dev
keywarden apikey create intern-alice --ref @interns --http --owner dinakar
  scope   @interns
  caps    read, http
  reaches 2 of 10 credential(s)

Membership resolves at check time, so adding a credential to the group grants it and removing one revokes it, without reissuing anyone's key. An unknown group expands to nothing — a typo narrows access, never widens it. The groups file is MACed like the vault, because editing it silently widens every key scoped to it.

The tool tells you when a key is too big

keywarden apikey create admin-everything --ref '**' --http --write --reveal --exec --audit

  reaches 10 of 10 credential(s)
  ! this key reaches every credential and carries reveal + write + exec - losing it loses everything
  ! reveal means this key is equivalent to the credentials it covers
  ! write plus reveal is the combination to keep to a console you are looking at
  ! 10 credentials in one key - consider a group with only what it needs

Advice nobody reads is not advice. The number is printed at the moment you create the key, and again in the console beside every key you already have.

Delegation may only narrow

A key can never issue a key more powerful than itself — not a capability it lacks, not a scope wider than its own:

this key does not carry reveal, so it cannot issue a key that does.
Delegation may narrow what a key can do, never widen it.

this key is scoped to [anthropic/**] and cannot issue a key reaching [**].

Without that rule write was the only capability that mattered: a console key with no reveal could mint one that had it, and the gate on reading credentials was a single extra request.

When a key is loose

keywarden panic --group interns     # everything scoped to that group
keywarden panic --owner alice       # everything that person holds
keywarden panic                     # every key, and every live grant

It lists what it is about to revoke and asks first. Over HTTP the same thing is POST /v1/apikeys/revoke with {"scope":"all"|"group"|"owner"}, which by default spares the key you are holding so you are not locked out mid-incident.

Revocation takes effect on the next request, including on servers already running — it does not wait for a restart. And keywarden says the thing people forget:

Revoking a key does not rotate the credentials it could reach. If you believe it was used, rotate them — and keywarden audit tail shows exactly what it touched.

Agents are the identity, not people

Most keywarden keys are not developers. They are agents, and an agent is a non-human identity that needs exactly what a person needs: an owner, a scope, a bill, and a name in the log. Non-human identities already outnumber human ones in most estates; giving each agent its own key is what makes anything downstream answerable.

keywarden apikey create triage-bot   --ref 'anthropic/**' --http   --agent triage-bot --owner dinakar --project support --ttl 30d

keywarden apikey create research-bot --ref 'anthropic/**' --http   --agent research-bot --owner dinakar --project growth --ttl 30d
AGENT           KIND   OWNER    PROJECT  RUNS  CALLS  TOKENS   COST
research-bot    agent  dinakar  growth      1      2     334  $0.0077
triage-bot      agent  dinakar  support     1      3      39  $0.0004

A run is the unit, not a request

A person makes a call. An agent makes forty, on its own, and "which credential was used at 03:14" is not a useful question — "what did that run touch, and what did it cost" is.

Over MCP this is free. An MCP server is spawned by one client for one session and exits with it, so the process is the run. keywarden stamps a session id at boot and every call inherits it: no header, no SDK, no instrumentation on the agent's side. Over HTTP a framework sends X-Keywarden-Session and X-Keywarden-Agent.

session=run-research-001  agent=research-bot  calls=2  12/min  api.anthropic.com  $0.0077
session=run-triage-001    agent=triage-bot    calls=3  18/min  api.anthropic.com  $0.0004

Each run records the credentials it used and the hosts it reached, which is the question a security review actually asks.

Runs that look wrong

A run making 30+ calls a minute over 20+ calls is flagged high-rate — the shape of a loop nobody is watching. Five or more denials is flagged repeated-denials — the shape of an agent probing for something it is not allowed to have. Both are plain thresholds on purpose: a heuristic you can explain is one you will act on, and a block budget is what actually stops it.

Who spent what

Provider dashboards tell you what an organisation spent, not who spent it. That gap is well documented: project keys separate workloads coarsely, the usage API returns aggregates, and teams find out after the fact that a couple of abandoned agent loops accounted for most of the bill.

keywarden is already in the request path with an actor on every call, so attribution is free. Give each person or service its own keywarden key and the answer falls out:

keywarden apikey create alice --ref 'openai/**' --http --ttl 90d
keywarden apikey create bob   --ref 'openai/**' --http --ttl 90d
ACTOR                CALLS         IN        OUT      COST
http:alice             412    418,220     96,410    $12.84
http:bob                38     92,004     31,887     $3.11

Rates are yours, not ours

keywarden ships no price list. Prices change, differ by contract and region, and a stale hardcoded number produces a confident wrong figure in a finance report. You enter the rates you are actually on, each records when you last checked it, and anything over 90 days old is flagged as stale. A model with no rate is counted but never guessed at.

Budgets that can actually stop something

scope   everything | one API key (a person, a service) | one credential glob
period  day | week | month
action  warn  - records and notifies
        block - refuses with HTTP 402 before the credential is touched
alerts  50% / 80% / 95% by default, fired once per period

A block budget is the difference between finding out at 3am and finding out at the end of the month. Alerts POST to any https webhook — n8n, Zapier, Slack — which is how email happens without keywarden growing an SMTP client.

The console

keywarden ui

Starts the agent, mints a console key that expires on its own, and opens a browser at 127.0.0.1:8787. One self-contained page, no build step, no external requests.

What it does:

  • Credentials — add, import, search, filter by provider and environment. It shows field names, last-used, and which hosts each credential may reach.
  • How to use — the panel that opens after you add a key: a prompt to paste into your agent, a curl command, the environment variables injection sets, and a Test it button that makes a real call so you can see it work.
  • Approvals — a rule with requireGrant denies until a human says yes. Those denials land here with exactly what was attempted; approving issues a grant scoped to that request and nothing wider.
  • Activity — every decision with the actor that caused it, and a live check that the hash chain is intact.
  • Usage — token totals by credential, actor and model.
  • Grants, API keys, Policy — issue, revoke, edit.

Everything stays in step. The CLI, your agent over MCP, and the console all write the same files, so a credential added in a terminal appears in an open console within a few seconds, and a revocation takes effect on the next request rather than the next restart.

Importing what you already have

Nobody starts from zero, they start from a .env with eleven keys in it. Paste or drop one into the console and variable names are matched against known providers, so OPENAI_API_KEY becomes an openai credential without being told. Parsing happens in the page; nothing reaches even the local agent until you have seen what it found.

Any API, without editing JSON

The built-in presets cover the APIs everyone has. They do not cover Attio, your internal service, or Azure OpenAI where the host is your own resource name — and being made to pick "generic bearer" then hand-edit a config file is the friction that sends people back to a .env.

keywarden add attio/prod --url https://api.attio.com

That infers the provider id, the environment variable (ATTIO_API_KEY), and the host allowlist (api.attio.com) from the URL, then prompts for the value. Override any of it:

keywarden add azure/prod --url https://my-resource.openai.azure.com \
  --env AZURE_OPENAI_API_KEY --auth-header api-key

The result is a real provider with a real egress allowlist, not a wildcard escape hatch — a wildcard host is refused, because it would defeat the control that stops a prompt injection mailing your key somewhere.

Reveal, rotation and version history

The promise is "your agent cannot read it" — not "nobody can ever read it". You are the operator; keywarden reveal has always existed at the terminal. The console can do the same, but only when its key carries a dedicated reveal capability:

keywarden ui --allow-reveal                       # console key that can reveal
keywarden apikey create ops --ref '**' --reveal   # or grant it explicitly

reveal is off by default, is never implied by any other capability, and every use is written to the audit log with the value's fingerprint — so the log proves which value was disclosed without recording it. It exists on the HTTP surface only. There is no MCP tool that returns a credential, with or without this flag, and a test asserts that the agent-facing tool list contains no such route.

Rotation keeps the old value. Paste a new key into Rotate / edit, and the previous ciphertext becomes a retained version rather than being overwritten. That is the moment things usually break: you rotate, something downstream was still on the old key, and now you have neither.

keywarden history anthropic/prod                  # what is retained
keywarden history anthropic/prod --rollback 2     # put version 2 back

Rollback moves forward — the current values are pushed onto the history and the restored set becomes a new version — so undoing a bad rollback is the same command again, and the audit log reads as a sequence of changes rather than a gap. Ten versions are kept per credential; superseded values are encrypted exactly like the current one and covered by the same vault MAC.

Concealed by default

Values are concealed until you ask, and a console key without reveal cannot ask at all — it sees field names and usage snippets, nothing else. That is the sensible default for a tab left open on a laptop.

What never changes is the agent's view. list_secrets and describe_secret return metadata; http_request and run use a credential without returning it. There is no fifth tool.

Serving a page next to a secrets API

Binding to 127.0.0.1 keeps the network out. It does not keep the browser out: any site you visit can make your browser issue requests to a loopback port, and the same-origin policy only stops it reading the reply, not causing the effect. So every request is checked for provenance before anything happens:

  • Origin must be the console's own, which blocks the drive-by case outright.
  • Host must be a literal loopback address, which is what makes DNS rebinding fail even after the name flips to 127.0.0.1.
  • The page itself ships under a CSP of default-src 'none' with frame-ancestors 'none', and loads nothing remote at all.

The console key lives in sessionStorage for that tab only, is scoped and expiring like any other keywarden key, and is revocable in one command.

Three surfaces, one authorisation model

The same vault, policy engine, grants and audit log are reachable three ways. Which one you use changes nothing about what is allowed.

surface for how the caller is identified
MCP (stdio) Claude Code, Claude Desktop, Cursor the client that spawned the server
CLI you, at a terminal filesystem access to the vault
HTTP (loopback) any language, CI, a script, the console a scoped keywarden API key

The HTTP surface is what makes keywarden usable from code that does not speak MCP, and it is the first place keywarden can tell one caller from another:

keywarden apikey create ci-runner --ref 'openai/**' --http --audit --ttl 30d
keywarden serve --port 8787
curl -s http://127.0.0.1:8787/v1/proxy/openai%2Fprod \
  -H "Authorization: Bearer kw_live_..." \
  -H "content-type: application/json" \
  -d '{"method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-4o","messages":[]}}'

The caller holds a keywarden key scoped to openai/**, carrying only the capabilities it was granted, expiring in 30 days, revocable in one command. It never holds the OpenAI key. Routes: /v1/secrets, /v1/secrets/:ref, /v1/proxy/:ref, /v1/run, /v1/audit, /v1/usage, /v1/whoami, /healthz.

The server binds 127.0.0.1 and refuses a routable interface without --allow-remote, because anyone who can reach that port gets an authorisation oracle for every credential the key covers.

Who used what, and what it cost

Every entry in the audit log names an actor, and the actor is inside the hash, so attribution cannot be rewritten without breaking the chain. Every proxied response is parsed for provider-reported token counts.

keywarden usage --since 7d
CREDENTIAL        CALLS          IN         OUT       TOTAL
openai/prod         142     418,220      96,410     514,630
anthropic/prod       38      92,004      31,887     123,891

ACTOR             CALLS          IN         OUT       TOTAL
http:ci-runner      118     356,900      74,220     431,120
mcp:mcp-client       62     153,324      54,077     207,401

keywarden records tokens, not money. Prices change, differ by contract, and a stale hardcoded rate produces a confident wrong number in a finance report. Note that run cannot be metered: once a credential is inside a subprocess, keywarden sees an exit code, not a token count.

Two ways to use a credential

Proxy, for HTTP APIs. The agent describes a request, keywarden attaches the credential and makes the call. Works for OpenAI, Anthropic, Stripe, GitHub, Slack, Cloudflare, Vercel, Supabase, and any API that authenticates with a header or a query parameter.

// what the agent sends
{ "ref": "openai/prod", "method": "POST", "url": "/v1/chat/completions", "body": { "model": "gpt-4o", "messages": [] } }

Inject, for everything else. AWS needs SigV4 request signing, a Postgres URL is not HTTP at all, and terraform apply wants real environment variables. keywarden spawns the process itself:

{ "command": "aws", "args": ["s3", "ls"], "inject": ["aws/prod"] }

The child process gets AWS_ACCESS_KEY_ID and friends. The model gets stdout, with any credential appearing in it masked on the way out.

Policy

~/.keywarden/policy.json decides which credential may be used, by which capability, against what. Rules are evaluated top to bottom, first match wins, and the default is deny.

{
  "version": 1,
  "default": "deny",
  "redactResponses": true,
  "rules": [
    {
      "ref": "openai/**",
      "http": { "allow": true, "methods": ["POST"], "paths": ["/v1/**"] },
      "exec": { "allow": false, "commands": [] },
      "rateLimitPerMinute": 30
    },
    {
      "ref": "aws/prod",
      "http": { "allow": false },
      "exec": { "allow": true, "commands": ["aws", "terraform"] },
      "rateLimitPerMinute": 10,
      "expiresAt": "2026-12-31T00:00:00.000Z"
    }
  ]
}

Or from the CLI:

keywarden policy allow "openai/**" --http --path "/v1/**" --method POST
keywarden policy allow aws/prod --exec aws --exec terraform --arg-deny "s3://*"
keywarden policy test aws/prod exec terraform

* matches inside one path segment, ** spans segments. expiresAt makes a rule temporary.

Naming a command is not enough on its own. Allowlist aws for aws s3 ls and the same binary does aws s3 cp into a bucket someone else owns. That is the sequence-level gap the MCP threat literature keeps pointing at: every individual call is authorised, and the combination is the exfiltration. So rules constrain arguments too:

"exec": {
  "allow": true,
  "commands": ["aws"],
  "argsDeny": ["s3://*", "--endpoint-url"],   // any match refuses the call
  "argsAllow": ["s3", "ls", "--region", "*"]  // if set, every argument must match
}

Grants: temporary, expiring, use-capped access

Policy is standing configuration. It is the wrong shape for "let the agent do this one thing, now, for fifteen minutes", which today means widening a rule and forgetting to narrow it again.

A grant is a capability that carries its own restrictions, borrowed from the macaroon and biscuit line of work, and issued by you at a terminal:

keywarden grant aws/prod --exec aws --ttl 15m --uses 5 --arg-deny "s3://*"
keywarden grant openai/prod --http --path "/v1/chat/**" --method POST --ttl 1h --uses 20
keywarden grant list
keywarden grant revoke <id>

Grants expire on their own, die when their use budget runs out, and are HMACed with a key derived from your vault, so a hand-edited grants.json is rejected rather than honoured. A denied attempt does not burn a use.

Set "requireGrant": true on a policy rule and standing configuration becomes necessary but not sufficient: nothing happens until you issue a grant. That is the human-in-the-loop approval step, without needing an interactive prompt inside a stdio server.

policy grant result
allows, no requireGrant — allow
allows, requireGrant live match allow
allows, requireGrant none deny
denies live match allow
denies none deny

Config integrity

Encrypting the secrets is half the job. policy.json decides whether a credential may be used and providers.json decides where it is sent. Both are plain files. Someone who cannot decrypt a single byte can still add a provider whose hosts are theirs and repoint your credential at it.

So the vault pins a hash of both files, and refuses to act on either until you have looked at the change:

keywarden trust show    # what drifted
keywarden trust         # review, then pin the current contents

The vault file itself is MACed as a whole, not just per-field, because flipping provider: "openai" to something else never touches a ciphertext and would otherwise verify cleanly.

What keywarden actually enforces

  • No plaintext tool. The MCP surface has no code path that returns a credential value.
  • Egress allowlist. A credential can only be sent to hosts its provider declares, plus any you add in policy. A prompt injection telling the agent to POST your key to attacker.example fails at the host check, before the network is touched.
  • HTTPS only, no redirect following. A 302 to another origin will not replay your Authorization header off-host.
  • SSRF guard. Loopback, private ranges, CGNAT, and link-local (which covers the 169.254.169.254 cloud metadata endpoint) are blocked, and the address is validated in the DNS lookup that the socket actually uses, so DNS rebinding does not open a window.
  • No shell. run passes an argv array to spawn with shell: false. There is no metacharacter parsing to inject into.
  • Constructed child environment. The child gets an allowlist of inherited variables plus the injected ones. Your other secrets, and keywarden's own passphrase, are not inherited.
  • Output redaction. Every tool result is scanned for known credential values, their base64 and URL-encoded forms, and about a dozen well-known key shapes. Defence in depth, not the primary control.
  • Tamper-evident audit. Every decision, allow or deny, is appended to a hash-chained log. keywarden audit verify recomputes the chain and reports the first modified or deleted entry.
  • Whole-file integrity. The vault is MACed including its metadata, so a credential cannot be repointed at another provider without detection. policy.json and providers.json are hash-pinned to the vault and refused when they change out of band.
  • Environment hardening. The server refuses to start when NODE_TLS_REJECT_UNAUTHORIZED=0, NODE_OPTIONS, or SSLKEYLOGFILE are set, and warns on NODE_EXTRA_CA_CERTS and HTTPS_PROXY. CVE-2026-21852 against Claude Code was one environment override that redirected outbound traffic with the Authorization header attached; a process whose job is attaching credentials must not start when the request path is under someone else's control.
  • Argument constraints. argsAllow / argsDeny narrow which invocations of an allowlisted command are permitted, not just which binary.
  • Attenuated grants. Expiring, use-capped, operator-issued capabilities, forgery-resistant via a vault-derived MAC.
  • Untrusted-data framing. Proxied response bodies are labelled as untrusted content from a named host, so an injected instruction in an API response is presented to the model as data.

The one file that is not ours

The console vendors GSAP 3.15.0 inline. It is not fetched: this page loads nothing from the network, and a console that displays credential metadata should not start making requests to a CDN.

It is pinned so it stays auditable. The header above the block records the version, byte count and sha256, and npm test recomputes that digest - edit the vendored code and the suite fails.

GSAP ships under its own standard "no charge" licence (https://gsap.com/standard-license), not keywarden's MIT. If you redistribute this, that licence travels with it.

To re-vendor after an upgrade:

npm install --no-save gsap
node -e "console.log(require('crypto').createHash('sha256').update(require('fs').readFileSync('node_modules/gsap/dist/gsap.min.js')).digest('hex'))"

Replace the block in web/ui.html, update the digest in its header, and run the tests.

Crypto

Envelope encryption, all from node:crypto, no third-party crypto libraries.

  • A random 256-bit data key encrypts each field with AES-256-GCM, with the credential's ref and field name as additional authenticated data, so a ciphertext cannot be moved between vault entries.
  • The data key is wrapped by a key derived from your passphrase with scrypt at N=2^17, r=8, about 128 MiB and roughly a second per attempt. That is deliberate: the vault file is what an attacker walks off with, so offline guessing has to be expensive.
  • Rotating your passphrase rewraps 32 bytes. It does not re-encrypt every secret.

Vault modes

--passphrase is the strong one. The MCP server needs KEYWARDEN_PASSPHRASE in its environment to unlock without a prompt.

--keyfile writes a random key to ~/.keywarden/masterkey so nothing has to prompt. It is convenient, and it means anyone who can read your home directory can open the vault. It is still far better than plaintext .env files scattered across projects, because the key is in one place, its use is policy-gated, and every use is logged. Know which trade you made. keywarden doctor will remind you.

On Windows, file modes are set but not enforced the way POSIX enforces 0600. See THREAT_MODEL.md.

CLI

keywarden init --passphrase|--keyfile   create the vault
keywarden doctor                        check the install, flag weak settings
keywarden trust [show]                  re-pin policy.json + providers.json after reviewing a change
keywarden grant <ref> ...               issue a temporary, use-capped capability
keywarden grant list | revoke <id>
keywarden add <ref> --provider <id>     store a credential (prompts for each field)
keywarden list                          metadata only
keywarden describe <ref>                metadata plus how it can be used
keywarden reveal <ref>                  print plaintext, asks first, always audited
keywarden history <ref> [--rollback N]  version history, and undo a rotation
keywarden rm <ref> [--field f]          delete
keywarden exec <ref[,ref]> -- <cmd>     run a command with credentials injected
keywarden policy show|init|allow|deny|test
keywarden audit [tail|verify]
keywarden passphrase                    rotate
keywarden providers                     built-in presets
keywarden mcp-config                    print the MCP client config
keywarden doctor                        check the install, flag weak settings

Custom providers

Anything not built in goes in ~/.keywarden/providers.json. See docs/PROVIDERS.md.

{
  "acme": {
    "label": "Acme Internal API",
    "hosts": ["api.acme.internal", "*.acme.io"],
    "baseUrl": "https://api.acme.io",
    "fields": ["token", "tenant"],
    "required": ["token"],
    "auth": { "type": "header", "name": "X-Acme-Key", "template": "{{token}}" },
    "env": { "ACME_TOKEN": "{{token}}", "ACME_TENANT": "{{tenant}}" }
  }
}

What keywarden does not protect you from

Read THREAT_MODEL.md before you trust it with anything expensive. The short version:

  • If the agent can run arbitrary local commands through some other tool, it can read your vault file and, in keyfile mode, your master key. keywarden protects the model's context, not your disk.
  • run gives the credential to a real process. If you allowlist a command that can be steered into exfiltrating its own environment, the credential leaves. Allowlist narrowly.
  • Redaction is a safety net with holes. A credential that an API returns re-encoded in a way we do not recognise will not be caught.
  • keywarden does not stop an agent from doing something expensive or destructive with a credential it is legitimately allowed to use. That is what policy scoping and rate limits are for.

Development

npm install
npm run build
npm test          # 194 unit tests + 53 end-to-end checks against the real CLI, MCP and HTTP servers

The e2e suite drives the actual binaries in a throwaway KEYWARDEN_HOME and asserts, among other things, that no tool response contains the credential.

Reading

  • THREAT_MODEL.md — what is in scope, and what is honestly not
  • docs/RESEARCH.md — the 2026 literature this design is drawn from, what was adopted, and what was considered and rejected
  • docs/COMPETITORS.md — the landscape, and where keywarden is genuinely different rather than just differently marketed
  • docs/TEAM.md — the multi-developer architecture: identity, key sharing without a readable server, approval workflow, cost accounting, and the build order
  • docs/PROVIDERS.md — writing a custom provider

Hosted

A hosted version is planned for people who want a team vault, browser-based management, and sync across machines, with the same zero-exposure guarantee. Everything in this repository stays MIT and stays fully usable standalone. See docs/HOSTED.md.

License

MIT

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选