---
title: AI agents & MCP
description: Grant AI agents scoped access to secrets over MCP.
sidebar:
order: 9
---
AI coding agents need secrets to do real work: an API key to call a service, a
database URL to run a migration. The usual way to hand them over is to paste a
value into a prompt or point the agent at a `.env` file. Both can leak. Prompts
are often logged, and sometimes sent to third-party APIs. `.env` files get
committed by accident. And once a value is in a log or a prompt, you can't pull
it back.
murk splits this into two steps.
To understand a project, an agent doesn't need values, it needs to know what
secrets exist. `murk agent plan` prints the schema — key names, descriptions,
and tags — with no decryption and no key involved. The agent writes code and
config against those names without seeing a value.
To run a command, you mint a grant: a short-lived key that decrypts only the
secrets you name, with a time limit. The agent runs against that grant, never
your own key, and you can revoke it. If a task only needs `STRIPE_KEY`, that's
the only thing the grant can read.
This narrows where secrets are exposed. It doesn't make a leak impossible: a
command the agent runs can still print a value to its own output, and a grant
key stored on disk can be read by anything running as your user. murk gives you
least-privilege, expiring, revocable access, not a sandbox. For real isolation,
run the agent as a separate user or in a container.
## Rules
1. **Never paste secrets into prompts.** Agent conversations are logged,
cached, and sometimes sent to third-party APIs. Once a secret is in a
prompt, assume it's leaked.
2. **Never give agents your `MURK_KEY`.** The key is your identity. If an
agent has it, it can decrypt everything in the vault, and you can't
revoke it without re-keying.
3. **Use `murk exec` to inject secrets.** Instead of exporting secrets to the
shell, run agent commands through `murk exec` so secrets exist only in the
subprocess environment:
```bash
murk exec -- python deploy.py
murk exec -- npm run migrate
```
When the agent itself is invoking the command, use `murk agent exec`. It
requires explicit `--only` keys, clears the inherited environment, strips
`MURK_KEY`, and marks the child as an agent context (`MURK_AGENT=1`), so
the run can only see the secrets you named and a nested `murk` won't fall
back to your stored key:
```bash
murk agent exec --only DATABASE_URL -- npm test
murk agent exec --only DATABASE_URL --only PG_PASSWORD -- ./migrate.sh
murk agent exec --only STRIPE_SECRET_KEY -- python scripts/refund.py
```
4. **Use `murk agent plan` for schema prompting.** Agents don't need secret
values to understand what's available. `agent plan` emits key names,
descriptions, examples, and tags as text or JSON: no decryption, no
`MURK_KEY`, no recipient metadata:
```bash
murk agent plan murk agent plan --json murk agent plan --tag db ```
Paste the output into agent system prompts so they know what env vars
exist and how to reference them, without ever seeing the values.
Reach for `murk info` when you want a fuller picture (recipients, your key
source, private overrides). Reach for `murk skeleton` when you want a
distributable vault file shaped like the real one but with `recipients` /
`secrets` / `meta` blanked.
Full flags for every command here are in the [CLI reference](/reference/cli/).

