1 / 8
The problem and the concept
Let's start with why this exists, not with the code.
Two bad options
Here's a situation that comes up constantly: an agent needs to run a test that requires a test-database password, SSH into a server, or open a logged-in page with a headless browser. Either way, it needs a secret. There are exactly two obvious ways to hand it one, and both are bad.
Option one — let the agent read .env.
agent ──Read(.env)──▶ file with secrets ──▶ the value ends up
in the model's context
The moment that value is read, it's text in the conversation with the
model. From there it settles into API request logs, into transcript
history, and if the model is cloud-hosted, into someone else's
infrastructure. There's no calling it back. Agents default to not reading
.env for exactly this reason — which just moves the question to how
they're supposed to use secrets at all.
Option two — paste the secret into the chat by hand.
human ──pastes password into the prompt──▶ chat history ──▶ logs, cache, backups
Same problem, different source: a human instead of a file. The only real difference is that it's deliberate, not an accident.
Both roads lead to the same place: the secret's value becomes tokens in the model's context. That's the thing to avoid — not hiding the secret from the agent altogether (then it couldn't do its job), just keeping the value out of the text the model actually sees and remembers.
The core idea
An agent almost never needs to see a secret's value. It needs the command
it's running to have access to it. A curl with a token in the header,
an ssh with a key, a psql with a password — in every case the secret
belongs to the process, not to the conversation.
So the right channel isn't "read it and paste it into the prompt" — it's "run the process with the secret in its environment, and show the model only the result":
agent asks to run a command
│
▼
Athanor injects the secret into the CHILD process's environment
│
▼
the model sees the command's stdout/stderr; if the command prints
the secret, it appears in that output
That's the default. Individual secrets where a text value is fine — a test-only public API key, say — can be marked "safe to return as text" explicitly. It's a deliberate exception, not the default mode. Exactly how that's wired up is covered in chapter 3.
Why age+sops, not Vault/1Password/Infisical
Off-the-shelf secrets managers — Vault, 1Password, Infisical — solve the
same problem, but they're built for teams: a server, accounts, sync. For a
solo developer with simple projects, that's more machinery than the job
needs — infrastructure built for "a team," not "one person, a couple of
VPSes." age (key-pair encryption) and sops (a declarative editor for
age-encrypted YAML) give the same guarantee — a secret on disk is always
encrypted — with no server, account, or network involved: two small
binaries and one key file.
Where the name comes from
Athanor is an alchemical furnace built for continuous transmutation: raw
matter goes in once, and from then on the furnace keeps it in the right
state and releases "energy" on demand. A secret works the same way — loaded
once (athanor add), then passed to a trusted process without direct tool
retrieval. That process is inside the trust boundary and can still print the
value it received.
Checkpoint question. Why is "don't let the agent read .env" an
incomplete way to frame the problem, and what actually needs preventing?
Answer: it was never only about the file; it is about removing unnecessary
exposure paths. You can block .env reads and still paste a password into
chat or ask a command to print its environment. Athanor removes plaintext
from the project and direct responses by default, while the command receiving
the secret remains trusted.