Skip to content
turinglabsorgPublic

About

Agent-blind secrets: ingest over Bitwarden, store with age, use by name

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

hush

██╗  ██╗██╗   ██╗███████╗██╗  ██╗
██║  ██║██║   ██║██╔════╝██║  ██║
███████║██║   ██║███████╗███████║
██╔══██║██║   ██║╚════██║██╔══██║
██║  ██║╚██████╔╝███████║██║  ██║
╚═╝  ╚═╝ ╚═════╝ ╚══════╝╚═╝  ╚═╝

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.

How hush works

┌────────┐  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

Install the latest GitHub Release (SHA-256 verified; cosign if present):

curl -fsSL https://raw.githubusercontent.com/turinglabsorg/hush/main/install.sh | sh

With the agent skill and a PATH symlink:

curl -fsSL https://raw.githubusercontent.com/turinglabsorg/hush/main/install.sh | sh -s -- --agent-skill --path-link

Pin 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-skill

That 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 ok

Agent email

If 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.

Generate locally

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 --json

The 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 --json

If 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.

Account password requirements

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.

Authenticator codes

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-command

The 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.

Bitwarden CLI compatibility

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.

Deposit

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> --json

For 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 10

The 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` items

Add --consume to trash the vault item after it is stored. hush listen is the polling daemon variant (human use). Agents should use pull.

Exchange a file

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.

Share a secret

Create an expiring, hidden-text Bitwarden Send from an encrypted vault entry:

hush send --name SERVICE_PASSWORD --title "Service access" --days 7 --json

The 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.

Use

hush list --json
hush run --name stripe-prod --env STRIPE_API_KEY --redact -- \
  curl https://api.stripe.com/v1/charges

There 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].

Agent sandbox

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 bw is blocked. The shim installed as bw in 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 real bw; hush itself via HUSH_BW_BIN.
  • Secret-bearing env never reaches the child. hush run strips BW_SESSION, BW_CLIENTID, BW_CLIENTSECRET, BW_PASSWORD and BITWARDENCLI_APPDATA_DIR: the child gets secrets only through --env. Otherwise a child shell could read the session and drive bw itself.
  • --redact filters child output (streaming, boundary-safe), so even a command that echoes its input cannot leak into logs.
  • doctor fails on loose permissions (~/.hush must be 0700, identity 0600) and reports whether the effective bw is 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.

Layout

~/.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.

Threat model

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.

Development

cargo fmt --check
cargo clippy --locked -- -D warnings
cargo test --locked

Integration 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).

License

MIT. Copyright (c) 2026 Turing Labs.

About

Agent-blind secrets: ingest over Bitwarden, store with age, use by name

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages