Digitorn
Digitorn
All posts
credentials

How credentials work on Digitorn: an encrypted vault driven from YAML

Register a key once with the CLI, reference it by name from any agent, and let the runtime resolve it per user at session start. Four scopes, sixteen catalog providers, hash-chained audit logs, no secrets in your YAML.

DDigitornMay 6, 20268 min read

The previous version of Digitorn asked you to drop API keys into env vars and reference them from your YAML with {{env.ANTHROPIC_API_KEY}} templates. It worked, it was simple, and it was the wrong shape for anything beyond a single-user dev box. The new credential system replaces it. This piece walks through what changed, why, and how to migrate.

The shape of the change

Before, your agent brain looked like this.

YAML
1brain:2  provider: anthropic3  model: claude-sonnet-4-64  config:5    api_key: "{{env.ANTHROPIC_API_KEY}}"

The runtime resolved {{env.X}} at deploy time, the API key landed in the bundle the daemon stored, and every user of that app shared the same key. Fine for solo use, an awkward fit for multi-user setups and a non-starter for compliance.

The new shape uses a credential: block.

YAML
1brain:2  provider: anthropic3  model: claude-sonnet-4-64  credential:5    ref: anthropic_main6    scope: per_user7    provider: anthropic8  config:9    api_key: "{{env.ANTHROPIC_API_KEY}}"   # dev fallback, optional

The ref is a name you registered earlier with the CLI. The scope says how the lookup happens (more on that below). The provider is asserted at compile time so a typo in a vendor field gets caught before deploy. The plaintext API key never appears in this YAML, never enters git, never gets bundled into the deployed app. It lives in an encrypted vault, scoped to the right user, and the runtime injects it onto the live provider client at session start.

Note
The legacy {{env.X}} template still resolves, so your existing YAML keeps working. New apps should use the credential: block. Mixing both (like the production builtins do) gives you a vault-backed prod path with a fast dev fallback.

The four scopes

The scope answers two questions at once: who can see this credential, and when does the runtime resolve it. Two are baked into the deployed bundle, two are looked up per session.

system_wideresolved at compile
Every user, every app
An admin-set DB connection string used by all built-in agents
per_app_sharedresolved at compile
All users of one app
A read-only API key the publisher ships with their public agent
per_userresolved at session
One user, every app they install
Your personal Anthropic key, reused across every agent you run
per_app_per_userresolved at session
One user, one app
Your Slack bot token for the team's helper agent only
Compile-time scopes are baked into the deployed app bundle. Session scopes are resolved when the user opens a session, so secrets stay per-user even on shared apps.

per_user is the right default for almost everything. Each user brings their own Anthropic key, your YAML stays the same, and removing a user from your team revokes their access to every agent at once. system_wide is for daemon-wide configs (the read-replica DB connection an internal report agent uses) and requires admin privileges to set. per_app_shared and per_app_per_user cover the niche cases between those two, the edges where one app needs a key the others should not see.

If you are setting up your first agent and are unsure, pick per_user and move on. The other scopes exist for when you actually need them.

Where the plaintext lives

The flow from a YAML reference to an actual Authorization header on a Claude API call is short, deliberate, and never lets the secret touch storage outside the vault.

From YAML reference to LLM call, where the secret actually livesAPP.YAMLcredential: { ref: anthropic_main, scope: per_user, provider: anthropic }committed to git, no secret materialsession start, runtime looks up the ref for this userCREDENTIAL VAULT (encrypted at rest, hash-chained audit log)anthropic_main → decrypt(envelope_dek, KMS) → api_key=sk-ant-***decrypted in-memory only, hot-swapped onto the live provider instanceprovider client calls api.anthropic.com with the resolved keyANTHROPIC APIPOST /v1/messages · Authorization: Bearer sk-ant-***
The plaintext secret never lives in the YAML, never in the bundle, never in your repo. It exists in two places at runtime: the encrypted vault, and the in-memory provider client during a turn.

Your YAML has a name, not a key. The vault stores the ciphertext, sealed with an envelope encryption scheme: each credential row carries a per-row data encryption key, that DEK is wrapped by a master key your KMS controls. Decryption happens only in memory, only when the runtime is opening a session for the user who owns the credential, only for the duration of that session.

The audit log is hash-chained. Every create, read, update, delete writes a row whose hash includes the previous row's hash. Tamper with one entry and the chain breaks. There's a POST /api/admin/credentials/audit/verify endpoint that walks the whole chain and tells you whether it's intact, which is what you give compliance when they ask "can you prove nothing was modified".

Setting one up

Three steps. CLI for the registration, YAML for the reference, deploy as usual.

1. Register the credential

Bash
1digitorn credentials create \2  --provider anthropic \3  --name anthropic_main \4  --label "Personal Anthropic key" \5  -f api_key=sk-ant-...

The --provider flag picks the catalog handler, which knows what fields to ask for (api_key for most, access_key_id plus secret_access_key for AWS, connection_string for databases). --name is the stable slug your YAML will reference via the credential: block - it must match ^[a-z][a-z0-9_-]{0,63}$ (lowercase, digits, dash, underscore; starts with a letter). --label is the human-readable display name shown in the picker UI and in digitorn credentials list. User-owned credentials default to per_user scope; the other scopes (system_wide, per_app_shared, per_app_per_user) are configured through the admin endpoints.

If you omit --name, it defaults to the provider name (anthropic, deepseek, ...). One credential per provider works fine for most setups; use distinct names when you want multiple keys for the same provider (one for prod, one for dev).

