# Secret references

A secret's value can reference another secret:

```bash
seekrit secrets set DB_HOST db.internal --env production
seekrit secrets set DB_PASSWORD 's3cr3t' --env production
seekrit secrets set DATABASE_URL 'postgres://app:${DB_PASSWORD}@${DB_HOST}:5432/app' --env production
```

At read time, every consumer sees the assembled value:

```bash
seekrit export --env production
# DATABASE_URL=postgres://app:s3cr3t@db.internal:5432/app
# DB_HOST=db.internal
# DB_PASSWORD=s3cr3t
```

Nothing is duplicated. Rotate `DB_PASSWORD` and `DATABASE_URL` follows on the
next read — no re-encryption, no second write, nothing to forget.

## How it works (and why it stays zero-knowledge)

The reference is stored **literally**. What seekrit holds is the ciphertext of
the text `postgres://app:${DB_PASSWORD}@${DB_HOST}:5432/app` — the server can no
more read that than any other secret, and it never resolves anything.

Expansion happens **on the client, after the layers are merged**, in the same
process that decrypted the values:

```
group secrets  <  app-env secrets  <  .env file        (merge, then expand)
```

Two consequences worth knowing:

- A reference resolves to whatever **won** the merge. If a group defines
  `DB_HOST` and the app environment overrides it, `${DB_HOST}` picks up the app's
  value — references compose with [groups](/docs/guides/environments) rather than
  fighting them.
- `process.env` is **not** a reference source. `seekrit run` layers the live
  shell on top *after* expansion, so `${HOME}` in a secret is not replaced by the
  host's home directory. This keeps a secret's value the same wherever it runs.

## The rules

| You write | You get |
| --- | --- |
| `${NAME}` | The value of `NAME` in the merged set. |
| `${NAME}` where `NAME` is not defined | Left exactly as written, and reported (see below). |
| `$${NAME}` | The literal text `${NAME}` — the escape. |
| `${FOO:-default}`, `${1}`, `${a.b}` | Left exactly as written: only valid secret names (`[A-Za-z_][A-Za-z0-9_]*`) are references, so shell and CI template syntax passes through. |
| `p$$w0rd$` | Unchanged. A `$` only starts a reference when followed by `{`. |

References are **recursive** — a referenced value may reference others — and a
**cycle** is an error (`A` → `B` → `A` has no answer, and every name in it
exists, so it can only be a mistake).

An unknown name is deliberately *not* an error: plenty of real config contains
text like `${GITHUB_SHA}` meant for something else downstream, and one such value
shouldn't break an environment's resolve. To catch typos, ask:

```bash
seekrit run --explain -- ./app
# DATABASE_URL  app:api/production  (interpolated)
# DB_HOST       group:common@production
# DB_PASSWORD   app:api/production
#
# unresolved reference(s), left as literal text: DB_HSOT
```

## Where it applies

Expansion is part of the read path, so it happens everywhere secrets are read —
each of these does it locally, and none of them needs a new permission:

| Reader | Notes |
| --- | --- |
| `seekrit run` / `seekrit export` | Expanded after merging; `--no-interpolate` opts out. |
| `seekrit secrets get NAME` | Expanded against *that environment's own* secrets — it doesn't fetch the composed group layers. `--raw` prints the stored text, and so does `--version <n>` (splicing today's values into an old template would be misleading). Use `seekrit export` for the fully-layered view. |
| [`seekrit-run` launcher](/docs/guides/run) | Same behavior and the same `--no-interpolate` flag. |
| [Language SDKs](/docs/guides/sdks) | `resolve()` returns expanded values; construct the client with `interpolate: false` (Python `interpolate=False`, Go `WithInterpolate(false)`) for the stored text. |
| [Agent egress proxy](/docs/guides/agent-proxy) | Expanded at startup, so a `{{seekrit:NAME}}` placeholder is substituted with the finished value. |
| [Kubernetes (ESO)](/docs/guides/kubernetes) | The in-cluster resolver expands before ESO writes the `Secret`. |
| [MCP servers](/docs/guides/ai-agents) | `run_command`, `export_env`, and a revealed `get_secret` all expand. |
| Dashboard | Shows the value as **stored**; the editor tells you which names a value references. |

Because expansion is client-side, a reader that predates this feature simply
sees the literal text — nothing breaks, it just isn't assembled.

## When not to use it

- **Don't reference across environments.** References resolve inside one
  resolved environment. A staging secret cannot pull from production — by
  design, since that would cross a key-grant boundary.
- **Careful with `secrets get NAME | secrets set NAME -`.** That would store the
  *expanded* text and freeze the reference. Pass `--raw` when you are copying a
  value around.
- **A password that happens to contain `${`** is almost always fine: only
  `${VALID_NAME}` is a reference, and only when that name exists in the same
  environment. If a stored value really does contain such a sequence and must
  survive verbatim, escape it as `$${…}`.
