Configuration
Hush is configured through a hush.yaml file in your repository root.
Basic Structure
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 overridesSource Merging Order
When decrypting, sources are merged in order with later values overriding earlier ones:
- shared - Base variables
- environment (development or production) - Environment overrides
- .env.local (unencrypted) - Personal overrides (not committed)
Example
# .env (shared)API_URL=https://api.example.comDEBUG=false
# .env.developmentAPI_URL=http://localhost:8787DEBUG=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
| Field | Description |
|---|---|
name | Identifier for the target (used in output messages) |
path | Directory where the output file should be written |
format | Output format: dotenv, wrangler, json, shell, or yaml |
Optional Fields
| Field | Description |
|---|---|
include | Glob patterns for variables to include |
exclude | Glob patterns for variables to exclude |
push_to | Configuration 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 nameExample
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 onesOutput Formats
dotenv
Standard .env file format:
DATABASE_URL=postgres://localhost/mydbAPI_KEY=sk_test_xxxOutput file: .env.development or .env.production
wrangler
Cloudflare Wrangler format for Workers development.
Recommended: Use hush run to keep secrets in memory:
hush run -t api -- wrangler devHush 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:
DATABASE_URL=postgres://localhost/mydbAPI_KEY=sk_test_xxxTroubleshooting: 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:
-
Delete
.dev.vars:Terminal window rm .dev.vars -
Use
hush runinstead ofhush decrypt:Terminal window hush run -t api -- wrangler dev -
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/shexport 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:
HOST=localhostPORT=3000BASE_URL=http://${HOST}:${PORT}Default Values
Provide a fallback if the variable is not set:
# Use PORT from secrets, or 3000 if missingPORT=${PORT:-3000}
# Use API_URL, or fallback to localhostAPI_URL=${API_URL:-http://localhost:8080}System Environment Variables
Explicitly read from the system environment (e.g., CI variables, machine info):
# Read CI variable from system environmentIS_CI=${env:CI}
# Read HOSTNAME from systemMACHINE_NAME=${env:HOSTNAME}Self-Reference
Useful for providing defaults to variables that might be overridden:
# 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
| Scenario | Use | Why |
|---|---|---|
”All NEXT_PUBLIC_* vars to web app” | Push | Pattern-based, zero maintenance |
”Rename API_URL → EXPO_PUBLIC_API_URL” | Pull | Transformation required |
”Add default PORT=3000 if not set” | Pull | Default values |
”Combine vars: ${HOST}:${PORT}” | Pull | Composition |
| ”New vars auto-flow to targets” | Push | No template updates needed |
| ”Subdirectory controls its own deps” | Pull | Explicit 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 automaticallyPros:
- 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.
# apps/mobile/.env (committed - just a template)EXPO_PUBLIC_API_URL=${API_URL} # RenameEXPO_PUBLIC_DEBUG=${DEBUG:-false} # Default valuePORT=${PORT:-3000} # Self-reference with defaultPros:
- 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):
API_URL=https://api.example.comSTRIPE_KEY=sk_test_xxxSubdirectory (.env):
# Map root secrets to framework-specific prefixesNEXT_PUBLIC_API_URL=${API_URL}
# Define local defaultsPORT=${PORT:-3000}When you run hush run in the subdirectory:
- Root secrets are decrypted
- Local
.envtemplate is loaded - Variables in the local template are resolved against root secrets
- 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:
creation_rules: - encrypted_regex: '.*' age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxMultiple 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
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