Skip to content

Configuration

envhaul reads a YAML file, envhaul.yaml by default.

Finding the config file

envhaul uses the first match of:

  1. --config / -c
  2. the ENVHAUL_CONFIG environment variable
  3. envhaul.yaml, envhaul.yml, .envhaul.yaml or .envhaul.yml in the current directory or a parent, stopping at the repository root (the first directory containing .git)

Use one file per environment, for example envhaul.yaml for development and envhaul.staging.yaml for staging:

envhaul -c envhaul.staging.yaml sync

Editor support

Add this first line to get completion and validation in editors that use the YAML language server (VS Code, JetBrains, Neovim, Zed):

# yaml-language-server: $schema=https://raw.githubusercontent.com/davidtucker/envhaul/main/schema/envhaul.schema.json

Top-level keys

Key Required Description
version no Schema version. Only 1 exists; it is the default.
name no Environment name, shown in generated file headers.
description no Free text.
aws_profile no Default AWS profile for references without @profile. Overridden by --profile. When unset, the SDK default applies (AWS_PROFILE, then default).
aws_region no Region for every request. Overridden by --region. When unset, each profile's own region from ~/.aws/config is used.
ssm_prefix no Joined onto SSM paths that don't start with /. Must start with /.
secret_prefix no Prepended to Secrets Manager ids that aren't ARNs, e.g. myapp/dev/.
vault no HashiCorp Vault / OpenBao settings.
onepassword no 1Password settings.
gcp no Google Cloud Secret Manager settings.
azure no Azure Key Vault settings.
components yes Map of component name to settings. Each component writes one file.

Source settings

Sources other than AWS each have an optional block of default settings and named profiles:

vault:
  address: https://vault.example.com
  profiles:
    prod:
      address: https://vault.prod.example.com
      token_env: VAULT_PROD_TOKEN

gcp:
  project: my-dev-project
  • Defaults apply to every reference to that source.
  • @name on a reference picks a profile. The profile's settings override the defaults, and anything it doesn't set falls back to them.
  • @name with no matching profile is used as the source's natural scope: a Vault namespace, a 1Password account, a GCP project or an Azure tenant. So {{gcp:db-password@my-other-project}} works without any profile.

Each source's page lists its settings. validate rejects unknown settings and profile names with file:line:col errors.

Components

components:
  api:
    description: Backend API     # optional, shown in shell completion
    env_file: services/api/.env  # relative to the config file
    vars:
      KEY: value
Key Required Description
env_file yes Output path. Relative paths resolve against the config file's directory, not the current directory. Two components can't write the same file.
vars yes Variables, written in the order listed.
description no Free text.

Component names can't contain spaces or slashes.

Variables

Variable names must match [A-Za-z_][A-Za-z0-9_]*. Values can be:

  • inline: ENV: dev
  • a reference: API_KEY: "{{ssm:api/API_KEY}}"
  • a template mixing both: DB_URL: "postgresql://{{secret:db#user}}:{{secret:db#pass|urlencode}}@host/db"

See References and pipes for the syntax.

Values are taken literally

