> ## Documentation Index
> Fetch the complete documentation index at: https://unmute.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unmute compiles to exactly three targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Those three are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# Secrets

> Names only, never values: how a credential reaches the agent and its tools.

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

<ParamField path="secrets" type="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.
</ParamField>

```yaml agent.yaml theme={null}
secrets:
  - OPENAI_API_KEY
  - SLNG_API_KEY
  - SIP_TRUNK_HOSTNAME
  - SIP_AUTH_USERNAME
  - SIP_AUTH_PASSWORD
  - SIP_FROM_NUMBER
  - BILLING_PHONE_NUMBER
  - SUPERVISOR_PHONE_NUMBER
```

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:

```text theme={null}
agent.yaml:7: this secret is not an UPPER_SNAKE environment variable name. A secret is a
  name, never a value: put the value in .env and list only the name here
```

Declaring the same name twice is refused too.

<Note> **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`. </Note>

<Warning>
  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:

  ```text theme={null}
  connections/twilio_sip.yaml:6: connection "twilio_sip" environment sip_password is not a
    valid environment variable name: use upper case letters, digits, and underscores, and
    do not start with a digit. This field takes a name and never a value. A deployment
    platform exports secrets through a shell, so a bad name would be missing at runtime
    with no error of its own
  ```
</Warning>

## 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:

| How the project reads the name                                                   | Example                                                           |
| -------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| model provider API key from the provider catalogue                               | `OPENAI_API_KEY`, `SLNG_API_KEY`                                  |
| `tracing.provider: langfuse`                                                     | `LANGFUSE_BASE_URL`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` |
| `tracing.provider: coval`                                                        | `COVAL_API_KEY`                                                   |
| a tool's `url_env` or `token_env`                                                | `SALON_API_URL`, `SALON_API_TOKEN`                                |
| a literal `os.environ`, `os.environ.get`, or `os.getenv` read in a local handler | `CRM_API_TOKEN`                                                   |
| a connection's `environment:` value                                              | `SIP_TRUNK_HOSTNAME`                                              |
| a `destinations:` value in `agent.yaml`                                          | `BILLING_PHONE_NUMBER`                                            |

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:

| Name                                                   | Why the compiler knows it                                                                                                                                                 |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REDIS_URL`                                            | some phone routes need Redis                                                                                                                                              |
| `UNMUTE_PUBLIC_URL`, `UNMUTE_OUTBOUND_TOKEN`           | `unmute dev` creates them for supported local phone runs. For the emitted Pipecat Daily helper, you set its exact public HTTPS base URL as the generated runbook directs. |
| `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` | a LiveKit worker needs a server connection                                                                                                                                |
| `DAILY_API_KEY`, `PIPECAT_CLOUD_ORGANIZATION`          | Pipecat routes need them in specific cases                                                                                                                                |

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:

```text wrap theme={null}
livekit: environment variables referenced but not declared in secrets: SIP_TRUNK_HOSTNAME (connections/twilio_sip.yaml environment sip_address)
```

## 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:

<CodeGroup>
  ```text Pipecat, Daily cold transfer theme={null}
  # Environment for pipecat (generated by unmute).
  # Copy this file to `.env` and fill in the values. Never commit `.env`.
  #
  # Only what you supply. The Daily helper's public URL is one
  # of those values because you host it; its exact URL is part of carrier signature
  # validation. compile-report.json lists every name the project needs.

  # agent side: the deployed agent reads these, so they belong in the platform
  # secret set.
  BILLING_PHONE_NUMBER=
  OPENAI_API_KEY=
  SLNG_API_KEY=

  # helper side: telephony_helper.py reads these where you run it. The deployed
  # agent reads none of them, so leave them out of the platform secret set.
  DAILY_API_KEY=
  ```

  ```text LiveKit, no phone route theme={null}
  # Environment for livekit (generated by unmute).
  # Copy this file to `.env` and fill in the values. Never commit `.env`.
  #
  # Only what you supply. The route's own values — the LiveKit connection and the
  # Redis its managed SIP service owns — are not here, because they are not yours
  # to set: `unmute dev` supplies them for a local run, and your platform or
  # operator supplies them at deploy time. "Carrier setup" in README.md says where
  # each one comes from, and compile-report.json lists every name the project
  # needs, including those.

  OPENAI_API_KEY=
  SLNG_API_KEY=
  ```

  ```text SLNG theme={null}
  # There is no .env.example on this target.
  #
  # An slng target compiles to a deployment body, not a project, so there is no
  # file for a secret to sit beside. `unmute deploy` works out what the vault must
  # hold from the compiled body itself. See "The SLNG vault" below.
  ```
</CodeGroup>

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:

```python theme={null}
REQUIRED_ENV = [
    "OPENAI_API_KEY",
    "SLNG_API_KEY",
]
```

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:

```text theme={null}
Missing required environment variable: REDIS_URL
This one is supplied for you: `unmute dev` sets it locally, and your platform
or operator sets it at deploy time. See "Carrier setup" in README.md.
```

## Secrets never flow through templates

`{{...}}` renders [variables](/reference/variables) only. Naming a secret in a
template is a compile error, not a value that leaks at run time:

```text wrap theme={null}
agent.yaml:70: conversation.greeting.text references {{OPENAI_API_KEY}}, but secrets never flow through templates; a secret reaches a tool through its own *_env field
```

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

| Seam                                   | Used for                                 |
| -------------------------------------- | ---------------------------------------- |
| a tool's `webhook.url_env`             | the base URL of an authenticated API     |
| a tool's `webhook.auth.token_env`      | the bearer token or API key              |
| a tool's `mcp.url_env`                 | the MCP server address                   |
| a tool's `mcp.auth.token_env`          | the token that server wants              |
| a connection's `environment:` values   | the carrier account behind a phone route |
| a model's `endpoint_env`               | pointing a model at your own gateway     |
| `os.environ` inside a `local:` handler | a handler that builds its own request    |

```yaml tools/reschedule_appointment.yaml theme={null}
webhook:
  url_env: SALON_API_URL
  path: /customers/{{customer_id}}/appointments
  auth:
    type: bearer
    token_env: SALON_API_TOKEN
```

Every one of these fields holds a name. `token_env` in particular is checked:

```text theme={null}
token_env must be an UPPER_SNAKE environment variable name, never a secret value
```

## 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](/deploy/livekit-cloud#secrets) restart instances without
rebuilding. [Pipecat secret updates](/deploy/pipecat-cloud#secrets-only-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

<Columns cols={2}>
  <Card title="CLI reference" icon="terminal" href="/reference/cli/overview">
    Every command, flag, and exit code.
  </Card>

  <Card title="Going live" icon="rocket" href="/deploy/going-live">
    Deploying with real credentials.
  </Card>
</Columns>
