Skip to main content
Your agent needs keys: one for the model provider, usually one more for each API a tool calls. A package never holds the value. It holds the name of an environment variable, and the value arrives at run time from your .env file locally, or your platform’s secret store once you deploy. That means agent.yaml is safe to commit, and the same package works on your machine and in production without editing. On this page:

Three steps

1

Put the value in .env

Next to agent.yaml, in a file git already ignores:
.env
unmute dev reads .env and .env.local from both the current directory and the package directory. Neither is ever generated with values, and neither is committed.
2

Declare the name in secrets:

agent.yaml
A list of names, each one UPPER_SNAKE. This is the package’s inventory of what the generated project reads. A lower case or punctuated entry is refused, because it is a typo that would otherwise fail at call time.
3

Point at the name from where it is used

Never at the value. Each has its own *_env field:
tools/reschedule_appointment.yaml
A Python handler reads its own with os.environ["SALON_API_TOKEN"].
Compile, and build/<target>/.env.example lists exactly the values you have to supply for that build, ready to copy to .env.

Every key a secrets block takes

One key, at the top level of agent.yaml.
list of UPPER_SNAKE names
The environment variables the generated project reads. Each entry is a capital letter, then capitals, digits and underscores; a lower case or punctuated entry is refused, and so is the same name twice. Left out, the compiler still infers the names it can see and warns that the inventory is incomplete.

Where a name goes

Every key a seam takes

Each one holds the name of an environment variable, never a value.
UPPER_SNAKE name
In tools/<name>.yaml. The base URL of an authenticated API. A webhook tool needs this or base_url, and the code targets read this one.
UPPER_SNAKE name
In tools/<name>.yaml. The MCP server’s address. The code targets dial the server themselves and need it; an slng package leaves it out.
UPPER_SNAKE name
Under auth: in a webhook: or mcp: block. The bearer token or API key, and required once auth: is written at all.
UPPER_SNAKE name
On a models: entry in agent.yaml. Points that model at your own gateway instead of the provider’s.
map of route fields to UPPER_SNAKE names
In connections/<name>.yaml. The account behind a phone route, one key per field the route needs.
map of names to UPPER_SNAKE names
In agent.yaml. Each entry is the desk an escalation reaches, mapped onto the variable holding its phone number.
A local: handler is the one case with no key: it reads os.environ itself, so the name lives in the Python you wrote rather than in agent.yaml. Declare it in secrets: all the same, so the inventory stays complete.

A secret is not a variable

They look similar and they are not the same thing.
{{...}} renders variables only. Naming a secret in a template is a compile error, not a value that leaks at run time. A template renders into speech, a prompt, a tool argument or a URL, so whatever it holds gets spoken, logged or traced. That is right for a customer’s name and wrong for a token.

What you do not declare

Some names the target supplies for you: LIVEKIT_URL and its key pair, REDIS_URL for the phone routes that need it, the public URL and token unmute dev creates for a local phone run. Leave those out of secrets:. You may still have to supply some of their values when you deploy. The generated README.md and compile-report.json list the complete set and say who supplies each one.

Where to go next

Secrets reference

Every seam, every check, and what the generated files do with the inventory.

Going live

Moving these values into a platform’s secret store.