Content
# Agent Vault
> Give your AI agent the ability to *use* secrets without ever letting it *see* them.
[](https://github.com/mvplab-ai/agent-vault/actions/workflows/ci.yml)
[](LICENSE)
[](https://www.python.org/downloads/)
<p align="center">
<img src="docs/demo.gif" alt="agent-vault demo: init the vault, add a secret at a hidden prompt, list without values, the whole vault encrypted on disk, a tool's output scrubbed of the secret, and an audit log" width="900">
</p>
Agent Vault is a local credential vault for AI agents. Your agent references a
secret **by name** (e.g. `stripe_key`) and never sees its value: the vault
injects the real credential outside the model's context, enforces per-secret
rules about which hosts each secret may travel to, scrubs secret values out of
everything the agent reads back, and writes an append-only audit log of every
use.
Think of it as a **hotel front desk** — the agent is a guest who can ask for
doors to be opened but never holds the master key.
```text
AI agent ──"use vault:stripe_key on api.stripe.com"──▶ Agent Vault
(sees only names) │
├─ inject real value
├─ check host allowlist
├─ scrub the response
└─ append to audit log
```
## Table of contents
- [Why](#why)
- [Features](#features)
- [Install](#install)
- [Quick start](#quick-start)
- [How it works](#how-it-works)
- [Agent setup (MCP)](#agent-setup-mcp)
- [Security model](#security-model)
- [Audit log](#audit-log)
- [Environment variables](#environment-variables)
- [Project layout](#project-layout)
- [Contributing](#contributing)
- [Security policy](#security-policy)
- [License](#license)
## Why
To do real work, agents need credentials — a Stripe key, a GitHub token, a
database password. The naive approach is to paste those secrets into the
agent's context. That is dangerous:
- The secret ends up in the model's conversation, which is sent to a provider
and often stored in logs and transcripts.
- A prompt-injection payload in a web page or document the agent reads can
trick it into exfiltrating the key.
- Once a secret is in the chat history, you cannot un-expose it.
Agent Vault keeps the real values in a locked box on your machine. The agent
operates on secrets **by name**, and a normal process — outside the model —
plugs in the real value, does the thing, scrubs the value from anything the
agent sees, and records it.
## Features
- **Reference by name.** Agents use `stripe_key`; they never receive the value.
- **Two doors, one core.** An MCP server for agent-native API calls and a
`run` command wrapper for any CLI tool.
- **Per-secret host allowlist.** A secret can only be sent to the hosts you
approve — a hijacked agent can't `POST` your key to `evil.com`.
- **Output scrubbing.** Secret values (and their base64 / percent-encoded
forms) are replaced with `{{vault:NAME}}` before the agent sees them.
- **Whole-vault authenticated encryption.** Values *and* metadata (including
each secret's allowlist) are encrypted and MAC'd with Fernet, so on-disk
tampering is detected and rejected.
- **Append-only audit log.** Every injection, request, and denial is recorded —
names and hosts, never values.
- **Local-only.** No cloud, no sync, no telemetry. The master key lives in your
OS keychain.
## Install
```bash
uv tool install agent-vault
# or
pipx install agent-vault
```
For development against a clone of this repo:
```bash
uv sync
```
## Quick start
```bash
# Create the vault and store the master key in your OS keychain.
agent-vault init
# Add a secret. You're prompted for the value with hidden input;
# --host limits where the secret may be sent over MCP HTTP.
agent-vault add stripe_key --host api.stripe.com
# List secret names and metadata — never values.
agent-vault list
# Run a tool with a secret injected as an environment variable.
agent-vault run --env GITHUB_TOKEN -- gh pr list
# Or interpolate a secret into command arguments with a {{vault:NAME}}
# placeholder. The value is substituted before exec and scrubbed from output.
agent-vault run -- curl -H "Authorization: Bearer {{vault:stripe_key}}" https://api.stripe.com/v1/charges
```
`{{vault:NAME}}` placeholders in the command are replaced with the real value
just before the child process is exec'd, and every stored secret value is
scrubbed back to `{{vault:NAME}}` in the child's stdout and stderr before you
(or your agent) see it. `agent-vault run` exits with the child's own exit code.
## How it works
Agent Vault is one **core** exposed through two thin frontends, so it covers
both things agents actually do — call APIs and run shell tools.
```text
┌───────────────── Agent Vault ─────────────────┐
MCP tools ───▶│ server.py ┐ │
│ ├─▶ core: store · policy · │
shell ───▶│ runner.py ┘ scrub · audit · keys │
(agent-vault run) │ │
└──────────────────────┬────────────────────────┘
▼
~/.agent-vault/vault.json (one Fernet-authenticated blob)
~/.agent-vault/audit.jsonl (append-only, 0600)
OS keychain (master key)
```
- **`store`** — load/save the encrypted vault, add/get/remove secrets.
- **`keys`** — master key from the OS keychain (or `AGENT_VAULT_KEY` for CI).
- **`policy`** — host-allowlist checks (exact + single-level wildcard).
- **`scrub`** — replace secret values (and encoded forms) with placeholders.
- **`audit`** — append-only JSONL log of every use and denial.
## Agent setup (MCP)
Agent Vault ships an MCP server so agents can use secrets without ever handling
their values. Add it to your agent's MCP configuration (for Claude Code, in
`.mcp.json` or your MCP settings):
```json
{"mcpServers": {"agent-vault": {"command": "agent-vault", "args": ["mcp"]}}}
```
The server exposes two tools: `vault_list_secrets`, which returns secret names
and metadata (never values), and `vault_http_request`, which performs an
authenticated HTTP call by injecting a named secret server-side, enforcing the
secret's host allowlist, and scrubbing the response before returning it.
## Security model
Agent Vault provides five guarantees:
1. **Secret values never appear in agent-visible output.** MCP responses and
`agent-vault run` output are scrubbed: every stored value is replaced with
its `{{vault:NAME}}` placeholder before the agent sees it. MCP response
scrubbing also covers the base64 and percent-encoded forms of a value, so a
reflected `basic`-auth or query credential is caught. Wrapped `run` children
do **not** inherit the `AGENT_VAULT_KEY` master key in their environment.
2. **Per-secret host allowlist on MCP HTTP.** Each secret carries an
`allowed_hosts` list. A prompt-injected agent cannot exfiltrate `stripe_key`
to `evil.com` via `vault_http_request` — off-allowlist hosts are denied
before any request is made, agent-supplied routing headers (`Host`,
`:authority`, `forwarded`, and the `X-Forwarded-Host/For/Proto/Port/Server`,
`X-Real-IP`, `X-Original-Host`, `X-Host` family) are rejected so they can't
reroute an allowlisted request, and any request in which a raw secret value
itself appears in `method`/`url`/`headers`/`body` is denied.
3. **Every use and every denied attempt is audit-logged.** Injections, HTTP
requests, and policy denials are all recorded (names and hosts, never
values). The audit log is symlink-safe, fsync-durable, and tolerant of a
corrupt line.
4. **Local-only, with the whole vault authenticated at rest.** There is no
cloud service, sync, or telemetry. The **entire** vault file — secret values
*and* metadata, including each secret's `allowed_hosts` — is encrypted and
authenticated with Fernet (AES-128-CBC + HMAC), so any on-disk tampering
(e.g. an agent editing `allowed_hosts` to widen a secret's policy) fails the
MAC and is rejected on load. The master key lives in your OS keychain (with
an `AGENT_VAULT_KEY` environment-variable fallback for headless use). Vault
writes are atomic (temp-file + `fsync` + rename), locked against concurrent
writers, and symlink-safe (`O_EXCL`/`O_NOFOLLOW`).
5. **Honest limitations, documented rather than hidden:**
- `agent-vault run` has **no host policy**, and by design hands the real
secret to the child process (that is how the child authenticates). Once a
process legitimately holds a secret it can do anything with it — transform
it, forward it, or store it — and no amount of output scrubbing can
prevent that. `run` will inject a secret into any command you ask it to.
Host allowlists apply only to MCP HTTP requests.
- Scrubbing is **best-effort** and matches secret values literally (plus
their base64/percent-encoded forms on MCP responses). If a tool transforms
a secret some other way (hashing, reversing, splitting), the transformed
form is not recognized or scrubbed. `run` output scrubbing is additionally
**line-based**: a secret straddling a line boundary, or one containing a
newline, is not scrubbed.
- Substituted `{{vault:NAME}}` values appear in the **child command's
argv**, visible in `ps` and other process listings on the same machine for
the child's lifetime. Prefer `--env NAME` injection where the tool accepts
credentials from the environment.
- Authentication detects tampering but **not rollback**. An attacker who can
overwrite `vault.json` cannot forge new policy, but can restore a *previous
valid* copy of the whole file (reverting a tightened allowlist or a rotated
secret). Protect the vault directory with filesystem permissions; anti-
rollback is out of scope for this local-file design.
- **Confidentiality assumes the agent is confined to the vault's tools.**
Agent Vault protects secret values from an agent that can only reach them
through `vault_http_request` and `run`. It does **not** defend against a
process running as your own user that can execute arbitrary code — such a
process can read the master key from your OS keychain (subject to the OS's
own prompts) or from `AGENT_VAULT_KEY` in the environment, and decrypt the
whole vault directly, with no scrubbing, allowlist, or audit entry. If you
pair Agent Vault with a shell-capable agent, the vault raises the bar and
gives you an audit trail for tool-mediated use, but it is not a sandbox and
cannot contain an agent that already has same-user code execution.
Exit codes follow Click's convention: `0` on success, `1` on a user or
validation error, `2` on a usage error. Policy denials are surfaced as
structured MCP errors, not exit codes; `agent-vault run` passes through the
child command's exit code unchanged.
Found a vulnerability? Please read [SECURITY.md](SECURITY.md) — do not open a
public issue for security reports.
## Audit log
The audit log lives at `~/.agent-vault/audit.jsonl` (or
`$AGENT_VAULT_HOME/audit.jsonl`). It is append-only, written with mode `0600`,
and stored as one JSON object per line. Each entry records a timestamp, an event
type (`add`, `remove`, `run_inject`, `http_request`, or `denied`), the secret
name, a target (a command or host), whether the action was allowed, and a short
detail string. It never contains secret values.
```bash
agent-vault audit --tail 20
```
## Environment variables
- `AGENT_VAULT_HOME` — override the vault directory (default `~/.agent-vault`).
Useful for isolating vaults per project or in tests.
- `AGENT_VAULT_KEY` — a base64 Fernet key used instead of the OS keychain. Set
this for headless or CI environments where no keychain is available.
## Project layout
```text
agent_vault/
cli.py # Typer CLI: init / add / list / remove / audit / run / mcp
runner.py # `agent-vault run` — placeholder + env injection, output scrubbing
server.py # MCP server: vault_list_secrets, vault_http_request
core/
store.py # encrypted vault storage (whole-file authenticated)
keys.py # master-key management (keychain + AGENT_VAULT_KEY fallback)
policy.py # host allowlist checks
scrub.py # replace secret values with {{vault:NAME}}
audit.py # append-only audit log
tests/ # pytest suite (unit + CLI + runner + MCP)
docs/ # design spec and implementation plan
```
## Contributing
Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) for
development setup, the test workflow, and the (higher-than-usual) bar for
changes that touch the security-sensitive core. By participating you agree to
the [Code of Conduct](CODE_OF_CONDUCT.md).
## Security policy
Please report vulnerabilities privately as described in
[SECURITY.md](SECURITY.md), not via public issues.
## License
MIT. See [LICENSE](LICENSE).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available