Skip to content

Configuration

Hush is configured through a hush.yaml file in your repository root.

Basic Structure

hush.yaml
sources:
shared: .env
development: .env.development
production: .env.production
targets:
- name: root
path: .
format: dotenv
- name: app
path: ./packages/app
format: dotenv
include:
- EXPO_PUBLIC_*
- name: api
path: ./packages/api
format: wrangler
exclude:
- EXPO_PUBLIC_*

Sources

The sources section defines your .env files and how they map to environments.

sources:
shared: .env # Base variables for all environments
development: .env.development # Development-specific overrides
production: .env.production # Production-specific overrides

Source Merging Order

When decrypting, sources are merged in order with later values overriding earlier ones:

  1. shared - Base variables
  2. environment (development or production) - Environment overrides
  3. .env.local (unencrypted) - Personal overrides (not committed)

Example

Terminal window
# .env (shared)
API_URL=https://api.example.com
DEBUG=false
# .env.development
API_URL=http://localhost:8787
DEBUG=true
# Result for development:
# API_URL=http://localhost:8787 (overridden)
# DEBUG=true (overridden)

Targets

The targets section defines where secrets should be written and in what format.

Required Fields

FieldDescription
nameIdentifier for the target (used in output messages)
pathDirectory where the output file should be written
formatOutput format: dotenv, wrangler, json, shell, or yaml

Optional Fields

FieldDescription
includeGlob patterns for variables to include
excludeGlob patterns for variables to exclude
push_toConfiguration for pushing secrets to Cloudflare Pages

Push Configuration

The push_to field enables pushing secrets to Cloudflare Pages:

targets:
- name: app
path: ./app
format: dotenv
include:
- NEXT_PUBLIC_*
push_to:
type: cloudflare-pages
project: my-pages-project # Your Cloudflare Pages project name

Example

targets:
- name: app
path: ./packages/app
format: dotenv
include:
- EXPO_PUBLIC_*
- NEXT_PUBLIC_*
- VITE_*

Include/Exclude Patterns

Use glob patterns to control which variables reach each target.

Include

Only variables matching any include pattern are written:

targets:
- name: client
path: ./client
format: dotenv
include:
- EXPO_PUBLIC_* # Variables starting with EXPO_PUBLIC_
- NEXT_PUBLIC_* # Variables starting with NEXT_PUBLIC_

Exclude

All variables except those matching exclude patterns are written:

targets:
- name: server
path: ./server
format: wrangler
exclude:
- EXPO_PUBLIC_* # Exclude client-only variables
- NEXT_PUBLIC_*

Combined

When both are specified, include is applied first, then exclude:

targets:
- name: api
path: ./api
format: json
include:
- API_* # Include all API_* variables
exclude:
- API_DEBUG_* # Except debugging ones

Output Formats

dotenv

Standard .env file format:

.env.development
DATABASE_URL=postgres://localhost/mydb
API_KEY=sk_test_xxx

Output file: .env.development or .env.production

wrangler

Cloudflare Wrangler format for Workers development.

Recommended: Use hush run to keep secrets in memory:

Terminal window
hush run -t api -- wrangler dev

Hush automatically sets CLOUDFLARE_INCLUDE_PROCESS_ENV=true for you, which tells Wrangler to read secrets from the environment.

Alternative: If you must write to disk, hush decrypt --force creates .dev.vars:

.dev.vars
DATABASE_URL=postgres://localhost/mydb
API_KEY=sk_test_xxx

Troubleshooting: Wrangler not seeing env vars?

Wrangler has a strict rule: if a .dev.vars file exists (even empty!), it ignores CLOUDFLARE_INCLUDE_PROCESS_ENV entirely.

If your secrets aren’t showing up:

  1. Delete .dev.vars:

    Terminal window
    rm .dev.vars
  2. Use hush run instead of hush decrypt:

    Terminal window
    hush run -t api -- wrangler dev
  3. Update Wrangler: Older versions may not support CLOUDFLARE_INCLUDE_PROCESS_ENV:

    Terminal window
    npm update wrangler

json

JSON object format:

{
"DATABASE_URL": "postgres://localhost/mydb",
"API_KEY": "sk_test_xxx"
}

Output file: .env.development.json or .env.production.json

shell

Sourceable shell exports:

#!/bin/sh
export DATABASE_URL="postgres://localhost/mydb"
export API_KEY="sk_test_xxx"

Output file: .env.development.sh or .env.production.sh

Variable Interpolation

Reference other variables using ${VAR} syntax. Hush v4 supports powerful expansion features:

Basic Reference

