hushsecrets your AI agent can use but never readGitHub

Changelog

All notable changes to hush. The format follows Keep a Changelog; versions follow semver. Pre-1.0, minor versions may break things — each entry says so when it does.

Unreleased

0.9.0 — 2026-09-30

Everything planned for 0.9: hush without Node, a key in the Secure Enclave, approvals for a machine nobody is sitting at, Windows in beta, and the docs as a site. No vault format change: a 0.8 vault opens unchanged, and 0.8 can open a vault written by this version, unless it has an enclave member (below).

A key in the Secure Enclave, with nothing to install

hush id --enclave makes a P-256 key inside the Mac's Secure Enclave. It cannot be copied off the machine, and every use asks for your fingerprint (or the Mac's password), enforced by the enclave. hush secure --hardware now offers this first on a Mac: it makes the key, adds it to the vault as you, and tells you to retire the software key. No Apple Developer ID is involved; see docs/BIOMETRY.md for how. An enclave member appears as hush_se_…, and as enclave in hush team ls. A vault with an enclave member needs this version to open.

hush without Node: one file, an installer, Homebrew

Approvals where there is no desktop

Over SSH, in a devcontainer, on a server, there was no one to ask, so every gated action was refused. Now hush approvals pair on the server and hush approvals accept on your laptop pair the two. From then on, an approval the server cannot show itself goes to your laptop (hush approvals listen): the usual dialog, or Touch ID, and a signed answer back. The relay in between can neither read nor forge a request or an answer, and cannot replay one. hush relay serve runs one yourself; with ssh -R nobody else is involved. The protocol is in docs/RELAY.md.

Windows, in beta

npm install now works on Windows. Your key is kept with DPAPI, approvals are a native Windows dialog, hush run npm … works through npm.cmd, secret files get owner-only ACLs, and there is a PowerShell hook. A Windows CI job checks these paths; tell us what breaks.

Security

Docs

Fixed

0.8.0 — 2026-09-30

Breaking: vault format hush/v3. A signed vault can be opened only by hush 0.8 or newer. Vaults are signed when made with hush init, or the first time an admin changes an existing vault's membership, or with hush team sign. Everyone on the team should upgrade before that happens.

Signed vaults: only an admin can change who can read one

An admin now signs the vault's header — every member's keys and role, the sets a scoped member may read, and a commitment to every data key — and every member's hush checks the signature against admins their machine already trusts, and checks that the key it unwrapped is the one the header commits to.

Giving someone only some sets

CI identities and a GitHub Action

After someone leaves

Also

0.7.0 — 2026-09-30

Vault merges go key by key

Two branches that both touched .hush/vault.json used to conflict as a wall of base64, and taking one side dropped the other's secrets.

Nine coding agents

hush install-mcp and hush install-skill now know Gemini CLI, VS Code (Copilot agent mode), Windsurf, Zed, Cline and Continue as well as Claude Code, Codex and Cursor — each in the file its own documentation names. Zed's settings file keeps every comment (the entry is inserted, not re-serialised). Codex, Gemini CLI and Zed share .agents/skills/, so the skill is written once for all three.

The audit log is a hash chain

Each line of .hush/audit.log carries the SHA-256 of the line above it. hush audit shows the log; hush audit verify names the first line that does not follow — an edit, a removal, an insertion or a reorder. It makes an edit visible, not impossible (SECURITY.md says so).

hush ui now prints http://127.0.0.1:…/#t=<token>. The fragment never reaches a server or a log; the page reads it, wipes it from the address bar and keeps it for the tab. The page itself no longer contains the token. It also refuses to be framed by another page.

Under the hood

0.6.0 — 2026-09-30

Upgrade recommended for every team vault.

hush notices when a vault's membership or key changes unexpectedly

Each machine now remembers, per vault, the members it has accepted, a commitment to the data key behind each generation it has seen, and which vault lives at which path. When something changes that nobody on this machine did — a member someone else added, a different key where one was already seen, a different vault in the same place — hush refuses to decrypt or add anything there until a person looks:

Members removed and key rotations made by teammates go through on their own; a rotation made elsewhere is mentioned once. The first time a machine sees a vault it is accepted as it stands (trust on first use).

Behaviour change: after a teammate adds someone, everyone else runs hush team accept once before their next hush run. That is the point.

The policy floor is created when an agent is set up

