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/hushOne npm package, no native build step. Node 20 or newer.
2. Sign this device in
$ hush loginThe 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 deviceinit 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 -0Every 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 memoryOn 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.
| Command | What it does | Key flags |
|---|---|---|
hush login | Approve this device in the browser and store a token on this machine. | --device-name <name> |
hush logout | Forget the local token, and revoke it on the server when asked. | --all |
hush whoami | Print the signed-in account, the token name and where the keyring lives. | --json |
hush init | Attach the current directory to a project and environment, then build a keyring. | -e, --env · --project <slug> |
hush push | Encrypt the local .env and store it as the next version. | -e, --env · -m, --message · -y |
hush pull | Fetch a version, decrypt it here and write .env. | -e, --env · --version <n> · --force |
hush diff | Compare the local file with the remote version; values stay masked. | -e, --env · --version <n> |
hush check | Exit with code 4 when local and remote disagree, for pre-commit and CI. | -e, --env |
hush run | Run a command with the environment injected in memory; no file is written. | -e, --env · -- <command> |
hush ls | List projects, environments, or the variable names inside a version. | -e, --env · --keys |
hush history | List versions with author, message and timestamp. | -e, --env · --limit <n> |
hush rollback | Make an earlier version current again, and push it forward as a new version. | -e, --env · -y |
hush invite | Invite a GitHub account and seal the project key to their public key. | --role <admin|member|readonly> |
hush members | List members, roles and pending invites for the project. | --json |
hush revoke | Remove a member and rotate the project key. | -y |
hush key export | Write the project key out so another machine of yours can unlock versions. | --out <path> |
hush key import | Import a project key from a file or from stdin. | --file <path> |
hush lock | Lock the local keyring so decryption needs your passphrase again. | --timeout <minutes> |
hush unlock | Unlock the keyring with your passphrase. | - |
hush doctor | Check token, keyring, connectivity and config sanity in one pass. | --json |
hush completion | Print a completion script for your shell. | bash | zsh | fish |
Global flags
| --json | Machine-readable output for every command. |
|---|---|
| --api-url <url> | Point the CLI at another deployment, staging or self-hosted. |
| --no-color | Disable ANSI colour and the spinner, for CI logs. |
| -y, --yes | Skip 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.
| 0 | Success. |
|---|---|
| 1 | Something went wrong, network, disk, or a rejected push. |
| 2 | Bad arguments or an unknown flag. |
| 3 | No valid token, or the token was revoked. |
| 4 | Local and remote disagree, resolve, then retry. |
Team workflow
Invite
$ hush invite @teammate --role member
$ hush membersRun 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 v5revoke 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
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 exportto hand the project key to the job, thenhush pullinside it. The server still only sees ciphertext. - Gate the build with
hush checkand 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.