The runtime acknowledges with a credential id, the plaintext is gone from your shell history because the CLI redacts the -f value in its echo.

2. Reference it from your YAML

YAML
1agents:2  - id: main3    brain:4      provider: anthropic5      model: claude-sonnet-4-66      credential:7        ref: anthropic_main8        scope: per_user9        provider: anthropic

Compact form works too if you're fine with the per-user default and skipping the provider assertion.

YAML
1agents:2  - id: helper3    brain:4      provider: anthropic5      model: claude-sonnet-4-66      credential: anthropic_main

The compact form emits a one-line warning at compile time encouraging you to spell out the scope. We added it because the explicit form is verbose for the common case and people just hard-coded keys instead. If your scope is per_user, the compact form is fine.

3. Deploy and use

Bash
1digitorn app deploy my-agent.yaml2digitorn dev chat my-agent

The first message you send opens a session, the runtime looks up anthropic_main for your user, decrypts it, hot-swaps the key onto the live Anthropic provider client, and the LLM call goes out with your key. No daemon restart, no key visible in any log, no secret material in any deployed file.

What's in the catalog today

Sixteen first-class providers, in three families.

LLM providers (4): anthropic, openai, deepseek, azure_openai. Each has a handler that knows the right fields, validates the key against the live provider on registration if you pass --test, and refreshes any rotating tokens.

Cloud and infrastructure (5): aws, gcp, postgres, mongodb, redis. The handlers cover access keys for the cloud providers and connection strings for the data stores.

OAuth integrations (6): github_oauth, notion, slack_oauth, google_oauth, discord_oauth, plus github_pat for the simpler personal-access-token flow. The OAuth handlers ship a background refresh loop that renews tokens within ten minutes of expiry without any agent-side code.

Other (1): stripe, with multi-key handling for the publishable, secret, and webhook signing keys at once.

Other LLM providers (Mistral, Ollama, vLLM, Groq, Gemini, Together) and other databases (MySQL, Snowflake, BigQuery) are on the roadmap. Until their catalog entry lands, configure them inline with {{env.X}} templates, the same way the legacy YAML did. The flexibility is intentional: the catalog is a curated set of well-understood handlers, anything outside it stays accessible through the inline path.

Sharing credentials across agents

Two agents on the same machine can reference the same credential by name. No copy-paste, no second registration. This is the day-to-day pattern for power users: register one Anthropic key, reference it from your coding agent, your research agent, your Slack bot, your meeting note-taker. Rotate once, propagates everywhere.

YAML
1agents:2  - id: writer3    brain:4      provider: anthropic5      model: claude-sonnet-4-66      credential: anthropic_main
YAML
1agents:2  - id: main3    brain:4      provider: anthropic5      model: claude-sonnet-4-66      credential: anthropic_main

Same ref, same vault entry. Sharing across users is opt-in via grants, not implicit, which is the right default for a multi-tenant runtime.

Rotation, revocation, the boring parts that matter

Rotating a key today is a delete + create. Find the credential id with digitorn credentials list, then:

Bash
1digitorn credentials delete <credential-id>2digitorn credentials create --provider anthropic --label anthropic_main -f api_key=sk-ant-NEW...

Per-user scopes pick up the new value on the next session. Deploy-time scopes need a redeploy of the apps that bake them in, which is by design: a system_wide credential is meant to be stable across sessions, so rotating it is an administrative event, not a quiet refresh. (A direct update CLI command is on the roadmap; the underlying API endpoint already exists at PUT /api/credentials/{id} if you want to call it manually.)

Revocation is digitorn credentials delete <credential-id> for your own credentials, or digitorn credentials grant-revoke <credential-id> <app-id> to remove an app's authorization without deleting the credential itself.

When you genuinely don't want the vault

The escape hatch is still there. If your provider isn't in the catalog, or you have a non-standard auth scheme, drop the credential: block and use the inline config.api_key: "{{env.X}}" template. The runtime resolves it the same way it always did. You lose the vault's encryption-at-rest, audit log, and per-user scoping, but you keep the simplicity of "edit a .env file and go".

We use this ourselves on dev machines for new providers we are integrating, and we always switch to the credential: block before shipping. The inline path is for fast iteration, the catalog path is for production.

Try it

The fastest way to see the difference is to install one of the builtins and inspect its YAML.

Bash
1curl -sSL https://digitorn.ai/install | sh2digitorn install hub://digitorn/digitorn-code34# look at the brain block5grep -A 4 "brain:" ~/.digitorn/apps/digitorn-code/app.yaml | head -8

You'll see the dual pattern in the wild: credential: block as the production path, config.api_key: "{{env.X}}" as the dev fallback.

Then register your own key:

Bash
1digitorn credentials create \2  --provider anthropic \3  --label anthropic_main \4  -f api_key=sk-ant-...

And run the agent. The next session uses your key, no edits to the YAML, no env exports, no shell history holding the value.

Further reading

#credentials#security#yaml#production#vault
ShareLinkedIn
Newsletter

One post a fortnight, in your inbox.

Engineering notes from the Digitorn team. No marketing, no launch announcements, no "10 prompts that will change your life". Just the things we write that we'd want to read.

One-click unsubscribe. We never share your address. Powered by our own infrastructure, not a tracker.
D
The Digitorn team

We build the open-source AI agent runtime that runs on your own machine. YAML over Python, multi-agent by default, marketplace for sharing.

Keep reading

Try it now

Ship your first AI agent in 5 minutes.

Open-source. Self-hosted. YAML-first. Bring your own LLM keys, agents run on your machine.