~/.hush/policy.json — your floor, which no repository's policy can go below — now gets written (empty) whenever an agent is brought near a vault: hush install-mcp (it is on the "this will write" list), hush install-skill, and answering yes to "will an AI agent use secrets here?". Its existence is what keeps approvals on when a copy of the vault is opened from a folder with no policy of its own. hush level has a new rung-3 check for a machine with an agent registered and no floor, and hush secure floor writes one.

Fixed

0.5.0 — 2026-09-24

The app, redesigned around what you came to do

hush ui is rebuilt. The old page was five sections of lists and jargon ("What a run gets", "rung 0 of 5"), with keys hidden behind a chevron and native prompt() boxes for editing. The new one:

Under the hood, the page lives in src/ui-page.ts, and the DOM is built only through textContent and attributes. No string of HTML exists for a value to escape from, and a test forbids every API that could create one. /api/state adds needs (variable names and file paths, from a scan cached for 10 s), posture.next and policy.promptAvailable.

Changed — asks instead of refusing, and the library stays a library

Breaking (pre-1.0): your library's default set is no longer injected into every folder. The library is a catalog: nothing in it reaches a project until that project adds it. To keep the old behaviour in a folder, run hush use default --library there. It is recorded as library:default in .hush/envs.json, and you can add more from the library later.

Less that hush does to a project without asking

Fixed

0.4.0 — 2026-09-16

hush install-mcp learns which agent it is talking to

It wrote .mcp.json — Claude Code's file — whatever agent was in the room, and printed a tick either way. In a Codex session that file is never read, so the one step that was supposed to make your agent aware of hush did nothing, and said it had worked. That is the worst shape a setup step can take.

It now detects the agents on the machine and writes what each one reads: Codex gets [mcp_servers.hush] in ~/.codex/config.toml (via codex mcp add when the CLI is there, otherwise an append that leaves the rest of the file untouched), Claude Code keeps .mcp.json, and Cursor gets .cursor/mcp.json. An entry you already have is never rewritten, and a file hush cannot parse is reported rather than clobbered. When it finds no agent at all it says so and prints the line to paste instead of writing a file nobody opens. --for codex|claude-code|cursor registers one it could not see.

hush install-skill follows the same map: Codex reads .agents/skills/hush/SKILL.md (documented path, not .codex/skills), Claude Code keeps .claude/skills/…, and Cursor gets a rule at .cursor/rules/hush.mdc with the frontmatter it expects. hush doctor now asks every agent rather than only Claude Code, so a working Codex setup no longer reads as "run install-mcp".

A security review, and the fixes it turned up

