Environment Variables
How the agent loads operator env, and how that relates to Settings
This page covers how minara-agent-v2 loads env at boot. For the
per-variable reference with defaults, formats, consumer files, and
"what breaks if unset", see
Reference → Environment variables.
User-visible knobs persist in Settings (settings.json#preferences).
Secrets persist in credentials.json (Settings → API Keys / Messaging).
Call sites read prefs.get / secrets.* / infra.get. Env is the
operator boot layer and the documented fallback in that chain.
Both the reference pages and the .env.example template at the project
root are generated from a single declaration source:
apps/agent/src/config/env-docs/ (narratives, in four languages) joined
with the runtime registries (preferences schema, API-key families,
messaging providers, LLM provider families, infra schema). They cannot
drift from each other; CI regenerates and diffs both.
MINARA_ is reserved for Minara-platform-specific variables
(MINARA_API_KEY, MINARA_BASE_URL, MINARA_SKIP_FUND_CONFIRM,
MINARA_DATA_DIR, MINARA_OPENAI_BASE_URL,
MINARA_OPENAI_CHAT_PATH). Agent-loop infrastructure variables
(gateway, log, model, scenario, memory knobs) drop the prefix.
TL;DR Convention
Any new skill or tool that needs an API key, secret, token, or overridable URL MUST:
-
Persist it in the right home, then read the façade. Secrets go through the API-key or messaging registry and
secrets.dataSource/secrets.messaging. User knobs go through the preferences schema andprefs.get. Operator infra (GATEWAY_PORT,MINARA_DATA_DIR) goes throughconfig/infra/schema.tsandinfra.get. Never hard-code secrets. Never accept them as CLI args you echo. -
Declare the narrative in
apps/agent/src/config/env-docs/in the same commit: add a block to the matching section module (what it controls, which skill/tool consumes it, when an operator would set it, the default behavior when unset, and the accepted value format), plus the cn / ja / ko translations underenv-docs/i18n/. Then runpnpm --filter @minara/agent generate:env-exampleandpnpm --filter @minara/docs generateand commit the regenerated.env.exampleand reference pages. A var that is not covered by any registry must declare anexemptbucket, or the generator fails. -
For domain skills (
apps/agent/src/skills/builtin/*.tsorapps/agent/src/skills/external/<id>/), declare the var inrequires_envso the SkillRegistry hides the skill entirely when the credential is missing:export const myNewSkill: DomainSkill = { id: "research.my_provider", // ... requires_env: ["MY_PROVIDER_API_KEY"], }; -
For tools (
apps/agent/src/tools/*.ts), the factory should return an emptyToolEntry[]when the secret is missing. The tool registry silently excludes unregistered names, so downstream skilltool_namesreferences will gracefully degrade:export function createMyProviderTools(): ToolEntry[] { const apiKey = secrets.dataSource("MY_PROVIDER_API_KEY"); if (!apiKey) return []; // ... } -
Never commit a real secret.
.envis git-ignored;.env.exampleis the committed template with blank values.
How .env loading works
Loading is handled by apps/agent/src/config/load-env.ts, a side-effect module
that calls Node 22's built-in process.loadEnvFile(".env"). It's imported
as the very first line of every entrypoint:
apps/agent/src/gateway/cli.ts(REPL mode)apps/agent/src/gateway/server.ts(HTTP mode)
The loader fills missing keys only. After boot, nothing writes process.env.
Resolvers classify origin from the remaining env snapshot plus the Settings /
credentials override.
Precedence at the call site, highest first: Settings / credentials
override, then variables already exported by your shell / CI / systemd,
then project .env, then the schema or infra default. The loader does
not overwrite existing keys. This matches Node's default behavior and is
the safest rule (no surprise masking of CI-injected secrets).
No dotenv dependency. We rely purely on process.loadEnvFile, stable
since Node 22.5. Missing .env is a no-op (not an error).
Checklist for adding a new key
Before merging:
- Home picked: preferences schema, API-key / messaging registry, or
infra schema (plus an
exemptbucket when it belongs to no prefs/secrets registry) - Call site reads
prefs.get/secrets.*/infra.get, notprocess.env.<NAME> - Narrative block added to the matching section module under
apps/agent/src/config/env-docs/sections/, covering purpose, consumer, when to set, default-when-unset, and value format - cn / ja / ko translations added under
apps/agent/src/config/env-docs/i18n/(the parity check fails otherwise) -
.env.exampleandcontent/docs/reference/env/regenerated and committed (generate:env-example+pnpm --filter @minara/docs generate) - For domain skills:
requires_env: ["<NAME>"]is set so the skill self-hides when the credential is missing - For tools: factory returns
[]when the secret is missing (never throws at boot) - Var name follows the convention
<PROVIDER>_API_KEY/<PROVIDER>_TOKEN/<PROVIDER>_<FIELD>: UPPER_SNAKE, provider prefix first - No real secret value is committed anywhere