Interpolation and Imports

Contents

Values are not always literal. Secret Config understands two kinds of markup inside the store:

Both are evaluated when the registry is loaded and again on every refresh!, never when a key is read. Reads stay in-memory hash lookups no matter how much markup a value contains.

Both can be turned off with interpolate: false. See Configuration.

Step 1: Substitute an environment variable

${env:NAME} reads an environment variable at load time:

development:
  mysql:
    host: ${env:DB_HOST}

If DB_HOST is not set, loading raises SecretConfig::MissingEnvironmentVariable. Supply a fallback after a comma to make it optional:

development:
  mysql:
    host: ${env:DB_HOST,127.0.0.1}

The default is stripped of surrounding spaces, and cannot itself contain a comma.

This is not the same as the automatic environment variable override, which applies to every key without being asked. ${env:...} is explicit, names a variable that need not match the key, and can be embedded in a larger string:

development:
  mysql:
    host: ${env:DB_HOST,127.0.0.1}:3306

Step 2: Substitute the host or process

development:
  logger:
    file_name: /var/log/my_application_${hostname}.log

Each process now writes to a file named after the machine it is on, from one shared setting.

Token Value
${hostname} Full host name
${hostname:short} Host name up to the first period
${pid} Process id

${pid} gives every process on a host its own file:

development:
  logger:
    file_name: /var/log/my_application_${hostname:short}_${pid}.log

Step 3: Substitute the date or time

development:
  logger:
    file_name: /var/log/my_application_${date}.log
    # => /var/log/my_application_20260808.log
Token Format
${date} %Y%m%d, for example 20260808
${date:format} Any strftime format
${time} %Y%m%d%H%M%S%L, down to milliseconds
${time:format} Any strftime format
development:
  logger:
    file_name: /var/log/my_application_${date:%Y-%m-%d}.log
    # => /var/log/my_application_2026-08-08.log

Note that these are evaluated at load time, not as the process runs. A process that starts before midnight and runs for a week keeps the date it started with, until it is refreshed or restarted.

Step 4: Substitute a random or selected value

${random} produces a URL-safe random value of 32 bytes. ${random:size} sets the size:

development:
  cache:
    namespace: my_application_${random:8}

The size must be a positive integer. Anything else, such as ${random:abc}, raises SecretConfig::ConfigurationError rather than quietly generating nothing.

${select:a,b,c} picks one of the supplied values:

development:
  mongo:
    primary: ${select:mongo1.example.net,mongo2.example.net,mongo3.example.net}

Each process picks its own, which spreads a fleet across several servers without coordinating anything. Values are separated by commas, are stripped of surrounding spaces, cannot contain a comma, and there must be at least two of them.

Both produce a new value on every restart and every refresh. That makes them wrong for anything that has to stay stable. A password written as ${random} becomes a different password every time the process starts, which is a locked-out application rather than a secure one.

For a value that must be generated once and then kept, use the command line’s __generate__ token, which materializes the value during an import and writes it to the store. See Command Line.

Step 5: Escape a literal ${

A value that should contain ${...} literally is escaped by doubling the dollar sign:

development:
  templates:
    greeting: $${name}, welcome back
    # => "${name}, welcome back"

Only ${ starts an interpolation. A bare $, or $$ on its own, is left alone.

Step 6: Share settings with __import__

A key named __import__ copies the settings under another path into its parent node. It is useful when several nodes share most of their settings and differ in only a few:

test:
  my_application:
    mongo:
      database:  secret_config_test
      primary:   127.0.0.1:27017
      secondary: 127.0.0.1:27018

    mongo_reporting:
      __import__: mongo
      primary:    reporting.example.net:27017

mongo_reporting ends up with database and secondary copied from mongo, and keeps its own primary. The __import__ key itself is removed and never appears in the registry.

How imports resolve:

The common use is a shared base path for a fleet of tenants, where each tenant imports the common settings and overrides the handful that are its own.

A setting that is both a value and a branch

A node can have a value of its own and still have children under it. In AWS SSM Parameter Store this happens whenever both /production/my_application/logger and /production/my_application/logger/level exist as parameters.

YAML cannot express that directly, so Secret Config uses the reserved key __value__ for the value that belongs to the node itself:

test:
  my_application:
    logger:
      __value__: info
      level:     debug

Read it as the node, not as __value__:

SecretConfig.fetch("logger")
# => "info"

SecretConfig.fetch("logger/level")
# => "debug"

SecretConfig.configuration and the CLI’s --export render such nodes back out with __value__, so an export can be edited and imported again without losing the node’s own value.

Reference

All interpolation tokens:

Token Result
${date} Current date as %Y%m%d
${date:format} Current date in the supplied strftime format
${time} Current date and time as %Y%m%d%H%M%S%L
${time:format} Current date and time in the supplied strftime format
${env:name} The named environment variable. Raises MissingEnvironmentVariable when unset
${env:name,default} The named environment variable, or default when unset
${hostname} Full host name
${hostname:short} Host name up to the first period
${pid} Process id
${random} URL-safe random value, 32 bytes
${random:size} URL-safe random value of size bytes
${select:a,b,c} One of the supplied values, chosen at random
$${...} A literal ${...}, not interpolated

That table is the whole list. Any other token raises SecretConfig::InvalidInterpolation, including the names of methods every Ruby object has.

Adding a token

A token comes from a subclass of SecretConfig::StringInterpolator that declares it and implements a method of the same name. The arguments after the : arrive as strings, split on , and stripped:

class MyInterpolator < SecretConfig::SettingInterpolator
  interpolation :region

  def region(default = "us-east-1")
    ENV.fetch("AWS_REGION", default)
  end
end

${region} and ${region:eu-west-1} then resolve through it.

The interpolation declaration is what makes the method reachable; a method without one is never called. That is deliberate. Dispatch used to accept any method the interpolator responded to, which included everything inherited from Object, so a value such as ${send:...} in the central store could run arbitrary code in every process that loaded it. Declaring each token keeps the reachable surface to exactly what is listed.

What the store is trusted to do

Interpolation runs against whatever the central store contains, so writing a setting is not quite the same as writing a plain string. Two tokens are worth knowing about when deciding who may write to a path:

Neither can run code, and no token can: dispatch is limited to the table above. But both mean that write access to a path should be treated as roughly equivalent to read access to the environment and the store of every process that loads it. Grant it accordingly, and prefer separate IAM policies per environment over one that spans them.

For a store you do not fully control, interpolate: false turns both off and returns every value exactly as stored, tokens and all:

SecretConfig.use(:ssm, path: "/production/my_application", interpolate: false)

That is a blunt instrument, since it also disables ${random}, ${hostname} and imports. See Configuration.

Reserved keys:

Key Meaning
__import__ Copy the settings at the given path into this node
__value__ The value belonging to a node that also has children
__generate__ Import-time random value. Handled by the CLI, not by interpolation

Next steps