## Agent context
Set `MURK_AGENT=1` to tell murk it's running for an agent. In an agent
context, **strict mode is forced**: murk won't fall back to your stored key
in `~/.config/murk/keys` (the agent must present its own
`MURK_KEY`/`MURK_KEY_FILE`, e.g. a [grant](/concepts/grants/) key, or fail
closed), won't write plaintext secrets to a file, and requires a RAM-backed
tmpdir for `murk edit`.
`murk agent exec` sets `MURK_AGENT=1` and `MURK_STRICT=1` for the child, so a
nested `murk` stays strict and won't fall back to your stored key on the
normal path. This is a safe default, **not a sandbox**: a child controls its
own environment, so it can unset those vars or read `~/.config/murk/keys`
directly. For real containment, run agents under a separate user or in a
container (see below). If you want a non-strict shell yourself, just don't
set `MURK_AGENT`. In CI, murk stays out of the way but prints a one-line
nudge toward the scoped path when it sees a pipeline decrypting with your
personal key.
**Self-scoping your own key.** The allow-tag policy (see [Restricting which
secrets agents can touch](#restricting-which-secrets-agents-can-touch),
below) normally binds only agent grant keys; `murk get`/`export`/`edit`
with your *own* key ignore it. Set `MURK_SELF_SCOPE=1` (agent context implies
it) to hold your own reads to the policy too: `get`, `exec` (and `agent
exec`), and single-key `edit KEY` fail closed on a non-allowed key; `export`
withholds forbidden keys (with a note on stderr); and bulk `murk edit` is
refused. Reach for it when you run an agent in your own shell and want the
guardrail to actually bite. It's still the murk binary enforcing it, not a
sandbox.

## Short-lived agent grants
`murk agent exec` is the safest pattern: the agent's command gets secret
*values* in its environment and never sees a key. Reach for a
[grant](/concepts/grants/) when the agent has to run `murk` itself over a
session, for example a long-running agent that calls `murk get` as it
works.
### One-shot setup: `murk agent init`
`murk agent init` does the whole safe-path setup in one command: it
(optionally) sets the allow-list, mints a scoped grant, and (when it writes
a key file) prints the exact run command plus an isolation recipe.
```bash
murk agent init --name codex --only STRIPE_SECRET_KEY --ttl 2h
murk agent init --name codex --only DATABASE_URL --allow-tag agents --ttl 30m
```
`--allow-tag` sets the vault's agent allow-list before granting: a single
vault write covers both, and a forbidden scope fails closed before anything
is saved. The manual building blocks are below.
`murk agent grant` mints a fresh ephemeral age identity and gives it read
access to exactly the keys you name, never your `MURK_KEY`:
```bash
murk agent grant --name codex --only STRIPE_SECRET_KEY --ttl 2h
murk agent grant --name codex --only DATABASE_URL --only PG_PASSWORD --ttl 30m
```
It writes the agent key to `~/.config/murk/agent-keys/<vault-hash>-<name>`
(or `--out PATH`, or `--out -` to stream it to stdout) and prints how to use
it. Run the agent with that key and `MURK_AGENT=1` (agent context, strict is
forced) so it won't fall back to your stored key:
```bash
MURK_KEY_FILE=~/.config/murk/agent-keys/<...>-codex MURK_AGENT=1 \
murk agent exec --only STRIPE_SECRET_KEY -- python scripts/refund.py
```
The granted key reads only its keys; anything else returns "key not found".
It is excluded from the shared layer entirely.
List and revoke grants:
```bash
murk agent ls # name, scope, TTL status
murk agent revoke codex --rotate # remove the grant and rotate its keys
```
Three things to keep in mind:
- **The TTL is advisory.** age keys can't self-destruct, and old vault
versions stay readable in git, so a leaked grant key works until you
`agent revoke` and rotate. The TTL tells you *when* to revoke; `agent ls`
flags expired grants. Revoke + rotate is the real close.
- **The key is a bearer credential.** Whoever holds the key file has the
access. Treat it like the secret it unlocks.
- **Real isolation is the OS's job.** An agent running as you, with read
access to your home directory, can read `~/.config/murk/keys` directly and
bypass murk. `MURK_STRICT` stops murk from *handing over* your key, but
for true containment run the agent in a sandbox, container, or under a
separate user that can't read your key directory.
## Restricting which secrets agents can touch
Tag your secrets and set an allow-list, and murk will refuse to inject or
grant anything outside it in agent mode:
```bash
murk describe DATABASE_URL "..." --tag agents # tag the agent-usable ones
murk policy set --allow-tag agents # default-deny everything else
```
Now `agent exec` and `agent grant` only work for keys tagged `agents`; asking
for an untagged or production key fails closed with a clear error. There's
no override flag, so a misbehaving agent can't talk its way past it.
`agents` is just an example tag; use whatever tags fit your vault (`dev`,
`ci`, ...). The policy lives in the vault header (MAC-covered, readable with
`murk policy show` even without a key) so it travels with the repo and
applies in CI. Note this is a guardrail enforced by the murk binary, not
access control: see the [threat model](/security/threat-model/).
A granted agent is held to the policy no matter how it reads: `murk get`,
`murk agent exec`, or the Python/Node bindings (`murk-secrets`). `get()` and
`export()` from the bindings refuse a forbidden key just like the CLI, so the
allow-list is enforced from every entry point. Tightening the policy applies
retroactively: drop a tag and the agent loses access on its next read, even
though its old grant key still exists.
## Serving secrets over MCP (`murk mcp`)
Agent harnesses that speak the
[Model Context Protocol](https://modelcontextprotocol.io) (Claude Code,
Cursor, omp, and others) can reach murk secrets directly through
`murk mcp`, a stdio MCP server built into the binary. It calls murk
in-process (no subprocess, no Node runtime) and is bound by the same grant
and policy machinery as everything above.
It runs **only** as a scoped agent: it fails closed unless it is launched
with a grant key **and** `MURK_AGENT=1`. Started with your stored key, a
plain recipient, or no grant, it refuses to start, so an MCP client can
never be handed your full read scope.
```bash
# Mint a scoped grant first (see above), then run the server with it:
MURK_KEY_FILE=~/.config/murk/agent-keys/<...>-codex MURK_AGENT=1 murk mcp
```
### One-command wiring: `murk agent connect`
Rather than hand-edit each editor's MCP config, let murk do it — minting the
grant and writing the config in one step:
```bash
# Auto-detect the editors this repo uses and wire them all:
murk agent connect --only STRIPE_SECRET_KEY --only DATABASE_URL --ttl 2h
# ...or target one (claude, cursor, vscode, zed, gemini, omp, codex):
murk agent connect cursor --only STRIPE_SECRET_KEY
```
`connect` mints one scoped grant for the vault and writes each editor's
project-local config, pointing at `murk mcp` with `MURK_KEY_FILE` and
`MURK_AGENT=1`:
| Claude Code | `.mcp.json` | JSON |
| Cursor | `.cursor/mcp.json` | JSON |
| VS Code | `.vscode/mcp.json` | JSON |
| Zed | `.zed/settings.json` | JSON |
| Gemini CLI | `.gemini/settings.json` | JSON |
| omp | `.omp/mcp.json` | JSON |
| Codex | `.codex/config.toml` | TOML |
It writes only a key-file **path**, never key material, and preserves any
other servers and comments already in the file. Add `--allow-exec` to also
expose the exec tool. Because these files are often committed, murk warns and
offers to add the path to `.gitignore`.
`murk agent disconnect [CLIENT]` removes only murk's entry, leaving other
servers untouched; add `--rotate` to revoke the grant and rotate the keys it
could read.
The server speaks JSON-RPC over stdout and logs only to stderr, so point
your MCP client at that command with `MURK_KEY_FILE` and `MURK_AGENT=1` in
its environment. It exposes two always-on read tools bounded to the grant,
plus an opt-in exec tool:
- **`murk_plan`**: the schema (key names, descriptions, examples, tags) of
the secrets *this grant may read*, as JSON. No values, and no keys outside
the grant's scope or the vault's agent policy: a narrowly-scoped agent
can't even enumerate what else the vault holds. Takes an optional `tags`
filter.
- **`murk_get { key }`**: one secret value, if the grant may read it. A key
outside the grant's scope or forbidden by the agent policy returns an
error result and never the value: fail-closed, like every other agent
path.
- **`murk_exec { only, command }`**: *opt-in*, enabled with
`murk mcp --allow-exec`. Runs a command with the named secrets injected
into its environment (no shell), returning captured stdout, stderr, and
the exit code. Every key in `only` must be in the grant's scope and
policy-allowed, or it fails closed before running anything; output and
runtime are bounded. The caveat: `only` scopes the injected *secrets*, not
the command: it runs as your user with your filesystem and network
access, so it is **not a sandbox**. Enable it only where the server
already runs under OS-level isolation.
You can verify it end to end without a client by driving the handshake over
a pipe:
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| MURK_KEY_FILE=<grant> MURK_AGENT=1 murk mcp
```
The transport is a local stdio pipe, not a network listener. The grant
bounds which secret *values* reach the agent (the capability-not-credential
model), but `murk_exec` (when enabled) runs real commands as your user, so
treat it like `murk agent exec`: a safe default, not a sandbox, with
OS-level isolation the real boundary (see
[Short-lived agent grants](#short-lived-agent-grants), above).
For an editor murk doesn't wire automatically yet, add the same `murk mcp`
entry (with `MURK_KEY_FILE` and `MURK_AGENT=1`) to that harness's MCP config
by hand.
## Check the code an agent wrote
Agents can accidentally hardcode a secret they were handed. `murk scan` compares files in your working tree against the vault's decrypted values and flags any leak. Run it after an agent edits code.

## Auditing agent activity
There's no separate agent log to consult. **git is the record.** Every
admin change to a grant or policy is a commit, so:
```bash
git log -p .murk # who created/revoked grants, changed policy, rotated values
murk diff # the same changes for the latest revision, decoded
```
Each shows the change attributed to its commit author (and signed, if you
use git commit signing). What git *can't* show is secret reads on a
developer's machine (murk never sees those), so don't treat the absence of
a read trail as proof a secret wasn't used. See the
[threat model](/security/threat-model/) for the full audit boundary.