Skip to main content
Glama
mvplab-ai

agent-vault

by mvplab-ai
README.md
# Agent Vault

> Give your AI agent the ability to *use* secrets without ever letting it *see* them.

[![CI](https://github.com/mvplab-ai/agent-vault/actions/workflows/ci.yml/badge.svg)](https://github.com/mvplab-ai/agent-vault/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](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).