Skip to main content
A package never contains a secret value. It contains the name of an environment variable, and the value arrives at run time from your environment or your platform’s secret store.

Declare them

list of strings
Unique UPPER_SNAKE environment variable names, never their values. Omit for no explicit secret inventory. Required credentials are still inferred from providers, tools, and connections; missing declarations warn rather than making those credentials optional at run time.
agent.yaml
A list of names. Each one must be UPPER_SNAKE: a capital letter, then capitals, digits, and underscores. A lower case or punctuated entry is a typo that would otherwise become a lookup failing at call time, so it is refused:
Declaring the same name twice is refused too.
The refusal does not repeat what you wrote. This slot is where a pasted credential lands when somebody mistakes a name for a value. A message that quotes it back puts the key in a terminal, a CI log, and any bug report copied from either. The file and the line are enough to find it. The same is true of every other place a name belongs: a connection’s environment: value, a destinations: number, a tool’s token_env.
The name must also be a valid shell identifier, which is the same rule. A platform would fail to export a name like 2FACTOR_API_KEY, so Unmute refuses it during validation. The same check runs on a connection’s environment values, where the error names the key rather than the value:

The rule: declare what the generated project reads

secrets: is the package’s explicit inventory of environment names the generated project reads. Some names appear directly in package files. The compiler infers others from a provider choice: Declare all of those names. A declared name also joins the generated environment requirements when the compiler cannot infer its use, for example when a local handler builds an environment lookup dynamically. Do not declare names supplied by the driver or deployment platform: These names are not package secrets, but you may still have to supply some of their values when you deploy. .env.example lists the values you supply. The compile report and README cover the complete required set and who supplies each one. Leaving out a name the compiler inferred is a warning, not an error. The inferred name still stays in the generated environment instructions and checks; the warning says the explicit secrets: inventory is incomplete:

How generated files use the inventory

The compiler combines declared secrets with environment names inferred from models, tools, tracing, destinations, and the selected phone route. The environment template. build/<target>/.env.example is the operator’s checklist, not a copy of secrets:. It lists the values you supply for this build. Every line in it is a line to fill in: What it holds depends on the build:
On other routes the generated file also carries a short note about which names a phone call adds and who reads them, so read the one your build wrote rather than assuming a shape. Names that the local runtime or deployment platform creates are left out. They are not lost: compile-report.json carries the complete list under required_env, and the emitted README.md explains where each value comes from. Runtime checks. The generated project checks the values the current session needs and names what is missing. The set comes from declared and inferred names, so a package with no secrets: block still checks names the compiler can infer:
An always-read name, such as a model or tool credential, stops the container at startup. A route-only name is checked when the phone path starts, so a browser session does not demand carrier credentials it never reads. When a missing name is one the file above left out because the runtime supplies it, the failure says so rather than telling you to set something you never saw:

Secrets never flow through templates

{{...}} renders variables only. Naming a secret in a template is a compile error, not a value that leaks at run time:
The reason is where a template ends up. It renders into speech, a prompt, a tool argument, or a URL, so its value is spoken, logged, or traced. That is right for a customer’s name and wrong for a token.

The seams a secret travels through

tools/reschedule_appointment.yaml
Every one of these fields holds a name. token_env in particular is checked:

Where the values come from locally

unmute dev builds the run’s environment from your shell, then .env and .env.local in the current directory, then .env and .env.local in the package directory. Later files win, so a repository wide .env can hold shared keys while a local or package file overrides a value. Both .env and .env.local are gitignored. Neither is generated with values or committed.

Where the values come from in production

On a code target, use your platform’s secret store. LiveKit secret updates restart instances without rebuilding. Pipecat secret updates need a separate rollout after the set is ready. Both accept a file of values; neither needs a new image just to replace an existing value.

The SLNG vault

An slng target has no .env.example and no build/<target>/ project tree: it compiles to a deployment body, not a project, so there is no file for a secret to sit beside. Instead, unmute deploy works out what the vault must hold from the compiled body itself: a tool’s authentication block, a hosted tool’s mirrored secret names, and every {{$NAME}} vault token in a prompt, a greeting, or a tool field. It reads the package’s secrets: list nowhere, so a name declared there that no tool authenticates with, and that no {{$NAME}} token uses, is never checked here. Declare it anyway if the same package also targets livekit or pipecat: those two do read secrets:. A name the vault already has is left alone; a missing name can be created on the spot, by running voiceai secret create <name>, which asks for the value on its own masked terminal prompt. The value never enters unmute’s own process, so it never reaches argv or either output stream. Creating a missing secret this way is the only write a deploy makes. No tool, MCP server, or trunk is ever created, changed, or deleted from here.

Where to go next

CLI reference

Every command, flag, and exit code.

Going live

Deploying with real credentials.