Send someone a secret over a link that works once.
Go to file
jx12n 65b37afd92
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
Record every hush release on the courier release channel
Two steps after deploy: crane exports the courier CLI out of courier's
own (distroless) image, pinned by tag and digest, and alpine/git runs
`courier deployed hush prod $CI_COMMIT_SHA` as the `releases` agent —
one immutable version of releases/hush.prod carrying git log
<serving>..<this>, and one message on `deploys` keyed on that version.
The deploy step writes the image that was serving to .prev-sha before
`set image`, so the notes are what replaced what. Clone depth goes
1 → 100 so that log can be rendered. The credential is the Woodpecker
secret courier_releases_token, scoped to the alpine/git image and push
events. Same call every other substrate makes — orchard9-k3sf
scripts/lib/release.sh is the reference.
2026-09-05 16:40:39 -06:00
cmd serve the MCP install instructions at /mcp 2026-09-05 14:03:34 -06:00
deployments/k8s serve the MCP install instructions at /mcp 2026-09-05 14:03:34 -06:00
docs release: build through the cluster, not through its public address 2026-09-05 14:28:42 -06:00
internal serve the MCP install instructions at /mcp 2026-09-05 14:03:34 -06:00
scripts release: stop building under the server's egress policy 2026-09-05 14:30:54 -06:00
vendor hush: one-time secret links the server cannot read 2026-09-03 00:08:38 -06:00
.dockerignore document the CSP the pages override; allowlist the build context 2026-09-03 01:01:28 -06:00
.gitignore document the CSP the pages override; allowlist the build context 2026-09-03 01:01:28 -06:00
.woodpecker.yml Record every hush release on the courier release channel 2026-09-05 16:40:39 -06:00
Dockerfile hush: one-time secret links the server cannot read 2026-09-03 00:08:38 -06:00
go.mod hush: one-time secret links the server cannot read 2026-09-03 00:08:38 -06:00
go.sum hush: one-time secret links the server cannot read 2026-09-03 00:08:38 -06:00
Makefile serve the MCP install instructions at /mcp 2026-09-05 14:03:34 -06:00
README.md serve the MCP install instructions at /mcp 2026-09-05 14:03:34 -06:00

hush

Send someone a secret over a link that works once.

Production: https://hush.threesix.ai

Paste a secret, get a link, send the link. The first person to open it and press Reveal sees the secret; the link is dead from that moment. Nobody needs an account, a client, or anything installed — a browser is the whole requirement.

The server cannot read what you sent. Encryption happens in your browser and the key lives in the URL fragment (…/s/ID#KEY), which browsers never transmit. hush stores ciphertext it has no way to open. That is not a promise about our operational discipline; it is a property of where the key sits.

What one-time actually buys you

Worth being precise, because "one-time link" is often oversold:

  • Bounded exposure. The secret is fetchable once, for at most its TTL, then it is gone. A credential sitting in a Slack thread is fetchable forever by anyone who later gains access to that thread.
  • Tamper evidence. If your recipient says "already used", someone else opened it. You have learned something a plain paste never tells you.
  • Nothing at rest to steal. A dump of hush's Redis yields ciphertext and no keys.

And what it does not buy you:

  • It does not protect the link. Whatever channel carries the link could be read by whoever can read that channel. One-time-ness limits the damage and makes it detectable; it does not make the channel private.
  • It does not authenticate the reader. Anyone holding the link can open it. The link is the capability. Treat it like the secret it carries.

If a secret must reach one specific verified human and nobody else, this is the wrong tool — use a channel with identity.

Usage

In a browser

  1. Open https://hush.threesix.ai.
  2. Paste the secret, press create a secret.
  3. Copy the link and send it however you like.
  4. The recipient opens it, presses reveal the secret, and reads it once.

There is no lifetime picker: the page offers one action, and the server applies its default TTL (24 hours). ttl_seconds on the API is where a caller that cares chooses.

Why there is a button

Slack, Teams, WhatsApp, iMessage and Outlook Safe Links all fetch a URL to build a preview before any human sees it. A service that destroys on GET therefore destroys most secrets in transit, and the recipient's "already used" is indistinguishable from a real interception.

So in hush, GET /s/{id} is a static page that touches no storage at all. Only POST /s/{id}/reveal reads and destroys. Link previewers are harmless by construction, not by user-agent guessing.

API

The API takes ciphertext. There is no endpoint that accepts a plaintext secret, because such an endpoint would make the server able to read secrets and the claim at the top of this file would become a matter of trust rather than arithmetic.

POST /api/secrets
{ "ciphertext": "<base64url AES-256-GCM, nonce prepended>", "ttl_seconds": 86400 }
→ 201 { "id": "…", "expires_at": "2026-09-04T…Z", "ttl_seconds": 86400 }

POST /api/secrets/{id}/reveal
→ 200 { "ciphertext": "…" }        first caller only, secret destroyed
→ 410 { "error": { "code": "gone" } }   every other case

410 gone is returned identically whether the id never existed, was already revealed, or expired. Distinguishing those would confirm to an attacker that a particular link once existed.

GET / serves the create page, GET /s/{id} the reveal page. /healthz, /readyz and /metrics are served on the same port but are not routed by the public ingress — they are reachable in-cluster only.

From an agent, over MCP

cmd/hush-mcp is a stdio MCP server exposing two tools, hush_create and hush_reveal. It runs locally and does the encryption on your machine, so using hush from an agent preserves the same zero-knowledge property as using it from a browser.

go install github.com/orchard9/hush/cmd/hush-mcp@latest

Per-client configuration — Claude Code, Codex, Gemini, VS Code, Claude Desktop, Cursor, omp — is served by the deployment at https://hush.threesix.ai/mcp. docs/MCP.md covers the design.

Limits

Thing Value Why
Ciphertext ≤ 64 KiB It is a courier for credentials, not a file host
TTL 5m … 7d, default 24h Long enough to be useful, short enough to bound exposure
Rate limit 30 creates / 10 min / IP Anonymous create is otherwise a free blob host
Reveals per secret exactly 1 The product

Operating it

Development

make help          # every target
make release       # build this commit in-cluster and roll it out, then smoke it
make test          # unit tests, no external dependencies
make dev           # a local Redis in Docker + hushd on :18500
make smoke         # full create → reveal → gone against the local instance
make vendor        # refresh vendor/ after a dependency change

go-chassis is a private module, so dependencies are vendored and both CI and the container build run with -mod=vendor and no network. make vendor is the only way dependency versions change.