envhaul keeps every scalar exactly as written, so 1.10, 0755, yes and 1e3 reach your .env unchanged instead of becoming 1.1, 493, true or 1000. An empty value, ~ or null becomes an empty string. Quote values that contain {{, # or :.

Generated files

Each env file starts with a header naming the config it came from, followed by the variables in config order:

# Generated by envhaul from envhaul.yaml (dev). Do not edit; changes are overwritten.
# Regenerate with: envhaul sync api

APP_ENV=development
DATABASE_URL=postgresql://admin:p%40ss@localhost/acme
GREETING='hello world'
  • envhaul owns these files: every sync rewrites them completely. Put local-only values in the config, or in a separate file your tooling loads (such as .env.local).
  • Files are written atomically (a temp file, then rename) with mode 0600, and are left untouched when the content hasn't changed.
  • Values are written bare when they only contain safe characters, in single quotes when they contain no ' or newline, and in double quotes with \\, \", \$, \n and \r escapes otherwise. Docker Compose reads every form back exactly. $ is escaped inside double quotes because Compose, shells and Vite expand variables there; python-dotenv and Node dotenv keep that backslash, so a value with a $ that also contains a ' or a newline reaches them with \$.
  • Some loaders expand variables even in single-quoted values: python-dotenv expands ${NAME}, and Vite expands $NAME and ${NAME}. If a secret can contain those, turn interpolation off in the loader (python-dotenv: interpolate=False).
  • sync warns when a generated file isn't ignored by git.

Missing values

A reference to a parameter or secret that doesn't exist (or a #key that a JSON secret doesn't have) is written as an empty string with a warning. This keeps optional integrations from blocking everyone else. To change that:

  • add |default:value to the reference to provide a fallback, or
  • run with --strict to fail instead (useful in CI).

Errors such as expired credentials or denied access always fail, and nothing is written.

Full example

# yaml-language-server: $schema=https://raw.githubusercontent.com/davidtucker/envhaul/main/schema/envhaul.schema.json
#
# Example envhaul configuration for a project with an API, a web frontend and
# infrastructure code. Each component becomes one .env file.
#
#   envhaul -c examples/envhaul.yaml validate   # check this file (offline)
#   envhaul -c examples/envhaul.yaml list       # show where every value comes from
#   envhaul -c examples/envhaul.yaml sync       # fetch values and write the .env files

version: 1
name: dev
description: Local development environment
aws_profile: acme-dev        # default profile for references without @profile
aws_region: us-east-1
ssm_prefix: /acme/dev        # relative SSM paths resolve under this prefix

# Settings for sources other than AWS (all optional).
vault:
  address: https://vault.acme.example
onepassword:
  account: acme.1password.com
gcp:
  project: acme-dev
azure:
  vault: acme-dev-kv

components:
  api:
    description: Backend API
    env_file: services/api/.env
    vars:
      # Inline values live in version control.
      APP_ENV: development
      LOG_LEVEL: debug
      CORS_ORIGINS: "http://localhost:5173,http://127.0.0.1:5173"
      CACHE_TTL_SECONDS: "300"

      # SSM Parameter Store. "api/STRIPE_KEY" resolves to /acme/dev/api/STRIPE_KEY.
      STRIPE_SECRET_KEY: "{{ssm:api/STRIPE_SECRET_KEY}}"

      # Another AWS account: shared third-party keys live in acme-shared.
      OPENAI_API_KEY: "{{ssm:api/OPENAI_API_KEY@acme-shared}}"
      SENTRY_DSN: "{{ssm:api/SENTRY_DSN@acme-shared}}"

      # Secrets Manager: pick keys from a JSON secret and URL-encode the password.
      DATABASE_URL: "postgresql://{{secret:acme/dev/db#username}}:{{secret:acme/dev/db#password|urlencode}}@localhost:5432/acme"

      # Optional values: fall back when the parameter does not exist.
      FEATURE_FLAGS: "{{ssm:api/FEATURE_FLAGS|default:none}}"

  web:
    description: Vite frontend
    env_file: web/.env
    vars:
      VITE_API_BASE_URL: "http://localhost:8000"
      VITE_SENTRY_DSN: "{{ssm:api/SENTRY_DSN@acme-shared}}"   # fetched once, used twice

  infrastructure:
    description: CDK app
    env_file: infrastructure/.env
    vars:
      DEPLOY_ENV: dev
      AWS_REGION: us-east-1
      # Absolute paths ignore ssm_prefix.
      HOSTED_ZONE_ID: "{{ssm:/acme/global/route53/HOSTED_ZONE_ID}}"
      GITHUB_TOKEN: "{{secret:acme/ci/github#token|trim}}"

  tools:
    description: Local developer tooling, pulling from several secret stores
    env_file: tools/.env
    vars:
      SIGNING_KEY: "{{vault:payments/keys#signing}}"     # HashiCorp Vault
      GITHUB_TOKEN: "{{op:Engineering/GitHub/token}}"    # 1Password
      MAPS_API_KEY: "{{gcp:maps-api-key}}"               # Google Cloud
      STORAGE_KEY: "{{azkv:storage-account-key}}"        # Azure Key Vault