██╗ ██╗██╗ ██╗███████╗██╗ ██╗
██║ ██║██║ ██║██╔════╝██║ ██║
███████║██║ ██║███████╗███████║
██╔══██║██║ ██║╚════██║██╔══██║
██║ ██║╚██████╔╝███████║██║ ██║
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝
Agent-blind secrets for humans and coding agents. The value is shared over Bitwarden (Send link or vault item), is stored encrypted with age, and is used by name. The CLI never prints plaintext.
MIT. Open source at github.com/turinglabsorg/hush.
┌────────┐ Send URL + name ┌────────────┐piped, never printed ┌────────────┐
│ Human │ ─────────────────▶ │ hush pull │ ──────────────────▶ │ age vault │
│ Agent │ (chat-safe) │ metadata │ │ ~/.hush/ │
└────────┘ └────────────┘ └─────┬──────┘
│ decrypt in memory
▼
┌────────────┐ ┌────────────────────┐
│ hush run │ ──▶│ transcript (blind) │
│ --redact │ │ [redacted by hush] │
└────────────┘ └────────────────────┘
bitwarden Send link → hush pull --name stripe-prod --send <url> → ~/.hush/vault/stripe-prod.age
agent chat: "use hush:stripe-prod"
hush run --name stripe-prod --env STRIPE_API_KEY --redact -- curl ...
The agent gets its own Bitwarden account (its own vault, optionally via BITWARDENCLI_APPDATA_DIR). Humans share secrets with it through Bitwarden Send, or through vault items (shared org collection) named hush put NAME.
Install the latest GitHub Release (SHA-256 verified; cosign if present):
curl -fsSL https://raw.githubusercontent.com/turinglabsorg/hush/main/install.sh | shWith the agent skill and a PATH symlink:
curl -fsSL https://raw.githubusercontent.com/turinglabsorg/hush/main/install.sh | sh -s -- --agent-skill --path-linkPin a version with --version v0.6.0. That release includes hush box and hush send. The installer picks the binary for the machine, so run it on the Mac Pro and on the MacBook separately. Do not copy the binary from one to the other.
curl -fsSL https://raw.githubusercontent.com/turinglabsorg/hush/main/install.sh | sh -s -- --agent-skillThat puts hush in ~/.local/bin and the skills in ~/.agents/skills/hush and ~/.agents/skills/hush-ingest (Send ingestion on the agent's smallest model), linked from ~/.claude/skills for Claude Code. Build from a checkout with --from-source only if you are not using the release.
Needs the Bitwarden CLI (bw) on PATH (or HUSH_BW_BIN) for ingest. hush box does not need it.
Then:
bw login --apikey # or `bw login` interactively; `bw config server <url>` first if self-hosted
bw unlock # export the printed BW_SESSION
export BW_SESSION="..."
hush init
hush doctor --json # must report okIf the agent needs its own inbox — e.g. to receive the verification mail
for its Bitwarden account — use ambox.dev:
agent-first, end-to-end encrypted email (your-agent@ambox.dev), open
source. That is how the live end-to-end check above registered its test
account.
When a new credential must originate on the agent machine, generate and encrypt it in one step. The plaintext is never written to stdout:
hush generate bitwarden-master-password --jsonThe default is 32 random bytes encoded as 64 hexadecimal characters. Use
--bytes to choose 16–128 random bytes. Existing names are protected unless
--force is explicitly supplied. Consume the stored value only through
hush run --redact.
For an agent-owned Bitwarden account, keep its master password under
BITWARDEN_MASTER_PASSWORD and let Hush perform login or unlock without
exposing either the password or the resulting session:
hush bitwarden unlock --email agent@example.com --json
hush run --name BITWARDEN_SESSION --env BW_SESSION --redact -- \
hush pull --name stripe-prod --jsonIf Bitwarden locks again, rerun hush bitwarden unlock. Hush reads the stored
master password, obtains a fresh session, and replaces only the encrypted
BITWARDEN_SESSION entry. Custom secret names are available through
--master-secret and --session-secret.
For account registration requiring upper/lowercase letters, digits and a
special character, use
hush generate SERVICE_PASSWORD --password --json. The encrypted value contains
32 random characters by default and always includes all four classes.
--bytes 16..128 selects the character count in this mode. Without
--password, generation retains its hexadecimal encoding and byte-count
behavior. Generation never prints the password and refuses replacement without
--force. Consume the exact stored value through hush run --redact; do not
append characters outside the vault or reuse another service's password.
This requires a build containing the TOTP commands (0.7.0-dev or later).
The v0.6.0 release does not include them; build this checkout with
./install.sh --from-source --agent-skill until a release is available.
Hush can keep a TOTP enrollment encrypted and generate its current numeric
code for an authorized login. It accepts a base32 seed or an
otpauth://totp URI, with SHA-1, SHA-256 or SHA-512, 6 or 8 digits, and a
15–120 second period. A plain seed uses the Google Authenticator defaults:
SHA-1, 6 digits, 30 seconds.
Capture the enrollment directly from the provider into a pipe, without
printing it, saving a QR screenshot or writing a plaintext file. The
receiving command is hush totp store --name SERVICE_TOTP --json; it reads
stdin, encrypts the enrollment and prints metadata only. An existing name
requires explicit --force to replace it. Human-supplied enrollments may
also be received through the usual Bitwarden Send workflow.
Inject the code directly into the login process:
hush run --name SERVICE_TOTP --env LOGIN_OTP --totp --redact -- login-commandThe child receives only the numeric code, never the seed. --redact is
required and masks both the stored enrollment and generated code on stdout
and stderr. Codes with less than ten seconds remaining are renewed before
starting the command. Invoke this at the actual verification step; a
long-running child does not receive refreshed codes. Hush does not handle
SMS, push approvals or passkeys through TOTP.
A live Send check with Bitwarden CLI 2026.6.0 failed before creation with an
uninitialized userClient error. The same operation succeeded with the official
2026.9.1 binary. Select a verified alternate CLI through HUSH_BW_BIN; keep the
normal hush pipeline and inspect existing Sends before retrying an uncertain
creation. Never print raw Bitwarden failure payloads or bypass hush.
Option A — Bitwarden Send (recommended). Create a text Send with the secret, then in the agent chat paste only the Send URL (URLs are not secret) plus the name:
Store this as
stripe-prod: https://vault.bitwarden.com/#/send/...
The agent runs:
hush pull --name stripe-prod --send <url> --jsonFor a password-protected Send, hand the password over out of band and use --passwordenv VAR or --passwordfile PATH (never a literal --password on the command line). hush pull receives the Send, encrypts, writes the vault, and prints metadata only:
{"event":"stored","name":"stripe-prod","sender":"self","replaced":false}Email-gated Sends. If the Send is restricted to an email address, pass --email plus one code source. hush drives the whole verification (submit address → wait for the code mail → submit newest code) in a single call; codes are single-use and minted per attempt, so always submit the newest one:
hush pull --name stripe-prod --send <url> --email agent@example.com --code-cmd '<hook prints ONLY the code>' --json
# or: --codeenv VAR / --codefile PATH (fresh code already in hand)
# tune: --code-timeout 300 --code-poll 10The hook is polled until it prints a code; hush ignores anything the hook returned before minting, so a slow mailbox can never feed a stale code. Rejected code → retry hush pull for a fresh one.
Option B — vault item. Add an item to the agent's vault (or a shared org collection) named exactly stripe-prod (secret in the password or notes field), or named hush put stripe-prod. Then:
hush pull --name stripe-prod --json
hush pull --json # one-shot scan for all `hush put NAME` itemsAdd --consume to trash the vault item after it is stored. hush listen is the polling daemon variant (human use). Agents should use pull.
A Signal text is a poor pipe past about 1000 characters. RSA alone is worse: OAEP with a 2048-bit key wraps a few hundred bytes, not the message.
hush box keeps the body on the two machines. It encrypts the payload with AES-256-GCM and wraps that key with the recipient's RSA public key. The directory, when you use one, stores public keys only.
The public directory is https://hush-directory-828110571677.europe-west1.run.app. HUSH_DIRECTORY_URL overrides it. The keys stay on the machines. The directory only keeps the public key and the sealed file, which it cannot read.
hush box register robin
# publishes robin@hush.sh. The same address is accepted as `robin`.
hush box seal --to robin@hush.sh --file ./photo.png -u 1
# prints {"event":"sealed","id":"...","to":"robin@hush.sh",...}
hush box open --id ID photo --out ./photo.png-u is required. Each open pulls once and the counter goes down. At 0 the directory deletes the sealed copy. Copy the same box.key and box.pub to another machine if both should open mail for that address, or register a second address and seal with --to.
open writes the vault and, with --out, the file. It prints metadata (name, sender, bytes, uses_left). It does not print the payload. The message you send is only the id, so the Signal text limit does not apply. The file can be any type.
Create an expiring, hidden-text Bitwarden Send from an encrypted vault entry:
hush send --name SERVICE_PASSWORD --title "Service access" --days 7 --jsonThe command prints only a Send link and expiry metadata. It uses the ambient
Bitwarden session or the encrypted BITWARDEN_SESSION, sends the payload through
subprocess stdin, and never places the value in arguments or temporary files.
The sender email is hidden. --days accepts 1–31 days and controls both expiration
and deletion; --max-access-count optionally limits retrievals. Only nonempty
UTF-8 text up to 1,000 characters is accepted. Raw Bitwarden failure output is
discarded to prevent a failed command from leaking the text or session.
hush list --json
hush run --name stripe-prod --env STRIPE_API_KEY --redact -- \
curl https://api.stripe.com/v1/chargesThere is no show / get. Decrypt happens in memory and is injected into the child environment.
Agents always use --redact: child stdout/stderr is filtered so every
occurrence of the secret becomes [redacted by hush].
The skill tells the agent the rules; these mechanisms enforce them.
hush agent-shim --dir ~/.hush/agent-bin # human: put FIRST in agent PATH
hush doctor --json # checks 0700/0600 modes + setup- Direct
bwis blocked. The shim installed asbwin the agent's PATH refuses every call with a pointer back to hush. Without it,bw send receive <url>needs no login and prints the secret straight into the transcript. The human keeps using the realbw; hush itself viaHUSH_BW_BIN. - Secret-bearing env never reaches the child.
hush runstripsBW_SESSION,BW_CLIENTID,BW_CLIENTSECRET,BW_PASSWORDandBITWARDENCLI_APPDATA_DIR: the child gets secrets only through--env. Otherwise a child shell could read the session and drivebwitself. --redactfilters child output (streaming, boundary-safe), so even a command that echoes its input cannot leak into logs.doctorfails on loose permissions (~/.hushmust be 0700,identity0600) and reports whether the effectivebwis a shim.
Honest limit: on one shared Unix user these are guardrails, not a proof —
a determined process can still exec the real bw by absolute path or read
the identity file. The hard sandbox (phase 2) is a dedicated agent user
plus a hushd broker owning the identity and the Bitwarden session, with
the agent talking to it over a Unix socket and never touching key material.
Until then: separate users where you can, shim + --redact everywhere.
~/.hush/config.json
~/.hush/identity # age X25519, mode 600
~/.hush/box.key # RSA private key for box, mode 600
~/.hush/box.pub # RSA public key for box
~/.hush/vault/<name>.age
~/.hush/vault/<name>.meta.json
Override the home directory with --home or HUSH_HOME. Point at another bw binary with HUSH_BW_BIN. bw auth (BW_SESSION, BITWARDENCLI_APPDATA_DIR) is inherited from the environment.
Hush stops secrets from entering agent transcripts: bw output is piped, never printed. It does not protect you from a process running as the same OS user with the identity file. hush run is the supported use path; reading vault ciphertext is expected, reading identity + decrypting is the bypass. Never run bw get / bw send receive by hand in front of an agent — the value would land in the transcript.
cargo fmt --check
cargo clippy --locked -- -D warnings
cargo test --lockedIntegration tests stub the Bitwarden CLI with tests/fixtures/fake-bw.sh (via HUSH_BW_BIN + FAKE_BW_DIR).
Live end-to-end (real account + server) was verified manually: Send receive → pull --send,
vault item → pull scan, --consume trash, listen poll, and run injection (compared by hash, never printed).
MIT. Copyright (c) 2026 Turing Labs.