Project Navigation
hush is a single Go binary that keeps every API key, OAuth token, and
service credential encrypted on a single trusted host. Agents request
short-lived, scoped sessions over Tailscale; the request is approved on
your phone via Discord; approved secrets are delivered ECIES-encrypted
end-to-end and injected into the agent process's environment β never
written to disk on the agent machine.
If your dev workflow runs untrusted code (npm/pip packages, LLM-generated
scripts, AI-agent tools that execute shell commands) and your secrets
currently live in shell rc files or cloud-provider credential files, hush
is for you. Vault, 1Password CLI, and dotfile-based env vars all leave
files on disk that commodity malware grep for first. hush makes those
files not exist.
TAILSCALE MESH
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β ββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ β
β β AGENT MACHINE β β VAULT HOST β β
β β (untrusted, clean disk) β β (mlocked memory; offline) β β
β β β β β β
β β interactive client / β β vault server β β
β β supervisor β β β β
β β β β β β² β β
β β β ECDSA-signed βββββββΌβββββββΊβ verify signature β β
β β β claim β β β check Tailscale IP β β
β β β β β βΌ β β
β β β β β Discord DM ββββββΊ phone β β
β β β β β β [Approve] β β
β β β ES256K JWT βββββββΌββββββββ€ issue scoped JWT β β
β β β β β βΌ β β
β β β secret fetch βββββββΌβββββββΊβ ECIES-encrypt to β β
β β β (ECIES bytes) β β β ephemeral pubkey β β
β β βΌ β β β β β
β β decrypt β env var β β ββββββββββββββββββββββ β β
β β inject into child β β [no key files anywhere] β β
β ββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Installation
hush requires a supported release of Go
(Go 1.26+) and is built CGO_ENABLED=0 β a single static binary.
Status: this is still ALPHA and PR's are welcome to improve the project.
Prerequisites: a vault host and an agent host on the same Tailscale
tailnet, plus a Discord bot you control
(https://discord.com/developers/applications) for the approval channel.
Build from source
git clone https://github.com/mrz1836/hush.git && cd hush
magex build && sudo install -m 0755 cmd/hush/hush /usr/local/bin/hush
Upgrade
Once hush is on your PATH, the binary can upgrade itself in place
from the GitHub releases:
hush upgrade # download the latest release tarball, verify SHA-256, install in place
hush upgrade --check # report the latest available version without installing
hush upgrade --force # reinstall the latest release even when already current
Channel selection is controlled by the UPDATE_CHANNEL environment
variable (case-insensitive; default stable):
UPDATE_CHANNEL=stable hush upgrade # latest non-prerelease (default)
UPDATE_CHANNEL=beta hush upgrade # latest prerelease, falls back to stable when none
UPDATE_CHANNEL=edge hush upgrade # most recent release of any kind (excludes drafts)
# Or override the env per-invocation with --channel:
hush upgrade --channel beta
The install target is the resolved path of the running binary
(os.Executable() with symlinks evaluated β typically $(which hush)).
hush upgrade requires write access to that directory; when the
directory is not writable the command exits with a clear error naming
the directory rather than silently installing elsewhere. Re-run the
command under sudo (or copy the new binary into place manually) when
that happens.
After a successful upgrade hush upgrade prints a Restart any running 'hush serve' to pick up the new version reminder β the
upgrade does not touch any supervised process.
πΊοΈ Command palette
Every hush subcommand at a glance β every entry below is real today.
| Command |
Purpose |
hush smoke |
Guided end-to-end test with a fake secret β start here |
hush init server / hush init client |
Bootstrap a vault host / enroll an agent host |
hush serve |
Run the vault server (Tailscale-only) |
hush secret add / list / remove / rotate |
Manage vault entries (rotate re-encrypts and hot-reloads) |
hush vault rekey |
Change the vault passphrase β rotates the root of trust (TTY-only) |
hush request --exec β¦ |
One-shot interactive fetch + child exec |
hush supervise <config.toml> |
Long-running daemon with grace cache + validators |
hush health / server-url / version |
Daily-driver helpers |
hush revoke --jti β¦ |
Kill an active session token |
hush upgrade |
Self-upgrade from a GitHub release (stable / beta / edge) |
Global flags β --config <path>, --verbose, --quiet, --no-color β work on every command.
β‘ Quick Start
Four flows, in the order you'll use them.
Try it in 60 seconds
One command, fake secret, real Discord approval:
hush smoke --state-dir ~/.hush-smoke --reset
hush smoke walks the setup prompts, creates an isolated test vault, adds
HUSH_SMOKE_TEST=hello-from-hush, starts a temporary Tailscale-only server,
enrolls a client, asks you to approve in Discord, verifies the fake secret,
and shuts the temporary server down.
Smoke uses isolated macOS Keychain entries, hush-smoke-discord /
hush-smoke-server, under its smoke state. It does not read, overwrite, or
delete the production hush-discord / hush-server bot-token item. A transient
clock probe outage is downgraded to a warning in smoke; a measured clock drift
outside the configured limit still fails.
Clock checks use read-only SNTP probes against time.apple.com,
time.cloudflare.com, and pool.ntp.org, with a 2-second per-provider
timeout and first valid offset winning. Successful live probes write a 0600
clock-sync.json recent-good cache under the state dir; if every provider is
unavailable later, a cache entry younger than 1 hour can be used as fallback
and is audited as clock_sync_cache_fallback. hush init server and
hush serve still fail closed on provider outage or measured drift unless the
operator explicitly passes --allow-clock-skew, which emits
clock_skew_override.
To prove client authentication and approval routing against an already-running
server without creating smoke state or touching Keychain/vault state, use an
enrolled client key:
hush smoke --against-running --client-key-file ~/.hush-client.key
π§Ή Cleanup: hush smoke clean archives smoke artifacts by default.
Add --destroy --confirm 'destroy smoke' to permanently delete them.
Bootstrap the vault host
Three commands, run on the host you trust with your secrets.
hush init server # interactive preflight + prompts
hush secret add ANTHROPIC_API_KEY # then OPENAI_API_KEY, GEMINI_API_KEY, β¦
hush serve # binds Tailscale, brokers approvals
π°οΈ Listen on the vault host's Tailscale IPv4 β not your laptop IP.
When hush init server asks for a listen address, run
printf '%s:7743\n' "$(tailscale ip -4)" on the server host and paste
the result. Set discord_approval_channel_id to route approvals to a
channel; leave it empty to DM the owner directly.
π macOS Keychain locked? Choose the env-token fallback during init
and run hush serve with HUSH_DISCORD_BOT_TOKEN exported in that
terminal. Full recovery flow in docs/SECURITY.md Β§2.4.
Enroll the agent host
Enroll a per-machine client key, then fetch secrets straight into a child
process. Nothing lands on disk.
hush init client --machine-index 1
HUSH_SERVER="$(hush --config ~/.hush/config.toml server-url)"
hush request \
--server "$HUSH_SERVER" \
--machine-index 1 \
--scope ANTHROPIC_API_KEY --scope OPENAI_API_KEY --scope GEMINI_API_KEY \
--max-uses 3 --ttl 10m \
--reason "claude-code session" \
--exec zsh
Approve on Discord; the shell you launched inherits all three keys in its
environment β and only there. They are zeroed the moment the shell exits.
βοΈ --scope is repeatable and comma-separated. Either
--scope A --scope B or --scope A,B works. --max-uses must be β₯ the
number of scopes (one fetch per scope per session). --exec names a
program, not a shell string β pass child arguments after --.
Run a long-running daemon
For agents that run overnight, swap hush request for hush supervise.
One approval per refresh window keeps a 24/7 child alive across crashes;
the grace cache silently restarts a child that dies inside
cache_grace_ttl, so a 3am OOM doesn't page you.
Save the snippet below to ~/.hush/supervisors/hermes.toml β the full
schema lives in
deploy/examples/supervisors/example-daemon.toml:
name = "hermes"
reason = "Hermes AI gateway"
server_url = "http://100.64.0.1:7743/h/example"
client_machine_index = 1
session_type = "supervisor"
requested_ttl = "20h"
refresh_window = "09:00-10:00"
cache_secrets_for_restart = true
cache_grace_ttl = "60m"
status_socket = "/tmp/hush/supervise-hermes.sock"
pid_file = "/tmp/hush/supervise-hermes.pid"
scope = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"]
[child]
command = ["/usr/local/bin/hermes", "gateway", "start"]
[validators]
ANTHROPIC_API_KEY = "anthropic"
OPENAI_API_KEY = "openai"
GEMINI_API_KEY = "google-ai"
Then:
hush supervise ~/.hush/supervisors/hermes.toml --dry-run # validate the config
hush supervise ~/.hush/supervisors/hermes.toml # run for real
Built-in validators (anthropic, anthropic-oauth, openai, google-ai,
github) hit each provider before the child starts β stale credentials
fail loudly instead of breaking your daemon at 3am. Full guide in
docs/DAEMONS.md.
π More? docs/OPERATIONS.md covers Keychain
ACL recovery, clock-skew overrides, and --non-interactive mode for
Terraform/Ansible provisioning. docs/CONFIG-SCHEMA.md
has the full server + supervisor TOML schemas, and
deploy/examples/supervisors/ holds
production templates ready to copy.
At a glance
What hush does:
- Keeps every secret encrypted in a single AES-256-GCM + Argon2id (256MB) vault file on one trusted host.
- Requires phone approval (Discord DM with interactive buttons) for every fresh session.
- Delivers secrets ECIES-encrypted end-to-end into agent process memory only β no disk writes on the agent.
What hush explicitly does NOT do:
- No multi-owner approvals β a single configured operator approves every request (multi-owner quorum is future scope).
- No cloud KMS / SaaS dependency. The vault is self-hosted and offline-capable.
- No public network exposure. The vault server is bound to a Tailscale interface and refuses to start otherwise.
Daily-driver helpers:
hush health --server "$(hush server-url)" β one-shot reachability + clock-skew check.
hush secret list β enumerate vault entries (TTY: NAME β description; pipe-friendly).
hush secret rotate β re-encrypt the vault and hot-reload hush serve (SIGHUP, no downtime).
hush vault rekey β change the vault passphrase itself; snapshots the old vault and prints a restart-required reminder. See docs/VAULT-REKEY.md.
hush server-url β print the canonical server URL from your TOML config, perfect for $(β¦) substitution.
hush revoke --jti <uuid> β kill an active JWT before its TTL expires.
π Operating modes
Two ways to consume secrets; pick by how often you can answer your phone.
| Mode |
Best for |
Approval cadence |
hush request --exec |
Interactive shells, one-shot scripts, CI jobs |
Once per invocation |
hush supervise |
24/7 daemons, AI gateways, long-running agents |
Once per refresh window (e.g. 09:00β10:00 daily) |
Pick request for ad-hoc work; pick supervise when a phone buzz per
restart would page you at 3am.
Why hush exists
When untrusted code lands on a developer machine β via npm/pip
supply-chain attacks, LLM-generated scripts, or trojans masquerading as
tools β the very first thing it does is enumerate known credential
patterns in known files: shell rc files, cloud-provider credential
files, .env files, signing keys, and PEM files.
hush exists to make this enumeration return nothing. Secrets stay
encrypted on a single trusted host. Agents fetch them only after a human
approves the request from a phone. Approved secrets are delivered into
process memory and zeroed when the process exits. Nothing on disk.
For the full threat model, see docs/SECURITY.md.
Architecture summary
hush is a single Go binary playing three roles:
- Vault server β holds the encrypted vault file in mlocked memory;
issues ES256K-signed JWTs after Discord approval; ECIES-encrypts
secret responses to the client's per-session ephemeral public key;
exposes a tiny HTTP API over Tailscale only.
- Interactive client β ECDSA-signs a claim with a per-machine
BIP32-derived key; receives a JWT after approval; fetches and
decrypts secrets; injects them into a child process's environment.
- Supervisor β long-lived per-daemon process that holds the JWT and
ephemeral ECIES key in mlocked memory across child crashes/restarts
within the session TTL; runs validators before child start; exposes a
Unix status socket for agent-visible freshness queries.
Seven security layers stack independently β compromise of any single
layer does not enable secret extraction:
- BIP32 HD key hierarchy β all keys derived at runtime from a
single passphrase. Zero key files on disk.
- ES256K asymmetric JWT signing β only the server can sign;
leaking the public key does not enable forgery.
- ECIES end-to-end secret transport β secrets are encrypted to a
per-session ephemeral pubkey; captured HTTP traffic shows binary
blobs.
- ECDSA-signed client requests β every claim and revocation is
signed with a registered per-machine client key.
- mlocked secure memory β
SecureBytes containers; secrets never
stored as Go string; explicit zeroing on shutdown.
- Signed hash-chained audit log β every event ECDSA-signed; chain
breaks on modification.
- Obscurity β random API path prefix, custom vault file format,
non-obvious binary name. Additive only β never load-bearing.
The network perimeter is Tailscale-only β the vault server refuses to
bind on a non-Tailscale interface. Tailscale is the current mesh-VPN
choice, but the architecture does not depend on it specifically; the
requirement is "no public reachability," and Tailscale satisfies it
cleanly. The Approver interface is also pluggable: Discord is the
reference implementation and the only one that ships today, but future
Slack / Telegram / PagerDuty backends can be wired without changing the
rest of the system.
For the full architecture treatment, see docs/ARCHITECTURE.md.
Programmatic integration
AI agents that consume hush β Claude Code, Codex, custom Go daemons β
can integrate in-process via the
github.com/mrz1836/hush/pkg/client SDK instead of
exec'ing the CLI. The SDK gives agents typed access to two surfaces:
1. Supervisor freshness β SupervisorStatus (read what the local
supervisor knows; no Discord roundtrip):
import "github.com/mrz1836/hush/pkg/client"
sup := client.NewSupervisorStatus(os.Getenv("HUSH_STATUS_SOCKET"))
status, err := sup.Snapshot(ctx)
if len(status.ScopeStale) > 0 {
log.Fatal("refusing to run β stale scopes:", status.ScopeStale)
}
fmt.Println("session expires at:", status.SessionExpiresAt)
2. Capability discovery β Me() (ask the vault server what
scopes exist and what does my current session look like, signed
with the agent's enrolled client key, without burning a Discord
approval):
resp, err := client.Me(ctx, client.MeRequest{
ServerURL: "http://100.64.0.1:7743/h/abcd1234",
ClientKey: myEnrolledPrivKey, // *ecdsa.PrivateKey
MachineName: "claude-code-laptop",
BearerJWT: os.Getenv("HUSH_BEARER"), // optional
})
if err != nil {
log.Fatal(err)
}
fmt.Println("scopes available:", resp.ScopesAvailable)
if resp.CurrentSession != nil {
fmt.Println("current jti:", resp.CurrentSession.JTI,
"expires:", resp.CurrentSession.ExpiresAt)
}
Together these let an agent plan: "do I already hold a fresh session
for this scope? when does it expire? what could I request if I need
more?" β all without touching the operator's phone.
See pkg/client/README.md for the full v1
surface. The SDK ships typed errors (ErrSocketUnavailable,
ErrInvalidResponse, ErrRefreshDenied, ErrUnauthenticated) so
callers can switch on them with errors.Is.
3. Lifecycle events β Watch() (reactive notification so an
agent can wind down BEFORE its credentials expire, instead of being
killed mid-task):
events, _ := sup.Watch(ctx, client.WatchOptions{
PollInterval: 30 * time.Second,
ExpiryThresholds: []time.Duration{15 * time.Minute, 5 * time.Minute, 1 * time.Minute},
})
for ev := range events {
switch ev.Type {
case client.EventExpiresSoon:
if ev.Threshold <= time.Minute {
checkpoint(); shutdownCleanly()
}
case client.EventStateChange:
log.Info("supervisor state β", ev.Status.State)
case client.EventSessionRenewed:
log.Info("fresh JTI", ev.Status.SessionJTI)
}
}
Watch() emits EventInitial, EventStateChange,
EventScopeHealthChange, EventSessionRenewed, EventExpiresSoon
(once per configured threshold per session), and EventError for
transient poll failures. The channel closes on context cancel.
Worked example: a runnable program demonstrating all three SDK
calls (Snapshot, Me, Watch) plus the agent-context flags on /claim
lives at examples/agent/. See
docs/AGENT-INTEGRATION.md for the
complete agent integration guide.
Tech stack
- Go 1.26+ β single static binary, built
CGO_ENABLED=0 exclusively.
- decred/dcrd/dcrec/secp256k1/v4 β
secp256k1 primitives used for ECDSA signing, ES256K JWTs, and ECIES
envelope encryption.
- decred/dcrd/hdkeychain/v3 β BIP32
HD key derivation from the operator passphrase, paired with stdlib
golang.org/x/crypto/argon2 for the KDF.
- Tailscale β the only network reachable to
the vault server. WireGuard underneath; identity-based ACLs above.
- Discord + discordgo
β phone-based approval channel; the reference
Approver implementation.
- golang-jwt/jwt v5 β JWT framework;
hush registers a custom
ES256K signing method.
- go-toml v2 β strict TOML
parsing for server and supervisor configs.
- zalando/go-keyring β OS
keychain access with ACL support.
- cobra + pflag β
CLI subcommand routing and flag parsing.
The SecureBytes mlock pattern is custom-implemented in
internal/vault/securebytes/; the design is inspired by
sigil but takes no dependency on it.
π Documentation
View the comprehensive documentation for hush:
| Doc |
Purpose |
docs/OPERATIONS.md |
Setup, day-to-day modes, --non-interactive, Keychain recovery |
docs/VAULT-REKEY.md |
Vault passphrase rotation runbook: secret rotate vs vault rekey, Keychain follow-up, snapshot/rollback |
docs/ARCHITECTURE.md |
Component model, trust boundaries, supervisor lifecycle |
docs/SECURITY.md |
Threat model, 7 security layers, residual risks |
docs/AGENT-INTEGRATION.md |
SDK guide for AI agents: pkg/client Snapshot / Me / Watch + agent-context flags on /claim |
docs/CONFIG-SCHEMA.md |
Server + supervisor TOML schemas, defaults, validation |
docs/DAEMONS.md |
Supervisor pattern, refresh tuning, validator authoring |
docs/API.md |
HTTP endpoint reference |
docs/LIFECYCLE-SCENARIOS.md |
Supervisor lifecycle scenarios β behavioral reference |
docs/TAILSCALE-ACLS.md |
Recommended ACL pattern restricting the vault port |
docs/CLEAN-MACHINE.md |
Agent-machine cleanup checklist |
π Security
Important Disclaimer
β οΈ Experimental Software β Use at Your Own Risk
hush is experimental, open-source software provided "AS-IS" without
warranty. By running it, you acknowledge:
- Status: ALPHA quality; expect bugs, edge cases, and breaking changes.
- No formal audit: hush has not been professionally audited or
penetration-tested.
- You own the host: the trusted vault host, your Tailscale config, and
your Discord bot are yours to secure β hush only does its part.
- No liability: the authors accept no responsibility for compromised
secrets, downtime, or damages.
Don't trust hush with a secret you can't afford to rotate. If it breaks,
you get to keep both pieces.
For the full threat model and the 7 security layers, see
docs/SECURITY.md. For security issues, see our
Security Policy or contact: hush@mrz1818.com.
Additional Documentation & Repository Management
Development Setup (Getting Started)
Install MAGE-X build tool for development:
# Install MAGE-X for development and building
go install github.com/magefile/mage@latest
go install github.com/mrz1836/go-mage/magex@latest
magex update:install
Build Commands
View all build commands:
magex help
Common commands:
magex build β Build the binary
magex test β Run test suite
magex lint β Run all linters
magex deps:update β Update dependencies
Binary Deployment
This project uses goreleaser for
streamlined binary deployment to GitHub. To get started, install it via:
brew install goreleaser
The release process is defined in the .goreleaser.yml
configuration file. Then create and push a new Git tag using:
magex version:bump bump=patch push=true branch=master
This process ensures consistent, repeatable releases with properly versioned
artifacts.
GitHub Workflows
hush uses the Fortress workflow system for comprehensive CI/CD:
- fortress-test-suite.yml β Complete test suite across multiple Go versions
- fortress-code-quality.yml β Code quality checks (gofmt, golangci-lint, staticcheck)
- fortress-security-scans.yml β Security vulnerability scanning
- fortress-coverage.yml β Code coverage reporting to Codecov
- fortress-release.yml β Automated binary releases via GoReleaser
See all workflows in .github/workflows/.
Updating Dependencies
To update all dependencies (Go modules, linters, and related tools), run:
magex deps:update
This command ensures all dependencies are brought up to date in a single step,
including Go modules and any managed tools. It is the recommended way to keep
your development environment and CI in sync with the latest versions.
π§ͺ Examples & Tests
All unit tests run via GitHub Actions.
View the configuration file.
Run all tests (fast):
magex test
Run all tests with race detector (slower):
magex test:race
Test Coverage
View coverage report:
magex test:coverage
Coverage reports are automatically uploaded to Codecov
on every commit.
Benchmarks
Baseline performance numbers for the hot crypto paths. Re-measure after any
refactor that touches the request path, vault loader, or transport encryption β
regressions land here before they land in production.
Run the suite:
magex bench:default time=2s
Capture a baseline file for diffs across branches:
magex bench:save time=2s out=bench.txt
Compare two runs:
magex bench:compare old=before.txt new=after.txt
Latest baseline β Apple M1 Max Β· darwin/arm64 Β· Go 1.26 Β· benchtime=2s:
| Benchmark |
ns/op |
B/op |
allocs/op |
Path covered |
BenchmarkValidate (internal/token) |
192,871 |
5,739 |
105 |
JWT parse + ES256K verify + store lookup (supervisor) |
BenchmarkEncrypt (internal/transport/ecies) |
173,705 |
3,345 |
40 |
Ephemeral keygen + ECDH + AES-CBC + HMAC envelope |
BenchmarkDecrypt (internal/transport/ecies) |
151,457 |
2,528 |
29 |
Pubkey parse + ECDH + KDF + HMAC verify + AES-CBC |
BenchmarkLoad (internal/vault) |
51,813 |
16,528 |
113 |
Encrypted-vault read (16 secrets, ~64 B each) |
Numbers are local-machine baselines, not SLOs. Use them to spot
β₯10% regressions on the same hardware after a code change. The CI
machine numbers will differ; track relative deltas, not absolutes.
Last measured: 2026-05-24
π οΈ Code Standards
Read more about this Go project's code standards.
π€ AI Usage & Assistant Guidelines
Read the AI Usage & Assistant Guidelines for details on
how AI is used in this project and how to interact with AI assistants.
π₯ Maintainers
π€ Contributing
View the contributing guidelines and please follow
the code of conduct.
How can I help?
All kinds of contributions are welcome π!
The most basic way to show your support is to star π the project, or to
raise issues π¬.
You can also support this project by becoming a sponsor on GitHub π
or by making a bitcoin donation
to ensure this journey continues indefinitely! π

π License

This project is licensed under the terms of the LICENSE file at
the repo root.