Reference variables defined in the same file or from the root secrets:

.env
HOST=localhost
PORT=3000
BASE_URL=http://${HOST}:${PORT}

Default Values

Provide a fallback if the variable is not set:

Terminal window
# Use PORT from secrets, or 3000 if missing
PORT=${PORT:-3000}
# Use API_URL, or fallback to localhost
API_URL=${API_URL:-http://localhost:8080}

System Environment Variables

Explicitly read from the system environment (e.g., CI variables, machine info):

Terminal window
# Read CI variable from system environment
IS_CI=${env:CI}
# Read HOSTNAME from system
MACHINE_NAME=${env:HOSTNAME}

Self-Reference

Useful for providing defaults to variables that might be overridden:

Terminal window
# If PORT is defined in root secrets, use it. Otherwise use 3000.
PORT=${PORT:-3000}

Push vs Pull: Choosing the Right Approach

Hush v4 supports two architectures for distributing secrets in monorepos. Choose based on your use case.

Quick Decision Guide

ScenarioUseWhy
”All NEXT_PUBLIC_* vars to web app”PushPattern-based, zero maintenance
”Rename API_URLEXPO_PUBLIC_API_URLPullTransformation required
”Add default PORT=3000 if not set”PullDefault values
”Combine vars: ${HOST}:${PORT}PullComposition
”New vars auto-flow to targets”PushNo template updates needed
”Subdirectory controls its own deps”PullExplicit ownership

Push Model (include/exclude in hush.yaml)

Best for: Pattern-based filtering where new vars should auto-flow.

targets:
- name: web
path: ./apps/web
format: dotenv
include:
- NEXT_PUBLIC_* # All matching vars flow automatically

Pros:

  • Zero maintenance when adding new vars matching the pattern
  • Centralized control in hush.yaml
  • Great for “all client vars go here” scenarios

Cons:

  • Cannot rename or transform vars
  • Cannot add defaults
  • Cannot combine vars

Pull Model (subdirectory .env templates)

Best for: Transformation, renaming, defaults, or when subdirectories own their config.

Terminal window
# apps/mobile/.env (committed - just a template)
EXPO_PUBLIC_API_URL=${API_URL} # Rename
EXPO_PUBLIC_DEBUG=${DEBUG:-false} # Default value
PORT=${PORT:-3000} # Self-reference with default

Pros:

  • Can rename vars (API_URL → EXPO_PUBLIC_API_URL)
  • Can provide defaults (${VAR:-default})
  • Can compose vars (${HOST}:${PORT})
  • Subdirectory explicitly declares what it needs

Cons:

  • Must update template when adding new vars
  • More files to maintain

Pull-Based Architecture (v4)

In monorepos, you can define .env templates in your subdirectories that pull values from root secrets.

Root Secrets (.env):

Terminal window
API_URL=https://api.example.com
STRIPE_KEY=sk_test_xxx

Subdirectory (.env):

Terminal window
# Map root secrets to framework-specific prefixes
NEXT_PUBLIC_API_URL=${API_URL}
# Define local defaults
PORT=${PORT:-3000}

When you run hush run in the subdirectory:

  1. Root secrets are decrypted
  2. Local .env template is loaded
  3. Variables in the local template are resolved against root secrets
  4. Result is injected into the command

This avoids the “prefix soup” problem where you have to duplicate secrets like NEXT_PUBLIC_API_URL, EXPO_PUBLIC_API_URL, VITE_API_URL, etc. in your root .env.

SOPS Configuration

Hush uses SOPS for encryption. Configure it in .sops.yaml:

.sops.yaml
creation_rules:
- encrypted_regex: '.*'
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Multiple Keys

For team environments, use multiple age keys:

creation_rules:
- encrypted_regex: '.*'
age: >-
age1alice...,
age1bob...,
age1charlie...

Anyone with any of these keys can decrypt the secrets.

Complete Example

hush.yaml
sources:
shared: .env
development: .env.development
production: .env.production
targets:
# Root gets everything for scripts
- name: root
path: .
format: dotenv
# Expo app only gets public variables
- name: mobile
path: ./packages/mobile
format: dotenv
include:
- EXPO_PUBLIC_*
# Next.js web app gets its public variables
- name: web
path: ./packages/web
format: dotenv
include:
- NEXT_PUBLIC_*
# API gets everything except public variables
- name: api
path: ./packages/api
format: wrangler
exclude:
- EXPO_PUBLIC_*
- NEXT_PUBLIC_*
# Shared library gets specific variables as JSON
- name: shared
path: ./packages/shared
format: json
include:
- API_URL
- APP_NAME