Commands
Overview
hush <command> [options]Hush keeps secrets encrypted at rest. The primary way to use secrets is hush run -- <command>, which decrypts to memory and injects environment variables—secrets never touch the disk.
Command Categories
| Category | Commands | Description |
|---|---|---|
| Primary (AI-Safe) | run, set, edit, inspect, has | Safe for AI assistants—never expose secret values |
| Setup | init, encrypt, status, skill | Configuration and encryption |
| Deployment | push, check | CI/CD and cloud deployment |
| Debugging | resolve, trace | Debug secret filtering and routing |
Global Options
| Option | Description |
|---|---|
-e, --env <env> | Environment: development or production (default: development) |
-r, --root <dir> | Start directory for project mode, execution directory for run |
--global | Use the explicit global store at ~/.hush |
-h, --help | Show help message |
-v, --version | Show version number |
run
Run a command with secrets injected as environment variables. This is the primary way to use secrets. Secrets are decrypted to memory only—they never touch the disk.
# Run with development secrets (default)hush run -- npm start
# Run with production secretshush run -e production -- npm buildhush run -e prod -- npm build
# Run with secrets filtered for a specific targethush run -t api -- wrangler devhush run --target web -- npm start
# Run with global secrets onlyhush run --global -- npm startOptions
| Option | Description |
|---|---|
-e, --env <env> | Environment: development or production |
-t, --target <name> | Filter secrets for a specific target from hush.yaml |
-- <command> | The command to run (everything after --) |
How It Works
- Decrypts secrets to memory (never written to disk)
- Merges shared → environment → local secrets
- Resolves subdirectory templates (if present)
- Filters root secrets based on target config (if matched)
- Merges template vars + filtered root secrets (additive)
- Spawns the child process with secrets as environment variables
- Inherits stdin/stdout/stderr for full interactivity
When --global is used, Hush reads only from ~/.hush. It does not merge project secrets.
Examples
# Local developmenthush run -- npm run dev
# Production buildhush run -e prod -- npm run build
# Wrangler with API secrets onlyhush run -t api -- wrangler dev
# Docker with secretshush run -- docker compose up
# Any command that needs secretshush run -- node scripts/migrate.jsset
Set a secret. Supports inline values, interactive prompts, or piped input.
# Inline value (recommended for AI agents)hush set DATABASE_URL "postgres://user:pass@host/db"hush set STRIPE_KEY "sk_live_xxx" -e production
# Interactive prompt (for users)hush set DATABASE_URLhush set API_KEY -e production
# Set in local overrides (.hush.local.encrypted)hush set MY_OVERRIDE --local
# Set in the explicit global storehush set --global OPENAI_API_KEY
# Bootstrap the global store without a separate init stephush keys generate --globalhush set --global OPENAI_API_KEY
# Piped input (for scripts/automation)echo "my-secret" | hush set MY_KEYcat cert.pem | hush set CERTIFICATEOptions
| Option | Description |
|---|---|
-e, --env <env> | Target environment file (default: shared .hush.encrypted) |
--local | Set in .hush.local.encrypted (personal overrides, not committed) |
--gui | Use a native GUI dialog for input instead of terminal or piped input |
--global | Set the secret in ~/.hush instead of the current project store |
Input Methods (Priority Order)
- Inline value:
hush set KEY "value"- value provided directly - Piped input:
echo "value" | hush set KEY- reads from stdin - GUI dialog: Opens when
--guiflag is used - Interactive prompt: Terminal prompt with visible input
How It Works
- Gets value from inline arg, pipe, GUI, or prompt
- In global mode, auto-creates
~/.hush/hush.yamland~/.hush/.sops.yamlwhen the global key already exists - Decrypts the target file
- Sets or updates the key
- Re-encrypts the file
- Confirms success with character count (no value shown)
For global mode, Hush now binds encryption to ~/.hush/.sops.yaml explicitly, so running hush set --global from inside another repo still uses the global recipient instead of a repo-local .sops.yaml.
Example Output
$ hush set DATABASE_URL "postgres://localhost/mydb"✓ DATABASE_URL set in .hush.encrypted (25 chars)
$ hush set API_KEYEnter value for API_KEY: sk_test_xxx✓ API_KEY set in .hush.encrypted (14 chars)edit
Edit secrets in your $EDITOR. Opens the decrypted file, and re-encrypts on save.
# Edit shared secretshush edit
# Edit environment-specific secretshush edit developmenthush edit productionhush edit devhush edit prod
# Edit local overrideshush edit localHow It Works
- Decrypts the target file to a temporary location
- Opens in
$EDITOR(vim, nano, code —wait, etc.) - Re-encrypts when you save and close
- Deletes the temporary file
init
Generate a hush.yaml configuration file with auto-detected targets.
hush init
# Initialize the explicit global storehush init --globalThis command scans your monorepo for packages with package.json files and creates an initial configuration.
When --global is used, Hush initializes ~/.hush with a single root target and a dedicated global key identity.
If you already ran hush keys generate --global or hush keys setup --global, hush set --global KEY ... can now bootstrap the missing global config files automatically.
Example Output
# hush.yaml (generated)sources: shared: .hush development: .hush.development production: .hush.production
targets: - name: root path: . format: dotenv - name: app path: ./packages/app format: dotenv - name: api path: ./packages/api format: wranglerencrypt
Encrypt source .hush files to .hush.encrypted files.
hush encryptWhat Gets Encrypted
Based on your hush.yaml sources configuration:
.hush→.hush.encrypted.hush.development→.hush.development.encrypted.hush.production→.hush.production.encrypted
After encryption, the plaintext .hush files are automatically deleted.
inspect
List all variables with masked values. Safe for AI agents.
hush inspecthush inspect -e productionExample Output
Secrets for development:
DATABASE_URL = post****************... (45 chars) STRIPE_SECRET_KEY = sk_t****************... (32 chars) API_KEY = (not set)
Total: 3 variables
Target distribution:
root (.) - 3 vars app (./app/) - 1 vars include: EXPO_PUBLIC_* api (./api/) - 2 vars exclude: EXPO_PUBLIC_*This lets AI agents reason about your configuration without seeing actual secrets.
has
Check if a specific secret exists. Returns exit code 0 if set, 1 if not.
# Check if a variable is sethush has DATABASE_URL
# Quiet mode (no output, just exit code)hush has API_KEY -q
# Use in scriptshush has DATABASE_URL -q && echo "DB configured"Options
| Option | Description |
|---|---|
-q, --quiet | Suppress output, only return exit code |
Example Output
DATABASE_URL is set (45 chars)Or if not set:
DATABASE_URL not foundpush
Push production secrets to Cloudflare (Workers and Pages).
# Push all configured targetshush push
# Push a specific targethush push -t apihush push -t app
# Preview without pushinghush push --dry-run
# Detailed preview showing each variablehush push --dry-run --verbosehush push -t app --dry-run --verboseOptions
| Option | Description |
|---|---|
-t, --target <name> | Push only the specified target |
--dry-run | Preview what would be pushed without making changes |
--verbose | Show detailed output (with --dry-run) |
Supported Destinations
| Target Type | Configuration | Wrangler Command |
|---|---|---|
| Cloudflare Workers | format: wrangler | wrangler secret put |
| Cloudflare Pages | push_to: { type: cloudflare-pages, project: ... } | wrangler pages secret bulk |
Configuration Examples
Cloudflare Workers (automatic with format: wrangler):
targets: - name: api path: ./api format: wranglerCloudflare Pages (requires push_to):
targets: - name: app path: ./app format: dotenv include: - NEXT_PUBLIC_* push_to: type: cloudflare-pages project: my-pages-project # Your Pages project nameHow It Works
- Decrypts production secrets from encrypted files
- Resolves subdirectory templates (if present)
- Filters variables per target using
include/excludepatterns - For Workers: Uploads each secret using
wrangler secret put - For Pages: Uploads all secrets at once using
wrangler pages secret bulk
status
Show configuration and file status. This is the first command to run when troubleshooting.
hush status
# Inspect the explicit global storehush status --globalExample Output
Hush Status
Store: project (/path/to/repo)
Config: hush.yaml Project: myorg/myrepo
Prerequisites: SOPS installed age key configured
Key Status: Local key: ~/.config/sops/age/keys/myorg-myrepo.txt 1Password backup: synced
Source Files: .hush.encrypted .hush.development.encrypted .hush.production.encrypted .hush.local (optional, not found)
Targets: root ./ dotenv -> .env.development app ./packages/app dotenv -> .env.development api ./packages/api wrangler -> .dev.varsTroubleshooting with Status
| You See | Meaning | Fix |
|---|---|---|
SOPS not installed | Missing prerequisite | brew install sops |
age not installed | Missing prerequisite | brew install age |
age key not found | No local key | npx hush keys setup |
age key configured but commands fail | direnv not loaded | direnv allow |
1Password backup: not found | Key not in 1Password | npx hush keys push |
check
Verify encrypted files are in sync with source files. Useful for pre-commit hooks.
# Basic checkhush check
# Warn but don't failhush check --warn
# JSON output for CIhush check --json
# Only check git-modified fileshush check --only-changedOptions
| Option | Description |
|---|---|
--warn | Warn on drift but exit 0 |
--json | Output machine-readable JSON |
--quiet | Suppress output |
--only-changed | Only check files modified in git |
--require-source | Fail if source file is missing |
Exit Codes
| Code | Meaning |
|---|---|
0 | All in sync |
1 | Drift detected (run hush encrypt) |
2 | Config error |
3 | Runtime error (sops missing, decrypt failed) |
Pre-commit Hook
npx hush check || exit 1Bypass with: HUSH_SKIP_CHECK=1 git commit -m "message"
resolve
Show what variables a specific target will receive, with filtering details. Essential for debugging why a variable is missing from a target.
# Check what variables api-workers receiveshush resolve api-workers
# Check with production environmenthush resolve api-workers -e productionOptions
| Option | Description |
|---|---|
-e, --env <env> | Environment: development or production |
Example Output
Target: api-workersPath: ./api/Format: wrangler (.dev.vars)Environment: development
✅ ROOT SECRETS (Matched Filters) (11): SUPABASE_URL (source: .env.development) STRIPE_SECRET_KEY (source: .env) R2_ACCESS_KEY_ID (source: .env) ...
🚫 EXCLUDED VARIABLES (8): EXPO_PUBLIC_API_URL (matches: EXPO_PUBLIC_*) FASTLANE_APPLE_ID (matches: FASTLANE_*) ASC_KEY_ID (matches: ASC_*) ...
📄 TEMPLATE EXPANSIONS (api/.hush): DATABASE_URL ← ${DATABASE_URL} PORT ← ${PORT:-8787}
📦 FINAL INJECTION (13 total): DATABASE_URL (template overrides root) PORT (template) SUPABASE_URL (root) ...trace
Trace a specific variable through all sources and targets. Use this to understand why a variable appears in some places but not others.
# Trace a variablehush trace DATABASE_URL
# Trace with production environmenthush trace STRIPE_SECRET_KEY -e productionOptions
| Option | Description |
|---|---|
-e, --env <env> | Environment: development or production |
Example Output
Tracing variable: SUPABASE_URL
Source Status: .env : ❌ Not found .env.development : ✅ Present .env.production : ✅ Present .env.local : (file not found)
Target Disposition (Environment: development): [root] : ✅ Included [app] : 🚫 Not included (not in include: EXPO_PUBLIC_*) [api] : ✅ Included [api-workers] : ✅ Included [landing] : 🚫 Not included (not in include: EXPO_PUBLIC_SUPABASE_*)skill
Install the Claude Code / OpenCode skill for AI-safe secrets management.
# Interactive: choose global or localhush skill
# Install globally (all projects)hush skill --global
# Install locally (this project only)hush skill --localOptions
| Option | Description |
|---|---|
--global | Install to ~/.claude/skills/ |
--local | Install to ./.claude/skills/ |
Global vs Local
- Global - Works across all your projects. Recommended for personal use.
- Local - Bundled with the project. Recommended for teams (commit
.claude/to git).
decrypt —force (Last Resort)
Write decrypted secrets to disk as plaintext files. Requires --force flag and interactive confirmation.
hush decrypt --forcehush decrypt --force -e productionWhy This Exists
Some edge cases genuinely require plaintext files on disk:
- Docker builds that can’t use
hush run - Legacy tooling that reads
.envfiles directly - CI systems without TTY support for
hush run
Safety Features
- Requires
--forceflag - Won’t run without explicit opt-in - Interactive confirmation - Must type “yes” to proceed
- Blocks non-TTY - Cannot be run by AI agents or in scripts
How It Works
- Decrypts encrypted source files
- Merges shared → environment → local overrides
- Interpolates variable references (
${VAR}) - Filters variables per target using
include/excludepatterns - Writes plaintext files to each target path
list (Caution)
List all variables with their actual values.
hush listhush list -e productionTroubleshooting
”no identity matched any of the recipients”
This error means SOPS cannot find your decryption key.
Most common cause: direnv not loaded.
Hush uses per-project keys at ~/.config/sops/age/keys/{project}.txt. SOPS needs the SOPS_AGE_KEY_FILE environment variable to locate them.
# 1. Verify direnv is installed and hookedbrew install direnvecho 'eval "$(direnv hook zsh)"' >> ~/.zshrc # or bashsource ~/.zshrc
# 2. Allow direnv in the projectcd /path/to/projectdirenv allow
# 3. Verify the env var is setecho $SOPS_AGE_KEY_FILE# Should output: /Users/you/.config/sops/age/keys/project-name.txt
# 4. Testhush statushush inspect”age key not found”
The key file doesn’t exist at the expected location.
# Check where hush expects the keyhush status # Look at "Local key:" line
# Option 1: Pull from 1Passwordhush keys setup
# Option 2: Get from team member and save manually# Save to the path shown in hush statusKey exists but wrong project
If you have a key but it’s for a different project:
# List all local keyshush keys list
# Pull the correct keyhush keys setup“SOPS is not installed”
brew install sops age # macOS# See Getting Started for Linux/Windowsdirenv not loading automatically
Make sure you’ve added the direnv hook to your shell:
# For zsh (~/.zshrc)eval "$(direnv hook zsh)"
# For bash (~/.bashrc)eval "$(direnv hook bash)"Then reload your shell or open a new terminal.