Skip to content

Docs

Documentation

Everything the CLI does, in the order you will need it. If you read one section, read Quickstart, the rest is reference.

Quickstart

1. Install the CLI

npm i -g @xnah/hush

One npm package, no native build step. Node 20 or newer.

2. Sign this device in

$ hush login

The CLI prints a code like K7QM-2XPD and waits. Open hush.hanx.pro/device, approve it with your GitHub account, and the terminal picks up the token. The code expires after 10 minutes.

3. Attach a repository

$ cd ~/code/api
$ hush init
  project      api
  environment  dev
  passphrase   ••••••••••••
✔ Keyring created · project key sealed to this device

init asks which project and environment this directory belongs to, then creates your device keypair and locks the private key with your passphrase. The keyring stays on this machine; the private key is never uploaded.

4. Push a version

$ hush push
✔ Encrypted .env (34 vars)
✔ Pushed v4 to dev  +2 ~1 -0

Every push is a new immutable version, up to 50 per environment , so an accidental change is a rollback, not a recovery project.

5. Pull it anywhere, or don't

$ hush pull                 # writes .env in this directory
$ hush run -- npm run dev   # or keep it in memory

On the next machine, login once and pull. Run hush diff before pulling if you want to see what is about to change. If a step looks wrong, hush doctor prints what the CLI thinks is going on.

The .env file is never uploaded. What leaves your machine is ciphertext, plus the metadata any sync service needs: names, sizes and timestamps.

Command reference

Every command is hush <command>. The table lists the flags that matter beyond the global ones.

hush commands, what they do and their key flags
CommandWhat it doesKey flags
hush loginApprove this device in the browser and store a token on this machine.--device-name <name>
hush logoutForget the local token, and revoke it on the server when asked.--all
hush whoamiPrint the signed-in account, the token name and where the keyring lives.--json
hush initAttach the current directory to a project and environment, then build a keyring.-e, --env · --project <slug>
hush pushEncrypt the local .env and store it as the next version.-e, --env · -m, --message · -y
hush pullFetch a version, decrypt it here and write .env.-e, --env · --version <n> · --force
hush diffCompare the local file with the remote version; values stay masked.-e, --env · --version <n>
hush checkExit with code 4 when local and remote disagree, for pre-commit and CI.-e, --env
hush runRun a command with the environment injected in memory; no file is written.-e, --env · -- <command>
hush lsList projects, environments, or the variable names inside a version.-e, --env · --keys
hush historyList versions with author, message and timestamp.-e, --env · --limit <n>
hush rollbackMake an earlier version current again, and push it forward as a new version.-e, --env · -y
hush inviteInvite a GitHub account and seal the project key to their public key.--role <admin|member|readonly>
hush membersList members, roles and pending invites for the project.--json
hush revokeRemove a member and rotate the project key.-y
hush key exportWrite the project key out so another machine of yours can unlock versions.--out <path>
hush key importImport a project key from a file or from stdin.--file <path>
hush lockLock the local keyring so decryption needs your passphrase again.--timeout <minutes>
hush unlockUnlock the keyring with your passphrase.-
hush doctorCheck token, keyring, connectivity and config sanity in one pass.--json
hush completionPrint a completion script for your shell.bash | zsh | fish

Global flags

Flags every hush command accepts
--jsonMachine-readable output for every command.
--api-url <url>Point the CLI at another deployment, staging or self-hosted.
--no-colorDisable ANSI colour and the spinner, for CI logs.
-y, --yesSkip confirmations. Required for scripted destructive runs.
-e, --env <name>Pick the environment for this command, overriding the directory default.

Exit codes

hush check and friends are meant to be scripted, so the exit code carries the verdict.

hush exit codes
0Success.
1Something went wrong, network, disk, or a rejected push.
2Bad arguments or an unknown flag.
3No valid token, or the token was revoked.
4Local and remote disagree, resolve, then retry.

Team workflow

Invite

$ hush invite @teammate --role member
$ hush members

Run invite on a machine that already holds the project key, that is what lets the CLI seal the key to your teammate's public key. The invite that travels through the server is sealed to them and unreadable to us.

Accept

They sign in with GitHub and accept in the dashboard. Their device keypair opens new versions from then on; nothing else about their account changes the encryption.

Revoke and rotate

$ hush revoke @teammate -y
✔ Removed · project key rotated to v5

revoke removes the member and rotates the project key, so their copy cannot open future versions. Roles run from readonly to owner; inviting and revoking need admin or owner.

The honest caveat

Revocation protects the future, not the past. Anything a teammate already pulled stays decrypted on their disk, and versions they could read before the rotation stay readable with the key they already have. If that matters, rotate the credentials themselves, the API key, the database password, inside the service those variables point to.

CI

Roadmap

Service tokens are on the roadmap.

Until non-interactive tokens ship, these are the honest options:

  • Keep a dedicated account approved once on the runner, with its token in the CI secret store.
  • Use hush key export to hand the project key to the job, then hush pull inside it. The server still only sees ciphertext.
  • Gate the build with hush check and let exit code 4 fail it.

None of this needs your passphrase in CI if the job's keyring is already unlocked. Watch the repository for the service-token work.

FAQ

Is my data safe?
Your values leave the machine encrypted, and the server has no key. The practical weak points are the devices you run the CLI on and your passphrase, and this is still a beta, do not store credentials you cannot rotate.
What if I lose my passphrase?
The recovery key shown once during setup unlocks the same device key. If you have lost both, the data is unrecoverable: there is no reset, no escrow and no support override, because any of those would be a backdoor.
Can the server read my values?
No. It stores XChaCha20-Poly1305 ciphertext, and the key is derived from your passphrase on your machine. Ciphertext is bound to projectId|environment|version|keyVersion, so a blob cannot be replayed into another environment without failing to decrypt.
How does rotation work?
A new project key is generated and sealed to every remaining member's public key. It protects future versions only, anything a member already pulled stays readable on their disk. Rotate the underlying credentials too if that matters.
Why libsodium?
One small, widely reviewed library covering everything the product needs: X25519 keypairs, crypto_box_seal for wrapping keys, XChaCha20-Poly1305 for files and Argon2id for passphrases. No hand-rolled crypto and no half-used primitives.
Does it work offline?
Local operations do: hush run, lock, unlock, doctor, and diff against the version you last pulled. Anything that needs the server, push, pull, history, rollback, invites, needs a connection, because that is where the ciphertext lives.

Serverless architecture

The API is a set of stateless Vercel functions in front of a Postgres database. There is no long-lived process holding key material, because no process ever receives any: the server validates a request, writes ciphertext rows and answers.

Sessions use the GitHub OAuth cookie; the CLI authenticates with a token whose SHA-256 hash is the only thing stored. Rate limits are applied per token and per IP, and device codes expire after 10 minutes.

Background work is a cron route: sweeping expired device codes and pruning versions past the retention window. The pages you are reading are static, the marketing site fetches nothing about you.

Questions this page did not answer? Reach out via the repository.