Skip to main content
Glama
DINAKAR-S

keywarden

by DINAKAR-S
README.md
# keywarden

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

keywarden is a local, encrypted credential vault that speaks [MCP](https://modelcontextprotocol.io).
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](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.

```
   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

```bash
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

```bash
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:

```python
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
)
```

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

```bash
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**. Streaming works and streamed calls are still metered — keywarden reads token
counts out of the event stream as it passes, with redaction applied over a sliding window so a
secret straddling a chunk boundary is still caught.

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`.

```bash
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.

## What keywarden does not protect you from

Read [THREAT_MODEL.md](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.

---

## Reference

Everything below is reference material for when you need it. Click a heading to expand.

<details>
<summary><b>Least privilege — scoped keys, groups, and the key you lose</b></summary>

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:

```bash
keywarden group set interns openai/dev anthropic/dev
keywarden apikey create intern-alice --ref @interns --http --owner dinakar
```

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. 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

```bash
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
```

Revocation takes effect on the next request, including on servers already running. 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.

</details>

<details>
<summary><b>Agents are the identity, not people</b></summary>

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.

```bash
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
```

**A run is the unit, not a request.** An agent makes forty calls 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 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. Over HTTP a
framework sends `X-Keywarden-Session` and `X-Keywarden-Agent`.

A run making 30+ calls a minute over 20+ calls is flagged `high-rate`. Five or more denials is
flagged `repeated-denials`. Both are plain thresholds on purpose: a heuristic you can explain is
one you will act on.

</details>

<details>
<summary><b>Who spent what — attribution and budgets</b></summary>

Provider dashboards tell you what an organisation spent, not who spent it. keywarden is already in
the request path with an actor on every call, so attribution is free.

```
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 on, each records when you last checked it, and anything over 90 days old is
flagged as stale.

**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.

</details>

<details>
<summary><b>The console (browser UI)</b></summary>

```bash
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.

- **Credentials** — add, import, search, filter. Shows field *names*, last-used, allowed hosts.
- **Copy test prompt** — every credential row has a button that copies a ready-to-paste prompt for
  your AI agent, so a key you just added can be tested in one paste. A "Provider test prompts"
  panel at the bottom lists every template for reference.
- **"Not sure" flow** — the Add-credential picker starts with a "Not sure - help me figure it out"
  option that generates a prompt for your agent to identify the right provider preset for a service
  it does not recognise, without ever seeing your key value.
- **How to use** — the panel that opens after adding: a prompt to paste into your agent, a `curl`
  command, the env vars injection sets, and a **Test it** button that makes a real call.
- **Approvals** — a rule with `requireGrant` denies until a human says yes. 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 on the hash chain.
- **Usage** — token totals by credential, actor and model.
- **Grants, API keys, Policy** — issue, revoke, edit.

**Importing what you already have.** Paste or drop a `.env` into the console. Variable names are
matched against known providers, so `OPENAI_API_KEY` becomes an `openai` credential. Parsing
happens in the page; nothing reaches even the local agent until you have seen what it found.

**Any API, without editing JSON.**

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

Infers the provider id, env var (`ATTIO_API_KEY`), and host allowlist from the URL, then prompts
for the value. A wildcard host is refused because it would defeat the control that stops a prompt
injection mailing your key somewhere.

**Reveal, rotation and history.** The promise is "your agent cannot read it", not "nobody can ever
read it". `keywarden reveal` has always existed at the terminal; the console can do the same only
when its key carries `reveal`. `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. It exists on
the HTTP surface only — no MCP tool returns a credential, ever.

Rotation keeps the old value. `keywarden history <ref>` lists retained versions; `--rollback N`
restores one. Ten versions are kept per credential.

**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. So every request is checked for Origin, Host must be a literal loopback address (blocks DNS
rebinding), and the page ships under a CSP of `default-src 'none'` loading nothing remote.

</details>

<details>
<summary><b>Three surfaces, one authorisation model</b></summary>

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:

```bash
keywarden apikey create ci-runner --ref 'openai/**' --http --audit --ttl 30d
keywarden serve --port 8787
```

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`.

</details>

<details>
<summary><b>Two ways to use a credential — proxy vs inject</b></summary>

**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.

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

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

```jsonc
{ "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.

</details>

<details>
<summary><b>Policy — deny-by-default rules, argument constraints</b></summary>

`~/.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.

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

Or from the CLI: `keywarden policy allow "openai/**" --http --path "/v1/**" --method POST`.
`*` 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. Rules constrain arguments too:

```jsonc
"exec": {
  "allow": true,
  "commands": ["aws"],
  "argsDeny": ["s3://*", "--endpoint-url"],
  "argsAllow": ["s3", "ls", "--region", "*"]
}
```

</details>

<details>
<summary><b>Grants — temporary, expiring, use-capped access</b></summary>

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.

```bash
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. A denied attempt does not burn a use.

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

</details>

<details>
<summary><b>Config integrity — keywarden trust</b></summary>

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:

```bash
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.

</details>

<details>
<summary><b>What keywarden actually enforces (checklist)</b></summary>

- **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.
- **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. Address is validated in the DNS lookup
  the socket actually uses, so DNS rebinding does not open a window.
- **No shell.** `run` passes an argv array to `spawn` with `shell: false`.
- **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.
- **Tamper-evident audit.** Every decision, allow or deny, is appended to a hash-chained log.
- **Whole-file integrity.** The vault is MACed including metadata, so a credential cannot be
  repointed at another provider without detection.
- **Environment hardening.** The server refuses to start when `NODE_TLS_REJECT_UNAUTHORIZED=0`,
  `NODE_OPTIONS`, or `SSLKEYLOGFILE` are set. 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.

</details>

<details>
<summary><b>Crypto and vault modes</b></summary>

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.
- 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).
- Rotating your passphrase rewraps 32 bytes. It does not re-encrypt every secret.

