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.
Sotto is one open-source product that you can run three ways. Whichever you choose, the server only ever stores ciphertext.
| Mode | What you get |
|---|---|
| Local | The CLI and an encrypted vault on your own machine. No account and no server. |
| Self-hosted | The same Apache-2.0 server on infrastructure you operate. It applies no plan limits and no hosted-service checks; see Self-hosting. |
| Sotto Cloud | The server Sotto operates at getsotto.co.uk, for when you would rather not run one. sotto login uses it by default. Plans and limits are on the pricing page, and service status is at status.getsotto.co.uk. |
You can start locally and add a server later: after sotto login, sotto push uploads the active
environment as it stands.
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, an organisation audit log, and an account reset for a lost Emergency Kit.
| 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, codec fuzzing, and Kani proofs for envelope-header and key-symbol parsing |
| CLI | Local secrets (init, set, get, ls, rm, import, export, run), environments (env ls/use/diff/copy), an interactive dashboard, login/push/pull sync, new-device setup, history and rollback, one-time links (share, shares ls/revoke); teams: org create/ls/invite/members/remove/plan/audit, grant, clone, rotate, expiring machine token create/ls/revoke (with SOTTO_TOKEN mode for CI); status, cloud status, lost-kit reset, themes, and shell completions |
| Server | OAuth login + sessions, account + snapshot sync (versioned writes, ETag), orgs + memberships + roles, per-member vault-key grants, transactional key rotation, expiring machine tokens, secret history, an organisation audit log, opt-in organisation deletion with a recovery window, account reset, and one-time links - ciphertext only. A Sotto Cloud deployment also enforces plan limits and can bill through Stripe |
| Web | A landing page and guides; the vault at /app, which unlocks in the browser to browse, search, reveal, and copy secrets, create one-time links, share an environment with a member, and rotate an environment key; a team panel with organisations, members, email invites, the plan, the audit log, and organisation deletion; the one-time link recipient page; and a Sotto Cloud account page at /cloud |
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).
The installers, the Homebrew formula and the GitHub Action all install published releases, while
this README follows the main branch, which can be ahead of the latest release. A command described
here may therefore not exist in your installed version yet; sotto --help lists the commands your
version supports.
See COMPATIBILITY.md for integration version requirements, platform claims, and the evidence currently available for them.
The Sotto tap provides another installation route for macOS. The formula packages reviewed CLI releases and requires macOS 15 or later. Apple Silicon is the primary platform; Intel is best-effort and Homebrew Tier 3, with Homebrew planning to end Intel support in or after September 2027. Check the tap's support and installation guidance and release-readiness record before using it. Avoid this preview for critical production secrets.
Install and upgrade the formula with:
brew install getsotto/tap/sottobrew update
brew upgrade getsotto/tap/sottoIf another sotto installation is on your PATH, inspect the resolved
executables and the Homebrew-managed binary before choosing which one to use:
which -a sotto
brew list --versions sotto
"$(brew --prefix)/bin/sotto" --versionTo remove only the Homebrew formula, run:
brew uninstall getsotto/tap/sottoUninstalling the formula leaves project files, local Sotto data, Keychain
entries, and other sotto installations in place. Confirm the selected binary
and preserve your Emergency Kit before removing or switching an existing
installation.
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 SOTTO_SERVER repository variable to your server's URL, including
https://getsotto.co.uk for Sotto Cloud: with SOTTO_TOKEN set, sotto run and sotto export
never fall back to a default server, so the token only goes to a server you named. 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 run -- npm start # inject the environment's secrets into any command
sotto get DATABASE_URL -c # copy a secret without printing it; clipboard clears after 45s when unchanged
sotto export --format dotenv --reveal # print a .env; refuses a terminal without --revealsotto init creates dev, staging and prod environments and selects dev. Export writes
plaintext, so it needs --reveal on a terminal, just like sotto get; --format also takes
shell and json. 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.
Everything so far stays on your machine. To sync another device, work with a team or run CI, log in to a server:
sotto login # Sotto Cloud by default; --server <url> for your own server
sotto push # upload the active environment's ciphertext
sotto share DATABASE_URL # a one-time link whose key never reaches the serversotto login opens a browser to sign in with GitHub. It uses Sotto Cloud at
getsotto.co.uk unless you pass --server <url> (see
Self-hosting to run your own), and later commands reuse the server you chose.
Either way the server only ever stores ciphertext - the web vault at /app on the same server
decrypts in your browser, with keys that never leave your devices.
To browse secrets interactively, see the dashboard guide.
Local coding agents can discover and run project-declared tasks through Sotto's local MCP server. Read the agent task guide for setup, approval, and the security boundary.
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.
If you lose the Emergency Kit, sotto reset gives your account fresh keys. It is a reset, not a
recovery: everything encrypted under the old keys becomes permanently unreadable, and organisation
admins must grant your shared environments again. If a device can still unlock the vault, export
what you need from it first.
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.
List environments, switch the default, compare two, or preview a copy between them:
sotto env ls # list environments; the active one is marked
sotto env use staging # switch the project's active environment
sotto env diff dev staging # compare keys; values stay hidden without --reveal
sotto env copy dev staging # preview additions and updates without writing
sotto env copy dev staging --confirm # apply the copy; never delete destination keysenv diff marks each key = (same value), ! (different values), < (only in the first
environment) or > (only in the second). Without --confirm, copying is a dry-run. A confirmed
copy adds and updates secrets but leaves keys that exist only in the destination untouched.
sotto import reads a dotenv file and sets each assignment in one environment, encrypting every
value locally. It writes to the active environment unless you name another with --env:
sotto import .env # into the active environment
sotto import --env staging .env.staging # into staging; the default environment is unchangedAn import merges into what is already there. Suppose staging already holds API_URL and REGION,
and .env.staging contains:
API_URL=https://staging.example.test
FEATURE_FLAG=onAfter importing it into staging:
API_URLtakes the value from the file, because an existing name is updated.FEATURE_FLAGis added, because a new name is created.REGIONis kept, because a name that is missing from the file is never removed.
Import writes to your local store only. After sotto login, upload the result as a separate step:
sotto push --env stagingThe dotenv file is only read: import does not change, consume or delete it, so protect or remove it
yourself if it holds real values. A file that does not parse changes nothing, but a failure while
writing does not undo the assignments already stored, so check sotto ls --env staging after an
interrupted import.
To get an earlier value back, see Restore an earlier secret version.
That history comes from the server, so it only helps for values you had already pushed. Run
sotto import --help for the quoting and literal-value rules instead of repeating them here.
sotto share encrypts one secret into a one-time link. The key sits after the # in the link,
which browsers never send to a server, so the server only ever holds ciphertext. Creating a link
needs sotto login; the recipient needs no account. Opening the link does not use a view, and
revealing the secret does.
sotto share DATABASE_URL # choose view and lifetime limits in an interactive terminal
sotto share DATABASE_URL --views 3 --expire 3600 # skip both limit prompts
sotto share DATABASE_URL --passphrase # also require a passphrase; send it separatelyIn an interactive terminal, each omitted share limit opens its own menu. The view menu offers
1, 2, 3, 5 or 10 views and a custom value; the lifetime menu offers no expiry, 1 hour, 1 day,
7 days, 30 days or a custom number of seconds. Custom values must be 1-100 views or
1-2592000 seconds. Supplying DATABASE_URL skips secret selection only: --views skips the view
menu and --expire skips the lifetime menu independently. Press Esc or Ctrl-C in any selection
menu to cancel without creating a share.
Non-interactive use requires an explicit secret name. When either limit flag is omitted there,
its existing default applies: one view and no expiry. To bypass all three selection menus in an
interactive terminal, supply the name and both limits, for example
sotto share DATABASE_URL --views 3 --expire 3600. Unlock and passphrase prompts may still apply.
A server can apply tighter limits to some accounts. When it refuses a request, the CLI prints the server's reason and creates no link.
Use sotto share --no-copy to disable interactive copying, or --copy to request it explicitly.
These flags control the clipboard only; --no-copy does not skip the share limit menus.
A link stops working when its views run out, when it expires, or when you revoke it. Revoking stops the server handing out the ciphertext; it does not erase it from the server:
sotto shares ls # your 100 most recent links: views used, expiry, revocation
sotto shares ls --json
sotto shares revoke <token> # the token is the part of the link between /s/ and #sotto shares ls never shows the ciphertext or the key in the link.
Teams share environments through an organisation. Each member receives their own sealed copy of an environment's key (a grant), and the server checks roles and grants on every request without being able to read anything.
sotto org create acme # prints the org id
sotto init --org <org-id> # an org-owned project
sotto push # create it on the server before sharing
sotto org invite <org-id> dev@example.com # invite an existing Sotto user; prints their user id
sotto grant <user-id> # share the active environment; prints their clone commandThe member runs the printed command in their own checkout:
sotto clone <project-id> <env-id> --org <org-id>To see who has access, remove someone, or rotate an environment's key yourself:
sotto org members <org-id> # members, roles and user ids
sotto org remove <org-id> <user-id> # remove a member and rotate every environment they held
sotto rotate # rotate the active environment's keyRemoving a member rotates the key of every environment they could access and revokes the machine tokens they created in the organisation, naming each so you can recreate it. Rotation protects what is written afterwards. Values the member has already seen stay exposed, so change them at their source.
sotto org plan <org-id> reads the configured server's current entitlement view for that
organisation. You must be logged in (sotto login); the local vault does not need to be unlocked.
It prints the assigned tier, effective tier, stored trial end, and either member and
organisation-project limits or limits: none (Team). These fields preserve compatibility with
existing organisation-tier records and deployments; they do not describe current Sotto Cloud
person-based pricing or a new Cloud trial offer.
Illustrative output from a legacy organisation-tier record:
tier: free
effective: free
trial: ends 2026-07-16T09:00:00Z
limits: 3 members, 1 org project(s)
A self-hosted server applies no plan limits, so it reports the effective tier as team, whatever
tier and trial end the organisation has stored:
tier: free
effective: team
trial: ends 2026-07-16T09:00:00Z
limits: none (Team)
These examples show stored compatibility fields, not current Cloud offers. Real deployments may show different stored tiers, trial dates, and limits.
Organisation owners and admins can inspect recent team activity with:
sotto org audit <org-id>
sotto org audit <org-id> --limit 20Use the ID printed by sotto org create <name> or listed by sotto org ls. Results are newest first;
the default is 100 events, and the server clamps --limit to 1-500. The command shows one line per
event as timestamp action actor, with optional target=..., env=..., and (detail) fields. It
is a bounded activity/metadata view, not a way to retrieve secret values, and it does not provide
pagination or a complete historical archive.
This command retains the legacy organisation Team-entitlement gate. Self-hosted deployments skip that plan gate, but the caller still needs the owner/admin role. If no events are available, the command prints no rows and exits successfully.
A machine token lets CI or a service read one environment without a password or Emergency Kit.
With SOTTO_TOKEN set, sotto run and sotto export fetch and decrypt that environment on each
run; set SOTTO_SERVER too, as the GitHub Actions example shows.
Machine 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.
Token commands use the logged-in user's access and the selected environment. In an organisation
environment, only an admin or owner can manage machine tokens. Listings contain active-token
metadata only: revoked and expired tokens are omitted, and the credential printed once by
sotto token create cannot be retrieved by listing. JSON includes token_id, name,
created_by, beneficiary_id, beneficiary_status, expires_at and expires_in_days;
expiry fields may be null. The token_id is the ID to revoke, not the token's name or the
SOTTO_TOKEN secret.
Use these commands to inspect, replace and revoke a token. The environment override applies to this command only:
sotto token ls
sotto token ls --json
sotto --env staging token ls --json
sotto token create --name ci
sotto token revoke <token-id>To replace a CI token, identify the old ID in the listing, create a replacement, update the CI
secret and verify CI uses it, then revoke the old ID with sotto token revoke <token-id>.
Revocation immediately stops that token from authenticating to the API; it does not rotate the
environment key. For an organisation environment, see the existing sotto rotate guidance
when key rotation is needed. Neither revocation nor rotation can erase plaintext that was already
copied.
From an initialised project, run sotto without a subcommand to browse its local secrets:
sottoA local identity must exist and be unlocked before the dashboard opens. Sotto prompts for the
master password when needed. The dashboard requires terminals on stdin, stdout, and stderr.
If one is missing, CI is active, or TERM=dumb, a bare invocation writes help to stderr and exits
with code 2.
Use sotto --help to request help explicitly.
Use the arrow keys or j and k to navigate. Press / to search, ? for help, and Tab to switch
environments. This switch lasts only for the dashboard session; sotto env use changes the
project's default. Press r to reveal or conceal the selected value, c to copy it to the
clipboard, and q to quit. Esc closes the current search or dialog; from the main view, it
clears an active search or quits.
Press t to browse themes with a live preview. Use the arrow keys to preview each theme, then
press Enter to save your choice or Esc to cancel. The theme precedence rules
explain how command and environment overrides affect the saved preference.
Run sotto status to see whether a local identity exists, whether the session is unlocked, and
which project and environment the current directory selects. It reads local state. It does not
check server connectivity or prove that every stored secret can be decrypted.
sotto status
sotto status --jsonUse sotto status --json in scripts. initialized reports whether a local identity exists, and
unlocked reports whether a master key is cached. project contains name and environment
from the nearest sotto.toml, or is null outside a configured project. A successful command can
report unlocked: false, so scripts must inspect that field if they need an unlocked session.
These JSON examples are illustrative. With a sotto.toml but no local identity:
{"initialized":false,"project":{"environment":"dev","name":"demo"},"unlocked":false}Without a sotto.toml or local identity:
{"initialized":false,"project":null,"unlocked":false}An absent sotto.toml means no project is selected. A malformed or unreadable file is an error.
To inspect the configured server and your account's Cloud eligibility, first log in:
sotto login
sotto cloud status
sotto cloud status --jsonThe command needs an active login but does not unlock the local vault or open a browser. It reports the configured server, the server's server_info, the account eligibility, and a billing_url only when the returned state says billing is available. The human-readable output also shows actions, next_actions, and any optional paid_through_epoch, recovery_until_epoch, or export_until_epoch values. A missing server_info or eligibility means that an older server does not expose that endpoint; it is not proof that access is allowed or denied.
The JSON form is useful for scripts. Its top-level shape is:
{
"server": "https://getsotto.co.uk",
"billing_url": "https://getsotto.co.uk/cloud",
"server_info": {
"deployment_mode": "cloud",
"entitlement_model": "organisation_tiers_v1"
},
"eligibility": {
"model": "person_eligibility_v1",
"state": "paid",
"deployment_mode": "cloud",
"account_initialized": true,
"billing_available": true,
"paid_through_epoch": 1798761600,
"recovery_until_epoch": null,
"export_until_epoch": null,
"actions": {
"setup": false,
"billing": false,
"export": false,
"revoke": true
},
"next_actions": ["revoke"],
"payer": "personal"
}
}The example is synthetic; actions and the optional dates come from the returned account state, and older servers can return null for either endpoint. sotto cloud status only reports status and may print a billing URL. It does not start a purchase or change eligibility. For local vault and environment diagnostics, use sotto status; that view is separate from the server and account information above.
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>.
Start in an existing configured project and keep the same environment selected throughout. You need a logged-in session and an unlocked local store. History comes from the server: values that have never been pushed are not available there for recovery.
sotto history DATABASE_URLChoose a readable, positive version number from the history. The example below uses 2;
replace it with an available version, without the displayed v prefix.
sotto rollback DATABASE_URL 2
sotto pushRollback restores the chosen value as a new local version. It does not erase or rewrite older history, and the server is not updated until the subsequent push.
For metadata-only JSON, use:
sotto history DATABASE_URL --jsonEach row has version, readable (whether the value could be decrypted) and size_bytes
(the plaintext byte count, or null for an unreadable version). No values are printed.
By contrast, --reveal prints secret values; it cannot be combined with --json.
If there is no server history, check the selected environment and whether it has been
pushed. Pushing now cannot recover older values that were never synced. For an unreadable
version, follow the CLI's suggestion to run sotto pull and try again; pulling does not
guarantee that a version can be decrypted with the available keys.
The CLI reads these variables:
SOTTO_SERVER: the server URL, overriding the one saved bysotto login. WithSOTTO_TOKEN, it is required unlesssotto loginsaved a server on that machine.SOTTO_TOKEN: a machine token.sotto runandsotto exportthen read the token's environment instead of the local vault.SOTTO_PASSWORD: the master password, for scripts that cannot answer a prompt. Prefer the hidden prompt where you can.SOTTO_SECRET_KEY: the Secret Key from your Emergency Kit, forsotto setupwithout a prompt.SOTTO_DATA_DIR: moves the local data directory, which holds the encrypted store, saved settings such as the server, and custom themes.SOTTO_THEMEandNO_COLOR: output styling; see Output themes.
One command brings up a complete deployment - 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).
SOTTO_DEPLOYMENT_MODE defaults to self_hosted. In that mode the server applies no plan limits
or other hosted-service restrictions, and it needs no Stripe account. The cloud mode is for the
service Sotto operates; read the runbook before you set it.
Point the CLI at your server once; later commands reuse it:
sotto login --server https://sotto.example.comCLI (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.
The browser client in web/ is a React and Vite app that runs the core through crates/wasm.
- Rustup with stable Rust 1.89 or newer
- The
clippyandrustfmtcomponents - The
wasm32-unknown-unknowntarget - For the web client: Node.js 22 (what CI uses) and
wasm-pack(CI pins v0.15.0) - For the server and its database tests: Postgres 16, which
docker compose up -dprovides - For the script checks: Python 3.11 or newer
The checked-in rust-toolchain.toml asks Rustup to install the required components and
target automatically.
Clone the repository, then build and test the workspace:
git clone https://github.com/getsotto/sotto.git
cd sotto
cargo build --workspace
cargo test --workspacecargo test --workspace needs no database, so it skips the server's integration tests and the
CLI's end-to-end test without failing. To run them, start the local Postgres and opt in with
SOTTO_RUN_DB_TESTS=1; they refuse a DATABASE_URL that points anywhere but this machine:
docker compose up -d
DATABASE_URL=postgres://sotto:sotto@localhost:5432/sotto SOTTO_RUN_DB_TESTS=1 \
cargo test -p sotto-server -p sotto-cliUse 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)
npm run typecheck # TypeScript checks; needs the WASM bindings a build generates
npm test # unit tests (Vitest)
npm run smoke # load the WASM core under Node and exercise itThe Playwright funnel suite (npm run e2e) needs a database and a production build first;
web/e2e/README.md explains the setup.
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 supply-chain CI job audits the whole lockfile with scripts/check-cargo-audit. The
lockfile audit guide covers the pinned cargo-audit version, the policy tests,
and the rules for the dormant lockfile exceptions.
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, its dependency audit, and the README translation check (scripts/check-readme-i18n)
also run in CI (.github/workflows/ci.yml). CONTRIBUTING.md links the guides for
fuzzing, Kani proofs, coverage, and the assurance checks.
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.
The scope and evidence prepared for an independent audit are in SECURITY-AUDIT-SCOPE.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.