ATHANOR
RU GitHub ↗

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.