References & scope

A reference is a dotted path of identifiers used in place of a literal value.

ref ::= ident ( "." ident )+

The first segment is the root. Three roots are supported:

rootresolves toshape
hosta field on a declared host block.host.<name>.<field> (3+ parts)
secretthe resolved plaintext of a declared secret block.secret.<name>.value (exactly 3 parts; value is the only allowed field)
varthe resolved string of a declared var block.var.<name> (exactly 2 parts; no field access)

Anything else errors with unknown reference root.

Host references

Form: host.<name>.<field> (at least three segments).

host "prod" {
  addr = "root@192.0.2.10"
  port = 22
}

resource "ssh_exec" "uptime" {
  host    = host.prod.addr   # -> "root@192.0.2.10"
  command = "uptime"
}

Resolution walks the host's evaluated attrs as a JSON map. Each segment after the host name indexes one level deeper, so nested fields work too:

host "prod" {
  ssh = {
    user = "root"
    addr = "1.2.3.4"
  }
}

resource "ssh_exec" "x" {
  host    = host.prod.ssh.addr   # -> "1.2.3.4"
  command = "true"
}

Secret references

Form: secret.<name>.value. Exactly three segments — value is the only allowed field, since secrets are leaves.

secret "pg_password" {
  from_env = "PG_PASSWORD"
}

resource "docker_container" "db" {
  host  = host.primary.addr
  image = "postgres:16"
  env = {
    POSTGRES_PASSWORD = secret.pg_password.value
  }
}

Secret refs are only allowed inside single-leaf string attrs. They are rejected at config-load time inside system_file.content, system_file.content_file, and system_dir.source_dir — see Secrets: the honesty guard for why.

Var references

Form: var.<name>. Exactly two segments — there is no field access, since a var resolves to a single string. Unlike host and secret, a var reference is almost always written inside a ${...} interpolation, because the resolved value is usually glued into a larger string (an image tag, a build command).

var "app_sha" {
  from_git_sha = "../app"
  default      = "dev"
}

resource "docker_container" "app" {
  host  = host.primary.addr
  image = "app:${var.app_sha}"
  pull  = false
}

A bare var.app_sha (outside ${...}) is also valid wherever a string value is expected, but the interpolation form is the common case. See Vars for sources and precedence.

Error cases

conditionerror
Fewer than 3 segments (host.prod)reference ... too short — expected at least 3 segments
Unknown host name (host.ghost.addr)unknown host ghost in reference ...
Unknown field (host.prod.missing)host prodhas no fieldmissing ...
Unknown secret name (secret.ghost.value)unknown secret ghost in reference ...
Secret field other than value (secret.s.fingerprint)reference to secret s has unsupported field ...
Unknown var name (var.ghost)unknown var ghost in reference ...
Unknown root (provider.x.y)unknown reference root provider``
Any ref inside a host bodyreferences not allowed inside host blocks
Any ref inside a secret bodyreferences not allowed inside secret blocks
Any ref inside a var bodyreferences not allowed inside var blocks
Secret ref inside system_file.content / content_file / system_dir.source_dirsecret reference not allowed in ...

Why only these roots

The evaluator collects all leaf blocks first — hosts, then secrets, then vars — before it evaluates providers and resources (see Evaluation order). By the time a resource body is read, the host, secret, and var scopes are fully populated, so any reference into them resolves immediately.

There is no resource.foo.attr form: resources cannot reference each other. Cross-resource references would need a topological evaluation pass; that has not shipped. Ordering between resources is expressed with depends_on, not references.

References inside strings

A reference may also appear inside a string as ${<ref>} — the placeholder is replaced with the resolved scalar value at evaluation time. The same root rules apply (host.*, secret.*, and var.*), and the same honesty guard fires for forbidden attrs. See String interpolation for the grammar and edge cases.

env = {
  DATABASE_URL = "postgresql://app:${secret.pg.value}@${host.primary.addr}:5432/app"
}

Bare identifiers as values

A single identifier with no dots is not a valid value. Either quote it as a string or extend it to a ref. The parser will report bare identifier ... not allowed as value (use a string or a reference like ...).

resource "ssh_exec" "x" {
  host    = prod          # error: bare identifier
  host    = "prod"        # ok — string
  host    = host.prod.addr  # ok — reference
}