ATHANOR
RU GitHub ↗

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.