An outside audit (Cloudflare's security-audit skill) was pointed at the one threat hush exists for: an AI agent with a shell that must not be able to hand itself a credential a human did not approve. It confirmed nine issues, each with a working local reproduction, and all nine are fixed. Two of them are worth knowing about because the behaviour you see changes.

The five leads that were found but not independently validated were fixed too: a repeated name in a dotenv import is now reported instead of silently overwritten, the add-secret dialog names the vault the value goes into, the request dialog names every secret the request really sends, key and set names from a hand-edited vault file are scrubbed before they reach a terminal, and the request summary and its substitution use one parser rather than two.

hush start — a way in for someone who has never used this

Arriving at hush meant arriving at forty commands, a vocabulary (vault, set, scope, layer) and a README. That is not how anyone wants to spend their first five minutes: they want their keys in and their project running.

cd your-project
hush start

It finds the .env files in the folder, offers them first (the usual answer), stores what it finds, asks the one question hush always asks up front (will an AI agent be near these keys), offers to run the dev script, and ends by naming three commands rather than forty. The other answers it accepts are "in another tool" (it prints the exact one-line pipeline for Doppler, 1Password, AWS, Infisical or Vault, and stores nothing itself) and "add one now" (the value goes into a hidden prompt, never onto the screen).

The wording lives in src/start.ts, where it is tested as wording: the opening never says "vault", "scope" or "layer"; the first choice always matches what is actually on disk; and the ending is asserted to name exactly three commands.

Discoverability, since a guided run nobody finds is not a guided run:

hush import reads another tool's export (it used to be an alias)

The command everyone reaches for is import, so it now does the importing. The name previously meant "the pre-unification spelling of hush add <file>", which nobody should be typing, and its one legacy form fails with the exact replacement rather than quietly doing something else:

hush import .env --env prod # removed hush add .env --to prod # what it did

hush import <file|-> --as <set> [--format dotenv|json|1password] is the new meaning. For a .env the two are the same thing, so the migration is one flag. There is no hush adopt, which is what this was called for about an hour.

How long an "Allow" lasts is now easy to change

The approval dialog has always offered "Allow once" and "Allow 15 min", with the second one built from approvalTtlSeconds. Fifteen minutes was only changeable by hand-editing policy.json, which is not a thing to ask of someone who is being interrupted by it.

Credentials that are a file: --materialize

Some tools read a path, not an environment variable: GOOGLE_APPLICATION_CREDENTIALS, KUBECONFIG, a .p12, a client certificate. The only answer before this was hush export, which writes every value in the vault to disk in plaintext and leaves it there.

hush run --materialize GOOGLE_APPLICATION_CREDENTIALS -- node app.js
hush run --materialize KUBECONFIG=/tmp/kube.config -- kubectl get pods

.env.schema validation

The most common failure is not a leak, it is a wrong value. A .env.schema in @env-spec syntax (the one Varlock reads, so an existing schema works) declares what a value should look like:

# @required @type=url API_URL= # @type=string(startsWith=sk-) STRIPE_SECRET_KEY= # @sensitive=false APP_ENV=development

hush import — coming from another tool

hush add .env was the only door in. hush import reads what another tool already exports — no accounts, no vendor SDKs, no network:

doppler secrets download --format json --no-file | hush import - --as "Prod"
op item get "Stripe" --format json | hush import - --format 1password --as "Work"
hush import secrets.json --format json --as "Prod" --dry-run

hush get --copy

hush get KEY prints a live credential into the scrollback. --copy sends it to the clipboard through a pipe instead, printing copied KEY to the clipboard (24 characters). The tool is found on PATH (pbcopy, wl-copy, xclip, xsel), the same way the age bridge finds age; finding none is a message naming what to install rather than a silent fall back to printing. Same reveal gate.

Exposure warnings

Two places a value can escape in a way hush cannot mask, both previously silent:

hush request — an API call with a credential you never hold

hush run needs a program that already knows how to authenticate itself. When there is no such program and something just needs to hit an endpoint, the only answer used to be curl — which is denied by default, because a shell holding the value can post it anywhere redaction cannot follow.

hush request makes the call itself. The value goes from the vault into a header inside the hush process and onto the wire; you and your agent only ever see the response, with any reflected value masked:

hush request POST https://api.stripe.com/v1/refunds \
  --header 'Authorization: Bearer $STRIPE_KEY' \
  --data '{"charge": "ch_123"}'

0.3.0 — 2026-09-14

0.2.0 — 2026-09-12

0.1.3 — 2026-09-12

One vocabulary: everything is a set

0.1.2 had two ways to describe the same bytes — "environments" and "service accounts" — with two command families, two UI sections and two MCP argument shapes. 0.1.3 collapses them. A set is some keys with a name you chose, an optional description and a note on when to use it. It lives in your library (yours, never in a repo) or in the project vault (committed). A project uses sets: its own default is the floor, everything else layers on top in the order you added it, later wins.

Eight commands, and hush in front of anything

hush add <file|KEY=value>   save secrets as a named set
hush use <set> ...          this project uses these sets
hush run -- <cmd>           run with them injected
hush dev                    run your dev script with them
hush ls                     library, project, what is used
hush rm <KEY|set>           remove
hush ui                     the app
hush team add|rm            share this project's vault

hush help --all             every command

Every old command still works and prints a one-line notice: hush set, hush import, hush accounts, hush env …, hush use fal=acme, --with a:b, hush add <svc> --account.

The app

One list of sets at two levels. Every card has the same use in this project toggle showing its position (● used · 2nd), in-place rename, description and when-to-use, an add-key row, move/reveal/replace/delete per key. The Service accounts section is gone; a New set form with an optional "for a service…" hint pre-fills the variable names a service needs.

Agents

Policy now gates the CLI too

.hush/policy.json applies to hush get, hush export, hush run (and pass-through, and hush dev) and hush add, not only to the MCP tools — an agent that shells out meets the same policy and the same approval prompt. Approval grants persist across processes (.hush/grants.local.json, mode 0600). allowEnvs names sets the way you do (work-fal), wherever they live. SECURITY.md says what this does and does not change.

Fixes

Upgrading

Nothing to migrate. use.json pins are still read; old command spellings still work.

0.1.2 — 2026-09-11

First public release: envelope-encrypted vault in the repo, per-member wraps, the MCP server with value-blind tools, output redaction, the local app, the security ladder, the age bridge for hardware keys.