6 / 8
MCP server
Every previous chapter described a tool for a human. This one is about how those same secrets reach an AI agent — the entire reason this project exists (chapter 1).
What MCP is, briefly
Model Context Protocol is an open standard for tools an agent can call.
Athanor implements a server for it (athanor serve-mcp), speaking over
stdio — a process's plain stdin/stdout, not a network connection. None of
this is tied to Claude: any MCP-compatible client (Claude Code, Claude
Desktop, Cursor, Windsurf, Cline, and others) connects to the same binary
through its own config. Athanor doesn't know or care which client it's
talking to — from the server's side, these are just calls to two tools.
The get_secret tool
input: { "name": "STRIPE_TEST_KEY" }
output: { "value": "sk-test-..." } — if Exposable(name) == true
error: 'secret "DB_PASSWORD" is not exposable
(expose:false in athanor.yaml); use run_with_secrets instead,
or set access_mode: full to override' — if false
This is the only tool that returns a secret's value as literal text — the
one place in the whole system where the Exposable() decision from chapter
3 actually gets applied. Run the server
with --full-access (or set access_mode: full in the manifest) and the
check is skipped for every secret — the server prints a loud warning to
stderr on startup so that setting can't quietly get left on.
The run_with_secrets tool
input: { "command": ["sh", "-c", "psql -c 'select 1' $DB_URL"] }
output: { "stdout": "...", "stderr": "...", "exitCode": 0,
"stdoutTruncated": false, "stderrTruncated": false }
expose isn't checked here at all — every secret in the project goes
into the command's environment. The response contains the command's
stdout/stderr, so the command can print a secret and return it to the agent.
Each stream is capped at 1 MiB; its *Truncated field reports clipping.
command is argv, an
array of arguments, not a shell string: exec.Command(argv[0], argv[1:]...)
skips the shell interpreter entirely, so special characters inside a secret
can't accidentally break the command or open the door to injection. When
the agent needs shell features — pipes, substitutions — it wraps the
command itself as ["sh", "-c", "..."]; the command text then gets
interpreted by the shell, but secret values still arrive through
environment variables, never through the command text.
Being honest about the limits here
run_with_secrets isn't a wall nothing can get through — it's the wall
moved to where it's actually useful. An agent allowed to run arbitrary
commands can, in principle, build a curl call with a secret in the URL and
exfiltrate it; the OS can't tell that apart from any other command reading
an environment variable. Athanor doesn't try to make that impossible — doing
so would mean sharply limiting what the agent can run at all, which would
defeat the tool's purpose. Instead, it makes a leak like that visible
after the fact.
The audit log
Every call to run_with_secrets (and athanor run from the CLI) appends a
line to ~/.local/state/athanor/audit.log:
2026-08-29T10:34:58Z command="sh -c echo $TEST_KEY" secrets=TEST_KEY
It records the command and the secret names involved, never values. A failed write to this log never blocks the underlying call — if the log is unwritable, the secret still reaches the process. The log exists for traceability, not enforcement.
Checkpoint question. Why doesn't run_with_secrets check the expose
flag for each secret, while get_secret does?
Answer: expose governs direct retrieval through get_secret.
run_with_secrets is a separate, broader trust boundary: it gives secrets
to a subprocess that may use, print, or transmit them. Granting this tool
therefore means trusting the commands it runs, independently of expose.