English | Español | Français | Deutsch | Português (BR)
End-to-end encrypted secret sync for developer teams. Stop Slacking your .env.
Warning
Sotto is pre-1.0 and has not had a third-party cryptographic audit. It works end to end, but should not yet be trusted with critical production secrets. See SECURITY.md.
Sotto is built around one Rust crypto implementation shared by the native CLI and the browser client through WebAssembly. The server stores and synchronises encrypted data without ever receiving plaintext secrets or usable keys.
The end-to-end flow works: encrypt locally, sync ciphertext, decrypt on another device or in the browser, and share a single secret via a one-time link. Teams work end to end too: organisations with roles, per-member environment grants, key rotation on member removal, machine tokens for CI, and lost-key account recovery.
| Component | Available now |
|---|---|
| Crypto core | KDF, XChaCha20-Poly1305 AEAD + AAD, key wrapping, X25519 sealed-box grants, the environment vault hierarchy, data-key rewrap (rotation), share-link crypto, and key encoding - with native↔WASM golden vectors |
| CLI | init, local secret management, run-style injection, login/push/pull sync, new-device setup, share; teams: org create/ls/invite/members/remove, grant, clone, rotate, machine token create/ls/revoke (with SOTTO_TOKEN mode for CI), lost-kit reset |
| Server | OAuth login + sessions, account + snapshot sync (versioned writes, ETag), orgs + memberships + roles, per-member vault-key grants, transactional key rotation, machine tokens, account reset, and share links - ciphertext only |
| Web | Login (cookie session), in-browser unlock + vault decryption via your own grant, one-time share create/receive, and a team panel: orgs, members, invite by email, share an environment with a member |
Prebuilt, signed binaries for macOS (Apple Silicon + Intel), Linux (x86_64 + ARM64), and Windows x86_64:
curl -fsSL https://raw.githubusercontent.com/getsotto/sotto/main/install.sh | shirm https://raw.githubusercontent.com/getsotto/sotto/main/install.ps1 | iexThe installer verifies the archive's SHA-256 checksum - and its Sigstore signature, when cosign
is installed - before installing (~/.local/bin on macOS/Linux, %LOCALAPPDATA%\sotto\bin on
Windows). Prefer to look first? Grab an archive from the
releases page and verify it manually per
SECURITY.md, or build from source (see Developing).
sotto completions <shell> prints a completion script to stdout. For example:
# bash
mkdir -p ~/.local/share/bash-completion/completions
sotto completions bash > ~/.local/share/bash-completion/completions/sotto
# zsh (add the directory to fpath in ~/.zshrc first)
mkdir -p ~/.zfunc
sotto completions zsh > ~/.zfunc/_sotto
# fish
mkdir -p ~/.config/fish/completions
sotto completions fish > ~/.config/fish/completions/sotto.fishRelease tarballs and zips also contain the matching completions/sotto.* files if you prefer to
copy them directly.
For GitHub Actions, use the Sotto Setup action to
install an exact CLI release and verify its checksum and Sigstore bundles before making sotto
available to later steps:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: getsotto/sotto-action@v1.1.0
with:
sotto-version: v0.9.0
- run: sotto run -- npm test
env:
SOTTO_SERVER: ${{ vars.SOTTO_SERVER }}
SOTTO_TOKEN: ${{ secrets.SOTTO_TOKEN }}The action ref and sotto-version are independent. Keep sotto-version as an exact vX.Y.Z
release. Set the optional SOTTO_SERVER repository variable
for a self-hosted server. See the action documentation
for matrix, Windows, and reusable-workflow examples.
Want a runnable demo? sotto-example walks through local secret injection with a Python GIF and copyable steps, plus tiny examples in JavaScript, TypeScript, Java, C#, PHP, Go, and C++. No account required; instructions cover macOS, Linux, and Windows.
sotto init # create your identity + first project - SAVE the printed Emergency Kit
sotto set DATABASE_URL # hidden prompt; encrypted locally before it ever touches disk
sotto import .env # optional: pull in an existing file, still encrypted locally
sotto export --format dotenv --reveal # print a .env; refuses a terminal without --reveal
sotto run -- npm start # inject the environment's secrets into any command
sotto login && sotto push # optional: sync ciphertext via the hosted instance (getsotto.co.uk)
sotto get DATABASE_URL -c # copy a secret without printing it; clipboard clears after 45s when unchanged
sotto share DATABASE_URL # one-time link; copied automatically in an interactive terminal
sotto share DATABASE_URL --views 3
sotto share DATABASE_URL --expire 3600 # lifetime in secondsBy default, a share allows one view and has no expiry; the link burns after the last view.
Use --env to select an environment for one command without changing the project's default:
sotto run --env staging -- npm test
sotto ls --env staging--env lasts for that command only; sotto env use changes the default.
Export writes plaintext, so it needs --reveal on a terminal, just like sotto get.
Use sotto share --no-copy to disable interactive copying, or --copy to request it explicitly.
Clipboard clearing is best-effort: replacing the clipboard protects the newer content, while
clipboard managers, suspension, or a terminated helper may retain a history copy.
sotto login uses the hosted instance at getsotto.co.uk unless you point
it elsewhere with --server <url> (see Deploying to run your own). Either way
the server only ever stores ciphertext - the web vault at the same address decrypts in your
browser, with keys that never leave your devices.
Working with a team:
sotto org create acme # prints the org id
sotto init --org <org-id> # an org-owned project
sotto org invite <org-id> dev@example.com # invite an existing Sotto user
sotto grant <user-id> # share the active environment (they run `sotto clone`)
sotto token create --name ci # SOTTO_TOKEN: run/export in CI, no password neededMachine tokens expire. A new token lasts 90 days unless you pass --expires-in-days with anything
from 1 to 365, and sotto token ls shows when each one ends. Two weeks before that, sotto run and
sotto export print a warning in the CI log. To replace a token, create a new one, update the CI
secret, then revoke the old one with sotto token revoke.
The CLI ships five built-in themes: nord (the default), sordino, terminal, monochrome,
and tokyo-night. Manage them with sotto theme:
sotto theme ls # list available themes; the active one is marked
sotto theme set nord # save a preference for later commands
sotto theme current # print the theme this shell resolves to--theme <name> picks a theme for one command. SOTTO_THEME sets it for a shell, and
sotto theme set saves it. Precedence is --theme, then SOTTO_THEME, then the saved
preference, then nord; an unknown name warns on standard error and falls back to nord.
Styling applies to interactive terminals only. --plain, a non-empty NO_COLOR (the
no-color.org contract), CI environments, piped standard output, and
redirected standard input each suppress colour and decoration while keeping the selected
palette, so scripts and logs read plain text.
Custom themes are TOML files in the themes directory inside your platform data directory:
~/Library/Application Support/sotto on macOS, %APPDATA%\sotto on Windows, and
$XDG_DATA_HOME/sotto or ~/.local/share/sotto on Linux (SOTTO_DATA_DIR moves that
directory). Each *.toml file needs the tokens below and takes its name from a name field,
or from the filename when the field is absent; a file that fails to parse is skipped. Colours
accept hex values, ANSI colour names, ansi(<index>) indices, and default.
bg = "#120024"
fg = "#ffffff"
accent = "#ff007f"
success = "#00ff66"
warning = "#ffaa00"
error = "#ff0033"
muted = "#775588"
border = "#331144"Saved as synth.toml, that file adds a synth theme. Apply it with sotto theme set synth,
or for one command with sotto --theme synth <command>.
sotto login # same account as the first machine
sotto setup # unpack the Emergency Kit onto this device
sotto pull # download the ciphertext you already pushedYou need the Emergency Kit printed by sotto init; without it, a new device cannot decrypt the vault.
CLI (native) ─────┐
├── sotto-core ── versioned encrypted data
Web client (WASM) ┘ │
▼
sync/API server
(ciphertext only)
The workspace contains four crates:
crates/core- shared cryptographic types and, the complete crypto implementation.crates/cli- thesottocommand-line interface and primary native client.crates/server- the Axum-based synchronisation API.crates/wasm-wasm-bindgenbindings that expose the core to web clients.
- Rustup with stable Rust 1.89 or newer
- The
clippyandrustfmtcomponents - The
wasm32-unknown-unknowntarget
The checked-in rust-toolchain.toml asks Rustup to install the required components and
target automatically.
Clone the repository, then build and test the complete workspace:
git clone https://github.com/getsotto/sotto.git
cd sotto
cargo build --workspace
cargo test --workspaceUse the CLI locally (no server required):
cargo run -p sotto-cli -- --help
cargo run -p sotto-cli -- init # create an identity + project; prints your Emergency Kit
cargo run -p sotto-cli -- set DATABASE_URL # hidden prompt
cargo run -p sotto-cli -- run -- your-command # inject secrets as env vars into a subprocessSecrets are encrypted at rest in a local SQLite store; the master key is cached in the OS keychain
with a TTL. Syncing to a server (login/push/pull/setup/share) is optional.
The server needs Postgres (a docker compose up -d brings one up for local use):
DATABASE_URL=postgres://sotto:sotto@localhost:5432/sotto cargo run -p sotto-server
curl http://127.0.0.1:8080/health # → okGitHub OAuth login requires GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET. For any non-local
deployment also set SOTTO_PUBLIC_URL to the server's externally reachable origin - it builds the
GitHub callback URL and must match the OAuth app's registered callback (it otherwise defaults to
http://localhost:8080) - and, for the web client, SOTTO_WEB_ORIGIN. Without OAuth the server
still boots (serving /health and running migrations), but login and every authenticated endpoint
(sync, share creation) are unavailable.
The browser client runs the same crypto core via WebAssembly (web/):
cd web
npm ci
npm run dev # dev server (proxies the API to localhost:8080)
npm run build # production bundle → web/dist (strict CSP + Subresource Integrity)One command brings up a complete hosted instance - Postgres, the server, and Caddy with automatic
HTTPS - from deploy/docker-compose.prod.yml; the runbook is
deploy/README.md. The pieces also work standalone: serve the web app and API
from one origin (so the session cookie and CSP stay same-origin) - the included
Caddyfile serves web/dist and reverse-proxies the API, with security headers;
Dockerfile builds the server image (migrations run on boot).
Run the same core checks used by CI:
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspaceSupply-chain policy is defined in deny.toml and checked in CI with
cargo-deny:
cargo deny checkThe full lockfile audit also runs in the supply-chain CI job.
Use Python 3.11 or newer; CI uses Python 3.12. The checker requires cargo-audit exactly 0.22.2:
cargo install cargo-audit --version 0.22.2 --locked
python3 -B -m unittest discover -s scripts/tests -v
scripts/check-cargo-auditThe audit policy records exact exceptions for dormant rsa and
spin lock entries. Each must match its package, version, source, kind and finding.
It must have no normal, build or dev dependency path across all targets, with default and
all workspace features. Cargo's dev edges correspond to dev-dependencies.
New findings, changed identities, reachable packages, failed scans and stale exceptions fail CI.
Remove an exception in the same PR that removes its finding; an empty exception list requires a
clean audit. The checker exits 0 on success and 1 on policy or scan failure. Raw cargo audit
still reports the dormant findings; the exceptions do not fix the vulnerable/yanked releases.
Project audit defaults keep advisory fetching and yanked checks enabled and override a developer's global audit configuration. Do not add advisory ignores or scan filters.
JSON mode can silently skip yanked checks in cargo-audit 0.22.2. The checker therefore requires an independent terminal-format scan with no registry failures and matching finding identities. This establishes a separate successful scan; it does not prove the earlier JSON scan completed.
The cross-implementation gate proves the native and WASM builds agree - native-produced ciphertext decrypts byte-for-byte in WASM from shared golden vectors:
wasm-pack test --node crates/wasmThe web build and its dependency audit run in CI (.github/workflows/ci.yml).
The server sends one anonymous ping per day (the first 10-20 minutes after boot) to
https://getsotto.co.uk/telemetry/v1/ping, so we can count active instances and see which
versions are in the wild. The response names the latest release, and the server logs a line when
it is running an outdated version. This is the entire payload - the sending code is
crates/server/src/telemetry.rs, and a unit test pins the
payload to exactly these four fields:
{ "instance_id": "0d0972a6-…", "version": "0.2.0", "os": "linux", "arch": "x86_64" }instance_id is a random UUID generated once and stored in your database - derived from nothing,
so it identifies no hardware, host, or account; deleting it makes the instance a fresh anonymous
counter. The ingest side stores no IP addresses and no derived location. There are no org, member,
or secret counts, and no usage events. The CLI, web client, and WASM never send anything.
Opt out with SOTTO_TELEMETRY=off (or the cross-tool
DO_NOT_TRACK=1) - when disabled the task is never started and
no request is ever made. SOTTO_TELEMETRY_URL redirects the ping (e.g. to aggregate a private
fleet), and records idle for 12 months are purged from the hosted census.
Sotto's model is zero-knowledge: plaintext secrets and usable decryption keys stay on client devices, and the server sees only ciphertext plus minimal metadata. This is implemented but not yet independently audited - see SECURITY.md for the model, the honest metadata exposure, how the (re-fetched, weaker) web surface is hardened, and how to verify signed releases. The full adversary model, guarantees, and explicit non-goals are published in THREAT-MODEL.md. Report vulnerabilities privately per SECURITY.md.
Sotto is Apache-2.0 and welcomes contributions. Start at CONTRIBUTING.md; good first issues are labelled, and questions that are not bugs belong in Discussions.
Please report vulnerabilities privately per SECURITY.md.
Licensed under the Apache License, Version 2.0 - all crates and the web client. You may not use this project except in compliance with the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.