String interpolation

A double-quoted string may embed ${<ref>} placeholders. Each placeholder is replaced at config-load time with the resolved value of the reference (see References & scope), coerced to its string form.

env = {
  DATABASE_URL = "postgresql://app:${secret.pg.value}@${host.primary.addr}:5432/app"
}
string         ::= '"' ( char | escape | interp )* '"'
interp         ::= "${" ref "}"
ref            ::= ident ( "." ident )+
escape         ::= "\\\"" | "\\\\" | "\\n" | "\\r" | "\\t" | "\\${"

A string that contains no ${...} lexes as a plain string literal. A string with at least one ${...} lexes as a template (alternating literal and interpolation segments) and the evaluator concatenates the resolved parts.

Allowed refs inside ${...}

Any reference the References & scope rules accept:

  • host.<name>.<field> — including nested fields like host.prod.ssh.addr.
  • secret.<name>.value — the resolved plaintext flows in. State stores a substring marker (see Secrets).

A bare identifier inside ${...} is rejected:

${foo}        ← error: "bare identifier `foo` not allowed in `${...}`"
${secret.foo} ← error: too short (secret refs need exactly 3 segments)
${a.${b}}     ← error: "nested `${` is not supported"
${}           ← error: "empty `${}` is not allowed"

Scalar coercion

The reference must resolve to a scalar — string, number, or bool. Numbers render via their JSON form (4000, 1.5), bools as true / false. Lists, maps, or null error at evaluation time:

cannot interpolate non-scalar value `${host.h.tags}` into a string
  (refs inside `${...}` must resolve to a string, number, or bool)

Escaping

\${ produces a literal ${ in the output and is not a placeholder. There is no other \$ escape — a bare \$ followed by anything else is a lex error.

shell_var = "literal \${HOME} not stratum"   # -> "literal ${HOME} not stratum"

The honesty guard still applies

Embedding secret.X.value inside a string interpolation lands in the same forbidden-attr check as a bare secret reference. The check fires by attr name, not by ref form:

# Both are rejected — system_file.content is in SECRET_FORBIDDEN_ATTRS.
content = secret.s.value
content = "prefix ${secret.s.value} suffix"

See Secrets: the honesty guard for the list of forbidden (kind, attr) pairs and the reasoning.

Where interpolation is most useful

Connection strings and other glued-together values where a bare ref doesn't fit:

resource "docker_container" "api" {
  host  = host.primary.addr
  image = "api:dev"
  env = {
    # Embed a secret inside a URI — bare `secret.pg.value` would only work
    # if the whole env value were the password.
    DATABASE_URL = "postgresql://app:${secret.pg.value}@db:5432/app"

    # Combine multiple host fields.
    INTERNAL_API = "http://${host.primary.addr}:4000"
  }
}

When a secret is interpolated into a larger string, state stores the value with an inline substring marker ("--requirepass <secret:pg:sha256:HEX>") instead of replacing the whole leaf with an object marker. See Secrets: substring redaction.