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, materialize, set, copy-key, move-key, edit, inspect, has | Safe command surfaces that avoid printing secret values |
| Setup | bootstrap, config, init, migrate, encrypt, status, doctor, skill | Repository setup, diagnostics, and bounded migration, with encrypt kept only as a retired legacy bridge |
| Topology | file, bundle, target | Manage the files, bundles, and targets that define what secrets go where |
| Deployment | push, check | CI/CD and cloud deployment |
| Debugging | resolve, trace, verify-target, diff, export-example | Debug safe output, provenance, target completeness, and example generation |
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 |
Update check
Hush checks for new versions once per day via a standard HTTP request to the npm registry. No telemetry is sent. To disable, set HUSH_NO_UPDATE_CHECK=1 (also respects NO_UPDATE_NOTIFIER=1 and any CI environment variable).
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 a specific v3 targethush run -t api -- wrangler devhush run --target web -- npm start
# Run with global secrets onlyhush run --global -- npm startOptions
| Option | Description |
|---|---|
-t, --target <name> | Resolve a specific v3 target; otherwise Hush uses runtime or the only non-example target |
-- <command> | The command to run (everything after --) |
How It Works
- Loads the v3 repository from
.hush/ - Resolves the selected target through the shared resolver/materializer
- Decrypts to memory only and emits audit events
- Spawns the child process with the resolved environment
- Cleans up any staged runtime state automatically on failure or signals
When --global is used, Hush reads only from ~/.hush. It does not merge project secrets.
Examples
# Local developmenthush run -- npm run dev
# 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.jsmaterialize
Write a v3 target or bundle to explicit file paths for CI, native build tooling, or other file-based consumers.
This command uses the existing v3 resolver/materializer, but unlike hush run it persists the resulting plaintext files under a chosen output root so later steps can import them into keychains, provisioning-profile directories, or other tools.
# Materialize a target to an explicit output roothush materialize -t ios-signing --json --to /tmp/fitbot-signing
# Materialize for the lifetime of one command, then auto-cleanhush materialize -t ios-signing --to /tmp/fitbot-signing -- bash scripts/ci/install-ios-signing.sh /tmp/fitbot-signing
# Materialize a bundle directlyhush materialize --bundle fitbot-signing --to /tmp/fitbot-signing
# Clean up a previously materialized output roothush materialize --cleanup --to /tmp/fitbot-signingOptions
| Option | Description |
|---|---|
-t, --target <name> | Resolve a specific v3 target; otherwise Hush uses runtime or the only non-example target |
--bundle <name> | Materialize a bundle directly instead of selecting a target |
--json | Emit machine-readable JSON describing written paths and cleanup hints |
--format <dotenv> | Render env output in dotenv format (default) |
--output-root <dir> / --to <dir> | Output root for persisted plaintext artifacts (default: .hush-materialized/) |
--cleanup | Remove the materialized output root instead of writing files |
-- <command> | Run a child command with the materialized files available, then auto-clean the output root when it exits |
Artifact path metadata
Artifact entries can control where they land under the chosen output root:
filename— override the final filenamesubpath— override the output directory under the rootmaterializeAs— override the full relative output path
Targets can also declare the same metadata for the target artifact itself.
How It Works
- Loads the v3 repository and resolves the selected target or bundle
- Materializes file and binary artifacts using the shared runtime pipeline in persisted mode
- Writes files under the chosen output root with restrictive permissions
- If a child command is provided after
--, runs that command with the resolved env plusHUSH_MATERIALIZE_OUTPUT_ROOTavailable and auto-cleans on exit - Otherwise emits an audit event and returns only metadata/path information to stdout
- Leaves manual cleanup available via
hush materialize --cleanupwhen you intentionally need a longer-lived output root
JSON output shape
--json returns metadata only — never raw secret contents. It includes full metadata like repositoryRoot, files, logicalPaths, hashes, and path details. This makes it suitable for CI pipelines that need a stable handoff surface without logging private values.
Auto-cleaning command flow
When you append a command after --, Hush treats the materialized files as ephemeral job-scoped state:
hush materialize -t ios-signing --to /tmp/fitbot-signing -- bash scripts/ci/install-ios-signing.sh /tmp/fitbot-signingIn that mode Hush:
- writes the files under the chosen output root
- runs the child command
- deletes the materialized output root afterwards, even if the command fails
Use this mode for signing, certificates, provisioning profiles, or other native-tooling flows that only need the files for one step.
set
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 productionhush set ELEVENLABS_API_KEY "$ELEVENLABS_API_KEY" --globalhush set FEATURE_FLAG "enabled" --local
# Write to a specific file using --filehush set DATABASE_URL "postgres://user:pass@host/db" --file env/project/localhush set API_KEY "sk_test_xxx" --file local
# Write to repo-local file using --repo-local shorthandhush set DATABASE_URL "postgres://user:pass@host/db" --repo-local
# Interactive prompt (for users)hush set DATABASE_URLhush set API_KEY -e production
# Set in machine-local overrideshush 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> | Write to env/project/development or env/project/production instead of the shared v3 file |
--local | Write to the machine-local override document under ~/.hush/state/projects/<slug>/user/local-overrides.encrypted |
--file <path-or-alias> | Write to a specific file: full path (env/project/local) or alias (shared, development, production, local) |
--repo-local | Shorthand for --file env/project/local |
--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
Flags can appear before or after the optional inline value. These are equivalent:
hush set --global API_KEY "value"hush set API_KEY "value" --globalHow It Works
- Gets the value from inline arg, pipe, GUI, or prompt
- Loads the v3 repository and requires an active identity
- Shows planned destination before writing (e.g.,
will write DATABASE_URL -> env/project/local) - Updates the selected
.hush/files/**.encrypteddocument, or the machine-local override doc - Writes the YAML document back in place and emits a write audit event
- Confirms success with destination file and scope
For global mode, Hush binds global writes 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.
Conflict Detection
When writing to shared, Hush checks if the key already exists in local, development, or production. If a conflict is detected, it warns:
will write DATABASE_URL -> env/project/shared⚠ DATABASE_URL already exists in env/project/localExample Output
$ hush set DATABASE_URL "postgres://localhost/mydb"will write DATABASE_URL -> env/project/shared✓ DATABASE_URL set in env/project/shared (25 chars)
$ hush set API_KEYEnter value for API_KEY: sk_test_xxxwill write API_KEY -> env/project/shared✓ API_KEY set in env/project/shared (14 chars)
$ hush set DATABASE_URL "postgres://localhost/mydb" --repo-localwill write DATABASE_URL -> env/project/local✓ DATABASE_URL set in env/project/local (25 chars)TTY Input
When entering secrets interactively, trailing newlines (\r, \n) are automatically trimmed to prevent issues with trailing whitespace in secrets.
copy-key / move-key
Copy or move one key between encrypted v3 file documents without printing the decrypted value. Use this when a secret exists in Hush but belongs in a different service/environment file for target resolution.
# Copy a shared production key into the API production filehush copy-key RESEND_API_KEY --from env/project/production --to env/api/production
# Move a key when ownership should change to the destination service filehush move-key RESEND_API_KEY --from env/project/production --to env/api/production
# JSON metadata only; no valueshush copy-key RESEND_API_KEY --from env/project/production --to env/api/production --jsonOptions
| Option | Description |
|---|---|
--from <file-path> | Source encrypted v3 file path, such as env/project/production |
--to <file-path> | Destination encrypted v3 file path, such as env/api/production |
--json | Output action metadata without values |
How It Works
- Requires an owner active identity because the command mutates encrypted repository documents
- Finds exactly one matching leaf key in the source file
- Writes the same encrypted entry under the destination file path
- Deletes the source entry only for
move-key - Re-encrypts touched file documents and emits audit metadata
copy-key and move-key do not edit bundle imports. They are explicit ownership/location changes. If multiple services intentionally share one secret, keep it in a project-* file and add explicit bundle imports instead.
delete-key
Delete one key from an encrypted v3 file document. This is the safe way to remove accidental or unwanted secrets from Hush.
# Delete a key from shared secretshush delete-key OLD_API_KEY --from env/project/shared
# Delete a key with JSON outputhush delete-key OLD_API_KEY --from env/project/shared --json
# Delete without confirmation prompt (for automation)hush delete-key OLD_API_KEY --from env/project/shared --yesOptions
| Option | Description |
|---|---|
--from <file-path> | Source encrypted v3 file path, such as env/project/shared (required) |
--yes | Skip confirmation prompt |
--json | Output action metadata without values |
How It Works
- Requires an owner active identity because the command mutates encrypted repository documents
- Finds exactly one matching leaf key in the specified file
- Shows preview of what will be deleted
- Prompts for confirmation (unless
--yes) - Removes the key from the document
- Re-encrypts the file and emits audit metadata
Example Output
$ hush delete-key OLD_API_KEY --from env/project/sharedThis will delete OLD_API_KEY from env/project/sharedType "yes" to confirm: yesok: trueaction: deletekey: OLD_API_KEYfrom: env/project/sharedlogicalPath: env/project/shared/OLD_API_KEYedit
Edit a v3 document through a decrypted temporary YAML file in your $EDITOR.
# Edit shared secretshush edit
# Edit environment-specific secretshush edit developmenthush edit productionhush edit devhush edit prod
# Edit local overrideshush edit local
# Edit the explicit global storehush edit --global
# Use a specific editor for this sessionhush edit shared --editor vimhush edit shared --editor "code --wait"
# Use EDITOR environment variableEDITOR=nano hush edit sharedOptions
| Option | Description |
|---|---|
--editor <command> | Override editor for this session (e.g., vim, code --wait) |
--global | Edit secrets in ~/.hush instead of the current project store |
How It Works
- Resolves the target v3 document (
shared,development,production, or machine-locallocal) - Decrypts that document to a temporary YAML file
- Opens the temp file in
$EDITOR(or--editoroverride), validates it on close, then re-encrypts it back to the.encryptedpath - Logs the resolved editor command for debugging
- Emits a write audit event
When --global is used, the edit flow also binds to ~/.hush/.sops.yaml, even if the current working directory has its own .sops.yaml.
Editor Resolution
Hush resolves the editor in this order:
--editorflag (if provided)EDITORenvironment variablevi(default fallback)
bootstrap
Bootstrap a v3 repository rooted at .hush/.
hush bootstrap
# Bootstrap the explicit global storehush bootstrap --global
# Force a child-local repo even when a parent .hush/ existshush bootstrap --new-repo
# Skip interactive confirmation (CI / non-interactive)hush bootstrap --yeshush bootstrap is the canonical setup flow for Task 6. It creates the initial v3 repository layout, .sops.yaml, default identities, bundle and target shells, and the machine-local active identity pointer. A fresh bootstrap should leave both hush status and hush inspect ready to run without extra repair steps.
When run inside a nested git repository, bootstrap walks upward to find an existing parent .hush/ repository and joins it. Use --new-repo to force a child-local repository instead. Use --yes (or -y) to skip the interactive confirmation prompt in non-interactive mode.
When package metadata does not declare a project identifier, bootstrap falls back to the repo basename (for example bottown) instead of inventing a nested local/<repo> identity.
Created layout
.hush/ manifest.encrypted files/ env/project/shared.encrypted.sops.yaml~/.hush/state/projects/<project-slug>/active-identity.jsonDefault identities and shells
- Identities:
owner-local,member-local,ci - Bundle shell:
project - Target shells:
runtime,example - Initial active identity:
owner-local
config
Inspect or update v3 structural config without treating plaintext YAML as the source of truth.
hush config showhush config show --jsonhush config show identitieshush config active-identityhush config active-identity member-localhush config readers env/project/shared --roles owner,member,cihush config readers env/project/shared --identities owner-local,ciSubcommands
| Subcommand | Description |
|---|---|
show [section] | Show v3 structure. Sections: manifest, identities, bundles, targets, imports, files, state |
active-identity [name] | Show the current active identity, or set it when a name is provided |
readers <file-path> --roles <csv> --identities <csv> | Update the file-scoped readers on one encrypted file |
Notes
showreturns structural data only. It does not materialize secret values.- Add
--jsontoshowfor machine-readable structural output. JSON output includes paths, readers, bundles, targets, and state metadata only — never decrypted values. active-identitywrites machine-local state under~/.hush/state/projects/<project-slug>/active-identity.json.readersupdates one ACL unit at a time, because files are the only ACL boundary in v3.
Topology Management
Files, bundles, and targets form a three-layer hierarchy. Build from the bottom up and tear down from the top.
Files → Bundles → Targets- Files are the encrypted documents that hold secret entries. Each file has its own ACL (readers).
- Bundles package one or more file references together. A bundle is the unit of access for a target.
- Targets consume a bundle and declare how secrets are materialized (format, mode, filename).
All topology mutations require the owner role. Reference validation runs before any disk write, so dangling refs are rejected before encryption.
Lifecycle example
# 1. Create an encrypted filehush file add env/api/production --roles owner,ci
# 2. Create a bundle that references ithush bundle add api-production --files env/api/production
# 3. Create a target that consumes the bundlehush target add api-production --bundle api-production --format dotenv
# 4. Verify the target resolveshush verify-target api-production --require DATABASE_URL
# 5. Teardown in reverse orderhush target remove api-productionhush bundle remove api-productionhush file remove env/api/productionfile
Manage encrypted file documents in the v3 repository.
# Add a new encrypted filehush file add env/api/production --roles owner,cihush file add env/api/staging --roles owner,member,cihush file add env/project/shared --identities owner-local,ci
# Update readers on an existing filehush file readers env/api/production --roles owner,cihush file readers env/api/production --identities owner-local,ci
# List all fileshush file listhush file list --json
# Remove a filehush file remove env/api/staginghush file remove env/api/production --keep-fileSubcommands
| Subcommand | Description |
|---|---|
add <namespaced-path> | Create a new encrypted file document with the given readers |
remove <namespaced-path> | Remove a file from the manifest and delete the encrypted disk file |
list | List all encrypted files with their readers |
readers <namespaced-path> | Update the ACL readers on an existing file |
Options for file add and file readers
| Option | Description |
|---|---|
--roles <csv> | Comma-separated roles: owner, member, ci |
--identities <csv> | Comma-separated identity names |
Options for file remove
| Option | Description |
|---|---|
--keep-file | Remove only the manifest entry; leave the encrypted disk file in place |
Safety semantics
- Requires the owner role. Non-owner identities cannot add, remove, or update files.
file removefails if the file is still referenced by any bundle. Remove the bundle first.- All mutations emit
metadata_changeaudit events.
bundle
Manage bundles of encrypted file references.
# Create a bundle from explicit file refshush bundle add api-production --files env/api/productionhush bundle add api-production --files env/api/production,env/project/shared
# Add or remove a file from an existing bundlehush bundle add-file api-production env/project/sharedhush bundle remove-file api-production env/project/shared
# List all bundleshush bundle listhush bundle list --json
# Remove a bundlehush bundle remove api-productionSubcommands
| Subcommand | Description |
|---|---|
add <name> | Create a new bundle, optionally with an initial file list |
add-file <bundle> <file> | Add a file reference to an existing bundle |
remove-file <bundle> <file> | Remove a file reference from a bundle |
remove <name> | Remove a bundle from the manifest |
list | List all bundles with their file references |
Options for bundle add
| Option | Description |
|---|---|
--files <csv> | Comma-separated namespaced file paths to include in the bundle |
Safety semantics
- Requires the owner role. All bundle mutations emit
metadata_changeaudit events. bundle addvalidates that every file path exists in the file index before writing.- Duplicate file refs in
--filesare rejected. bundle removefails if the bundle is still referenced by any target. Remove the target first.
target
Manage targets in the v3 repository.
# Add a targethush target add api-production --bundle api-production --format dotenvhush target add ios-signing --bundle ios-signing --format json --mode filehush target add web-example --bundle web --format dotenv --mode example
# List all targetshush target listhush target list --json
# Remove a targethush target remove api-productionSubcommands
| Subcommand | Description |
|---|---|
add <name> | Create a new target |
remove <name> | Remove a target from the manifest |
list | List all targets with their bundle and format |
Options for target add
| Option | Description |
|---|---|
--bundle <name> | Required. The bundle this target consumes |
--format <format> | Required. Output format: dotenv, json, wrangler |
--mode <mode> | Materialization mode: process (default), file, example |
--filename <name> | Override the output filename for file-mode targets |
--subpath <path> | Override the output subdirectory under the materialization root |
--materialize-as <name> | Override the full relative output path |
Safety semantics
- Requires the owner role.
target addvalidates that the referenced bundle exists before writing.target removefails if the target name is not found.- All mutations emit
metadata_changeaudit events.
init
Deprecated alias for hush bootstrap.
hush initThis command still works for one transition cycle, but it only prints a deprecation warning and delegates to hush bootstrap. New setup flows should use hush bootstrap directly.
encrypt
Retired legacy bridge. This command no longer performs general encryption work outside the migration path.
hush encryptCurrent behavior
hush encrypt now fails fast and points you to hush migrate --from v2.
The v3 CLI no longer preserves a standalone plaintext-to-encrypted runtime bridge outside migration.
inspect
Inspect readable v3 logical paths with redaction applied. Safe for AI agents.
hush inspecthush inspect -e productionhush inspect --jsonOptions
| Option | Description |
|---|---|
-e, --env <env> | Environment: development or production |
--json | Output machine-readable JSON (never includes secret values) |
Example Output
Hush inspect
Active identity: owner-localReadable files: 2Unreadable files: 1
Readable entries: env/app/shared (roles=owner,member identities=owner-local,member-local) env/apps/web/env/NEXT_PUBLIC_API_URL kind=value exposure=visible https://api.example.com env/app/secrets (roles=owner identities=owner-local) env/apps/api/env/STRIPE_SECRET_KEY kind=value exposure=sensitive [redacted]
Unreadable files: env/app/ci-only (roles=ci identities=ci)JSON output shape
--json returns { target, entries: [{ key, file, sensitive, set, value? }] }. The value field is included only for entries where sensitive: false. Sensitive entries never have a value field.
inspect reports the v3 file boundary directly. It does not summarize legacy source files or target filters.
has
Check if a specific secret resolves to a non-empty value. Returns exit code 0 if set, 1 if missing or empty.
# 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"
# Machine-readable output for agents and scriptshush has DATABASE_URL --jsonOptions
| Option | Description |
|---|---|
-q, --quiet | Suppress output, only return exit code |
--json | Emit { key, target, exists, declared } (never values) |
Example Output
DATABASE_URL is set (45 chars)If the key exists but resolves to an empty value:
DATABASE_URL exists but is emptyOr if not set:
DATABASE_URL not found in target runtimehas is a target-runtime presence check. It does not validate whether a value is a real credential versus a placeholder, example, or template string.
push
Push resolved v3 target output to Cloudflare Workers.
# 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 |
Configuration Examples
Cloudflare Workers (automatic with format: wrangler):
targets: api: bundle: api-runtime format: wranglerLegacy push_to: cloudflare-pages mappings are not part of the v3 runtime target model. Migrate those repos first, then rewrite the deployment flow explicitly.
How It Works
- Loads the v3 repository and resolves a wrangler-formatted target through the shared materializer
- Emits the same audit-safe materialization flow used by
hush run - Pushes each resolved environment variable with
wrangler secret put
status
Show repository and machine-local v3 state. This is the first command to run when troubleshooting.
hush status
# Inspect the explicit global storehush status --global
# Machine-readable outputhush status --jsonOptions
| Option | Description |
|---|---|
--json | Output structured JSON with repository readiness, paths, counts, and machine-local state (never includes secret values) |
Example Output
Hush status
Repository: readyRoot: /path/to/repoStore: project (/path/to/repo)Manifest: /path/to/repo/.hush/manifest.encryptedFiles root: /path/to/repo/.hush/filesActive identity: owner-local
Repository state: kind: v3 manifest files: 1 encrypted files: 3 identities: 3 bundles: 2 targets: 2 imports: 0
Machine-local state: project slug: myorg-myrepo-1234abcd state root: ~/.hush/state/projects/myorg-myrepo-1234abcd active identity path: ~/.hush/state/projects/.../active-identity.json (present) audit log path: ~/.hush/state/projects/.../audit.jsonl (present)If the repo still uses hush.yaml, status reports that migration is required before relying on normal v3 command flows.
Troubleshooting with status
| You See | Meaning | Fix |
|---|---|---|
Repository: missing | No v3 repo at this root | hush bootstrap |
Repository: legacy-v2 | Repo still uses hush.yaml authority | migrate or bootstrap into v3 |
Active identity: (not set) | Machine-local identity pointer is missing | hush config active-identity <name> |
active identity path ... (missing) | State file has not been created yet | set the active identity |
audit log path ... (missing) | No diagnostic command has written audit yet | run a read/write command or check state permissions |
doctor
Diagnose root discovery, key resolution, and store configuration for the current directory.
hush doctor
# Diagnose as if bootstrapping a child-local repohush doctor --new-repo
# Machine-readable outputhush doctor --jsonOptions
| Option | Description |
|---|---|
--new-repo | Diagnose as if bootstrapping a child-local repository |
--json | Output { checks: [{ name, ok, detail }] } mirroring the diagnostic sections |
hush doctor prints a structured diagnostic report covering:
- Directory Context — current directory, git root
- Repository Root Discovery — which
.hush/was found (or not), whether a parent repo exists, and the resolved root - Key Resolution — selected key source, key identity, all attempted paths with existence markers
- SOPS Key Match — whether the selected private key’s public half matches the
.sops.yamlcreation rule - Decryption Check — whether the repository loads and how many files it contains
- Recommendations — actionable fixes for any detected issues
Use this command when hush bootstrap fails, when hush inspect reports “no identity matched”, or when you need to understand why Hush picks a particular repository root in a nested-git-repo layout.
check
Validate the v3 repository and flag leftover plaintext or legacy artifacts. Useful for pre-commit hooks.
# Basic checkhush check
# Warn but don't failhush check --warn
# JSON output for CIhush check --jsonOptions
| Option | Description |
|---|---|
--warn | Warn on drift but exit 0 |
--json | Output machine-readable JSON |
--quiet | Suppress output |
--allow-plaintext | Ignore leftover plaintext or legacy artifacts (not recommended) |
Exit Codes
| Code | Meaning |
|---|---|
0 | Repository is valid |
3 | Repository validation failed |
4 | Leftover plaintext or legacy artifacts were found |
Pre-commit Hook
npx @chriscode/hush check || exit 1check validates .hush/manifest.encrypted, every .hush/files/**.encrypted document, and scans for leftovers such as hush.yaml, .env*, legacy .hush*.encrypted, or .hush-materialized/.
resolve
Resolve a v3 target and show file provenance. Use this to see what a target actually depends on.
# Resolve a targethush resolve runtime
# The env flag is accepted for CLI compatibility, but v3 resolution is driven by bundles and fileshush resolve runtime -e production
# Machine-readable provenance without secret valueshush resolve runtime --jsonOptions
| Option | Description |
|---|---|
-e, --env <env> | Environment: development or production |
--json | Output target, bundle, file, logical path, and provenance metadata without values |
Example Output
Hush resolve
Target: app-devBundle: appFormat: dotenvActive identity: developer-localResolved files: 2Resolved logical paths: 2
Files: env/app/shared env/app/secrets
Values: env/apps/api/env/STRIPE_SECRET_KEY file=env/app/secrets namespace=env env/apps/web/env/NEXT_PUBLIC_API_URL file=env/app/shared namespace=env
Artifacts: (none)If resolution fails because the active identity cannot read a required file, resolve prints the denied file paths and their readers.
--json is safe for automation: it includes logical paths and provenance, but does not include decrypted scalar values.
trace
Trace a logical path or leaf key through repository files and targets. Use this to understand where a value lives and why a target can or cannot see it.
# Trace a leaf keyhush trace DATABASE_URL
# Trace a full logical pathhush trace env/apps/api/env/DATABASE_URL
# Machine-readable target reachability diagnosticshush trace RESEND_API_KEY --jsonOptions
| Option | Description |
|---|---|
-e, --env <env> | Environment: development or production |
--json | Output repository matches and per-target reachability diagnostics without values |
Example Output
Hush trace
Selector: STRIPE_SECRET_KEYActive identity: teammate-localMatched logical paths: 1
Repository files: env/app/secrets (unreadable; roles=owner identities=developer-local) env/apps/api/env/STRIPE_SECRET_KEY
Targets: app-dev (acl denied) env/app/secrets (roles=owner identities=developer-local)trace uses the same v3 resolver as runtime commands, so ACL failures and interpolation provenance match real resolution behavior.
When a key exists in Hush but is not selected by a target bundle, trace now explains that explicitly and lists candidate source bundles. The fix is still explicit: add a bundle import, or copy/move the key into the file consumed by that target bundle. Hush does not perform ambient inheritance.
verify-target
Verify that a v3 target resolves and contains required keys. Use this in release automation before syncing remote runtime secrets or flipping traffic.
# Verify the target resolves at allhush verify-target api-production
# Require specific runtime keyshush verify-target api-production --require JWT_SECRET --require RESEND_API_KEY
# JSON output for CI or agentshush verify-target api-production --require RESEND_API_KEY --jsonOptions
| Option | Description |
|---|---|
--require <key> | Require a leaf key to resolve from the selected target bundle. Repeatable. |
--json | Output machine-readable verification results without values |
How It Works
- Loads the v3 repository and active identity
- Resolves the named target with the same resolver used by
run,push, andmaterialize - Checks file ACLs, conflicts, and required leaf keys
- Prints only paths, key names, files, bundles, and errors — never decrypted values
If a required key is missing, run hush trace <KEY> to see whether it exists in another file or bundle that the target does not import.
Service × environment topology guidance
For monorepos, model concrete consumer surfaces as service-environment bundles/targets:
api-developmentapi-stagingapi-productionroot-developmentroot-stagingroot-productionUse project-* only for intentionally shared material:
project-sharedproject-developmentproject-stagingproject-productionFiles remain the security boundary, bundles package files, and targets consume bundles. A target such as api-production should not see project-production secrets unless its bundle explicitly imports that shared bundle/file or the key is copied/moved into an API-owned file. This keeps production secret flow visible and agent-safe.
diff
Compare the current v3 target or bundle against HEAD by default, or another git ref with --ref. Use this when you need a safe review surface with provenance and redaction preserved.
# Compare the default runtime target against HEADhush diff
# Compare against another git refhush diff --ref HEAD~1
# Compare a specific target or bundlehush diff --target runtimehush diff --bundle projectOptions
| Option | Description |
|---|---|
-t, --target <name> | Compare a specific target; otherwise Hush uses runtime or the only non-example target |
--bundle <name> | Compare a bundle instead of a target |
--ref <git-ref> | Git ref to compare against (default: HEAD) |
Example Output
Hush diff
Reference: HEADSelection: target runtimeActive identity: owner-local
File changes: ~ env/project/shared ref roles=owner,member identities=owner-local,member-local current roles=owner identities=owner-local
Resolved changes: ~ env/project/shared/API_TOKEN (value) ref [redacted] file=env/project/shared namespace=env current [redacted] file=env/project/shared namespace=envdiff compares both sides through the shared v3 resolver, so file ACL failures, interpolation provenance, and imported provenance are judged the same way real command resolution works.
export-example
Emit a deterministic, redacted example from a target or bundle. Use this for docs, onboarding, and scaffolding without materializing protected values.
# Safe example for the default targethush export-example
# Choose a specific target or bundlehush export-example --target runtimehush export-example --bundle project
# Write output to .env.example in the repo roothush export-example --write
# Write to a custom pathhush export-example --write --to /path/to/.env.example
# Overwrite an existing file with different contenthush export-example --write --forceOptions
| Option | Description |
|---|---|
-t, --target <name> | Export a specific target; otherwise Hush uses runtime or the only non-example target |
--bundle <name> | Export a bundle summary instead of a target |
--write | Write the redacted example to a file instead of only printing to stdout |
--to <path> | Destination file path for --write (default: .env.example in the repo root) |
--force | Overwrite the destination file even if its content differs from the generated output |
Writing .env.example for fresh-clone discovery
Commit a generated .env.example so new contributors can see the required environment variables without decrypting secrets:
hush export-example --writegit add .env.examplegit commit -m "chore: update .env.example"Re-run after changing the repository topology so the example stays in sync with the actual secrets surface. The --write flag refuses to overwrite a file whose content diverges without --force, protecting against accidental overwrites.
Example Output
Hush export-example
Selection: target runtimeFormat: dotenvActive identity: owner-localProtected values omitted: 1
Target example: PUBLIC_URL=https://example.com
Artifacts: artifacts/project/runtime/env-file (file:dotenv) # [redacted sensitive artifact]export-example never emits raw sensitive: true values. Protected scalars are omitted from the example surface, and protected file or binary artifacts become deterministic placeholder content.
migrate
Convert a legacy hush.yaml repository to the v3 .hush/ layout with a single bounded migration flow.
# Inventory onlyhush migrate --from v2 --dry-run
# Convert and validatehush migrate --from v2
# Remove validated leftovershush migrate --from v2 --cleanupOptions
| Option | Description |
|---|---|
--from v2 | Required legacy source version selector for the big-bang migration flow |
--dry-run | Inventory the legacy repo without mutating it |
--cleanup | Remove validated legacy leftovers after a successful migration |
How It Works
- Inventories the legacy
hush.yamlrepo, encrypted source files, targets, and repo references - Creates
.hush/manifest.encryptedand v3 file documents - Migrates local overrides into machine-local state
- Validates the converted repo through the shared v3 materialization pipeline
- Leaves legacy files in place until
--cleanupremoves them
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).
completion
Generate a shell completion script for bash, zsh, or fish.
# Generate and install for your shellhush completion bash >> ~/.bashrchush completion zsh > ~/.zsh/completions/_hushhush completion fish > ~/.config/fish/completions/hush.fishOptions
| Shell | Install command |
|---|---|
bash | hush completion bash >> ~/.bashrc (or /etc/bash_completion.d/hush) |
zsh | hush completion zsh > ~/.zsh/completions/_hush (make sure fpath includes the directory, then autoload -Uz compinit && compinit) |
fish | hush completion fish > ~/.config/fish/completions/hush.fish |
Installation examples
hush completion bash >> ~/.bashrcsource ~/.bashrcmkdir -p ~/.zsh/completionshush completion zsh > ~/.zsh/completions/_hush# Add to ~/.zshrc if not already present:# fpath=(~/.zsh/completions $fpath)# autoload -Uz compinit && compinitsource ~/.zshrchush completion fish > ~/.config/fish/completions/hush.fishUnknown or missing shell argument exits with an error listing supported shells.
decrypt —force (Last Resort)
Write decrypted targets to .hush-materialized/ as persisted plaintext artifacts. Requires --force flag and interactive confirmation.
hush decrypt --forcehush decrypt --force -e productionWhy This Exists
Some edge cases genuinely require persisted plaintext artifacts:
- Docker or packaging workflows that cannot inherit process env directly
- Legacy tooling that can only read files on disk
- Temporary break-glass debugging of target output
Safety Features
- Requires
--forceflag - Won’t run without explicit opt-in - Interactive confirmation - Must type
yesto proceed - Writes under
.hush-materialized/- Plaintext is isolated to one explicit directory - Shared materialization audit - Uses the same audited runtime flow as
hush run
How It Works
- Loads the v3 repository and selects all non-example targets
- Materializes each target through the shared runtime pipeline in persisted mode
- Writes target artifacts under
.hush-materialized/targets/and related artifact paths - Leaves the files on disk until you delete them explicitly
list
List variable names. Values are masked by default. Use --reveal to print plaintext values.
hush listhush list --revealhush list -e productionOptions
| Option | Description |
|---|---|
--reveal | Print plaintext values instead of masked output |
Troubleshooting
”no identity matched any of the recipients”
This error means SOPS cannot find your decryption key.
Most common cause: direnv not loaded.
Hush stores per-project keys at ~/.config/sops/age/keys/{project}.txt. Hush now auto-matches those files against the .sops.yaml recipient for normal CLI commands, so a fresh hush bootstrap works even if package.json.repository is missing. SOPS_AGE_KEY_FILE is still the best explicit override when you need to force a specific key file.
# 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 when you want an explicit overrideecho $SOPS_AGE_KEY_FILE# Optional, but if set it 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
# Get the key from a team member and save it manually# Save to the path shown in hush status, then run:hush keys setupKey exists but wrong project
If you have a key but it’s for a different project:
# List all local keyshush keys list
# Copy the correct key into the expected local path, then verify ithush 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.