3 / 8
Manifest and access model
The last chapter covered how a secret sits encrypted on disk. This one is
about how Athanor decides who gets what. The whole decision lives in one
file — athanor.yaml.
Athanor.yaml, line by line
# scoped (default, honors expose flags) | full (escape hatch)
access_mode: scoped
# public key secrets_file is encrypted to
recipient: age1qxxxxxxxxxxxxxxxxx
# path to the encrypted file
secrets_file: ./secrets.enc.yaml
deploy:
# where `athanor deploy` places files on the VPS
remote_path: /opt/myapp
secrets:
DB_PASSWORD:
# only available via run_with_secrets and the TUI
expose: false
STRIPE_TEST_KEY:
# can be returned as text via get_secret
expose: true
The manifest itself is not secret — it holds no secret values, only
names, flags, and the public key. Committing it to git is fine, even if
you've decided to keep secrets.enc.yaml out.
One detail worth knowing about secrets_file: the path resolves relative
to athanor.yaml itself, not the directory a command happens to run from
(Manifest.SecretsFilePath()). That means it can point outward — say, to
~/shared/secrets.enc.yaml — and two projects' manifests pointing at the
same path end up sharing one literal encrypted file. That's what lets two
projects on the same VPS have genuinely shared secrets with no duplication;
more on this in the deploy chapter.
One place the decision gets made: Exposable
The whole access model comes down to a single function in
internal/manifest:
Exposable(name):
if access_mode == full → true, no matter what
otherwise → true only if secrets[name].expose == true
(unknown name → always false)
That's a deliberate choice, not an implementation detail: the decision "can this secret be returned as text" gets made in exactly one place. Not five separate checks scattered across commands that could drift apart — one function, one source of truth.
Where the check applies — and where it doesn't
Store.Get(name) is a low-level function that always returns a value if
one exists, without looking at expose at all. That's on purpose: Store
is storage, not a gatekeeper. The Exposable check lives on the
calling side:
| Access channel | Checks Exposable? |
Why |
|---|---|---|
CLI (athanor list, athanor add) |
no | trusted local context — you already have a terminal in this project |
TUI (athanor tui, enter/y) |
no | same trusted local context as the CLI |
MCP get_secret |
yes | the only channel that actually returns a secret as text to an outside consumer — the agent |
MCP run_with_secrets / CLI run |
no | Athanor does not return the value directly, but the trusted command receives it and can print or transmit it |
expose: false doesn't mean "the agent can't touch this secret" — it's
still reachable through run_with_secrets. The flag specifically means "not
directly retrievable through get_secret." That distinction is the basis of
the MCP server chapter.
access_mode: full — an escape hatch, not a default
access_mode: full in the manifest (or --full-access on serve-mcp)
makes Exposable return true unconditionally — get_secret can then
return anything, regardless of an individual expose setting. This is
deliberately a config setting, one a human flips in the file or at
server startup, never a parameter the agent could pass in a tool call —
otherwise expose: false stops meaning anything the moment the agent can
just bypass it at runtime.
Checkpoint question. If a secret has expose: false, does that mean the
agent has no way to use it? If not, how does it?
Answer: no. expose: false blocks exactly one channel — get_secret,
returning the value as text. Through run_with_secrets the secret is still
available to the running process. Athanor does not add it to the response,
but the process's stdout/stderr returns to the agent and can contain the
value if the command prints it.