`--passphrase` is the strong mode. 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. Still far
better than plaintext `.env` files, because the key is in one place, its use is policy-gated, and
every use is logged. On Windows, file modes are set but not enforced the way POSIX enforces
`0600`. See [THREAT_MODEL.md](THREAT_MODEL.md).

</details>

<details>
<summary><b>CLI reference</b></summary>

```
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 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 add <ref> --url <base>        add against any HTTP API without editing providers.json
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 ui                            open the browser console
```

</details>

<details>
<summary><b>Custom providers</b></summary>

Anything not built in goes in `~/.keywarden/providers.json`. See
[docs/PROVIDERS.md](docs/PROVIDERS.md).

```json
{
  "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}}" }
  }
}
```

</details>

---

## Reading

- [THREAT_MODEL.md](THREAT_MODEL.md) — what is in scope, and what is honestly not
- [docs/USING.md](docs/USING.md) — install, four ways in, MCP client wiring
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — writing a custom provider
- [docs/RESEARCH.md](docs/RESEARCH.md) — the 2026 literature this design draws from
- [docs/COMPETITORS.md](docs/COMPETITORS.md) — landscape, and where keywarden is actually different
- [docs/TEAM.md](docs/TEAM.md) — multi-developer architecture
- [docs/HOSTED.md](docs/HOSTED.md) — hosted version plans
- [CONTRIBUTING.md](CONTRIBUTING.md) — building from source, running tests

## 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](docs/HOSTED.md).

## License

MIT

TDQS

A3.9/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a clearly distinct concern: auditing, listing all secrets, inspecting one secret, listing provider presets, making an HTTP request, and running a local command. Even http_request and run, which both consume credentials, are clearly separated by remote HTTP vs local execution.

Naming Consistency3/5

list_secrets, list_providers, and describe_secret follow a readable verb_noun pattern, but audit_tail, http_request, and run break it: audit_tail is noun-first, http_request is a noun phrase, and run is a bare verb. The set is understandable but mixes conventions.

Tool Count5/5

Six tools is a well-scoped size for a credential vault/usage server. Each tool covers a meaningful part of the workflow without redundancy or bloat.

Completeness4/5

The server covers the core credential-usage workflow well: list, describe, use over HTTP, use locally, and audit past use. Credential creation/update/delete is absent, but the descriptions suggest new credentials may be added by the user outside the tool set, so this is a minor workaround gap rather than a fatal one.

Maintenance

ActivityMaintained
ResponsivenessNo issues