tsafe — Explicit runtime authority for secrets
tsafe exec --contract <name> -- <command>— named authority contract in.tsafe.yml/.tsafe.json;tsafe audit/tsafe audit --explainfor receipts.
Docs: quick-start · golden paths (incl. coding agents §6) · export-and-exec · cli-reference · security
Most teams still do some version of this: keep passwords in .env, copy them from a password manager into a terminal, export a few secrets into the shell, forget what is still live, then hope nothing leaks into the wrong process, shell history, CI log, or git diff.
tsafe is better because it replaces that ambient mess with one explicit flow: secrets stay encrypted at rest, tsafe set prompts securely for values, and tsafe exec gives one process exactly the secrets it is authorized to receive (via a contract or, as a narrow escape hatch, flags — see the reference). Operator visibility: tsafe audit and tsafe audit --explain answer “what happened?”; flag details and JSON in docs/cli-reference.md.
30-second use
If you use .env today, the shortest mental model is this:
tsafe initcreates your encrypted local vaulttsafe setstores a secret in the vault and securely prompts for the valuetsafe exec -- ...runs one command with every non-alias secret in the vault injected into that process. Parent ambient credentials are stripped first; for true "least authority" use a contract or--keys/--ns(below).
Repeatable / team workflows: define a contract in .tsafe.yml and run tsafe exec --contract <name> -- ... (see export-and-exec and quick-start). Pre-flight: tsafe exec --plan / --dry-run for “what would run” before you execute (same reference and cli-reference).
That is the whole product: encrypted at rest, no plaintext .env file, no ambient shell leak, audit receipt written locally.
tsafe is a local-first secret manager for the problem behind most .env workflows: secrets leak through ambient shell state, copied plaintext files, and inherited parent credentials. It keeps secrets encrypted at rest, injects them only into the process you intend to run, and records what happened in an append-only audit log.
Authority model in one line: with an authority contract or --keys / --ns, a process receives exactly the secrets it is authorized to receive — no ambient access, no leaked parent credentials, no silent empty-string injections. Without one of those flags, tsafe exec injects every non-alias secret from the active vault into the child env (parent ambient credentials are still stripped; the dangerous-name guard is still on). See authority contracts for the recommended pattern.
Problems tsafe solves
- Ambient credential leaks: parent shell credentials like
GITHUB_TOKEN,AZURE_CLIENT_SECRET, orVAULT_TOKENreach child processes you never meant to authorize. - Plaintext secret sprawl:
.envfiles, copied values, and shell exports leave secrets on disk, in shell history, and in git accidents. - Missing authority review: scripts say "run the app with secrets" without declaring which process should receive which keys.
- No operator receipts: you cannot answer which secrets were accessed, by which profile, and when, on a developer machine.
If that is the problem, the smallest working loop is still three commands:
The bare
tsafe exec --form above injects every non-alias secret in the vault. That is the right onboarding shape (one secret, one command), but for repeatable scripts and CI use a named authority contract (tsafe exec --contract <name> -- …) so the authorisation set is declared, not implicit.
For repeatable workflows, use a named authority contract (tsafe exec --contract <name> -- ...); example shape and pre-flight are in docs/features/export-and-exec.md and docs/quick-start.md (avoid duplicating long YAML here so docs stay the source of truth).
Default Core Profile
This is the current default-on core product story and the safest mental model for operators and release notes:
tsafe exec --contract <name> -- <command>— recommended default for scripts and CI; contracts bundle profile, namespace, allowed/required keys, and trust posture into a single reusable, auditable surfacetsafe exec -- <any command>— ad-hoc injection; escape-hatch flags (--ns,--keys,--require,--mode) can narrow a contract but are secondary to named policy — see export-and-exec and cli-reference- MCP/editor wrappers are a thin consumer pattern over
tsafe exec(and sometimestsafe agent unlock), not a shippedtsafe mcpproduct surface tsafe agent unlock— local session unlock for repeated commands; setsTSAFE_AGENT_SOCKso inheritedtsafecalls skip password prompts for the TTL when the install channel actually includes the companiontsafe-agentbinary- Encrypted at rest (XChaCha20-Poly1305 + Argon2id) — no plaintext
.envfiles to leak or commit - Multi-profile vaults (dev/staging/prod) — profile is a security boundary, not just organization
- Append-only audit per profile:
tsafe audit,tsafe audit --explain, and JSON via--explain --json(see cli-reference);tsafe doctorfor health checks - Snapshots/restore, plain
.envimport, secret generation, TOTP/QR, and age-backed team-vault collaboration core - Interactive TUI, biometric/keyring unlock, and Azure Key Vault pull (
tsafe kv-pull) as the current default-on core provider path tsafe explain exec|export|namespaces|agent|contracts|pull|vault-recovery— in-terminal docs for authority concepts
Gated Non-Core Features
These surfaces are implemented or in active repo scope, but they should be treated as optional / gated non-core rather than part of the default product tier:
- Additional cloud/provider pulls beyond AKV: AWS Secrets Manager/SSM, GCP Secret Manager, HashiCorp Vault, and 1Password
- Multi-source pull orchestration, extended password-manager/browser CSV import, OTS / one-time sharing, and git-helper convenience surfaces
- Browser extension and native-host flows, SSH key-management surfaces, plugins, and OpenTelemetry
Some repo/docs areas also describe planned-only / post-core surfaces such as VS Code, system tray, and hosted/service-style collaboration depth. Those are not part of the default release story and should not be read as active core features.
Drop-in workflow integration: your Dockerfiles, Makefiles, scripts, and CI pipelines stay unchanged. You wrap the invocation with tsafe exec --contract … -- (preferred) or tsafe exec --.
Two-minute quickstart
Install: every supported path (shell installer, GitHub Releases assets, Homebrew tap / Scoop / WinGet / AUR / Nix, MSI or portable ZIP where published, cargo install only when the matching crates.io dependency graph for that release is actually live, local just install) is spelled out in docs/quick-start.md. Artifacts and naming use tsafe-<version>-default-core-full-<target> when a tagged GitHub release channel actually exists for that line; tracked manifests align with packaging/README.md — use those docs for what ships on a given tag; do not assume a package URL or crates.io install path until that release matches it.
For the current frozen core-only release boundary, a truthful cargo install tsafe-cli claim requires crates.io to already contain the exact compiled standalone tsafe graph for the named release shape you are claiming. In practice, default-core-cli and default-core-cli+agent require the standalone CLI graph (tsafe-cli, tsafe-core, tsafe-azure), while default-core-cli+tui and default-core-full also require tsafe-tui. If that standalone graph is not live yet for the release line, use the tagged release archives/packages or a local workspace install instead of advertising cargo install.
A standalone tsafe install is enough for normal exec usage, but agent-backed local-session reuse still requires a separately shipped tsafe-agent install, and browser/native-host workflows require a separately shipped non-core tsafe-nativehost install. cargo install tsafe-cli alone should not be described as covering those companion runtimes or any broader provider stack.
# Recommended once you add a contract to .tsafe.yml:
# tsafe exec --contract <name> -- docker compose up
That's it. The command you actually care about gets the secrets it needs in its environment. No
.envfiles, no export juggling. The vault is encrypted at rest and access is audit-logged.
tsafe set DATABASE_URL prompts securely for the secret value, so you do not need to put the value on the same command line.
Contracts are the recommended default for scripts, CI, and team-shared commands: an authority ceiling you can narrow per invocation but never widen. Full examples, edge cases, and pre-flight (--plan / --dry-run) live in docs/features/export-and-exec.md and docs/quick-start.md.
Not sure whether to use exec, get, or export? See docs/features/exec-vs-get.md.
Where files live
tsafe now splits durable vault data, config, and local receipts across the platform directories exposed by ProjectDirs. On Linux that means XDG-style defaults instead of one shared app-data root.
| Artifact | Default location | Notes |
|---|---|---|
| Vault files | data dir /vaults/<profile>.vault |
Encrypted vault per profile |
| Audit receipts | state dir /audit/<profile>.audit.jsonl |
Append-only local receipts for vault access and exec activity |
| Global config | config dir /config.json |
Default profile and persisted exec settings |
| TUI debug log | state dir /tui-debug.log |
Written only when TSAFE_TUI_DEBUG=1 |
Examples:
- macOS: data
~/Library/Application Support/tsafe/..., config~/Library/Application Support/tsafe/config.json - Linux: data
~/.local/share/tsafe/..., config~/.config/tsafe/config.json, state~/.local/state/tsafe/... - Windows: platform data/config/state defaults resolved by
ProjectDirsfortsafe
If TSAFE_VAULT_DIR is set, that directory becomes the vault root, its parent holds config.json, and runtime receipts/state move under <parent>/state/.
See docs/features/storage-paths.md for the full path matrix and override behavior.
Table of Contents
- Problems tsafe solves
- Two-minute quickstart · Full install guide
- Where files live
- Architecture
- Repository Structure
- Security
- Developer Quick Start
- CLI Reference
- Git Integration
- Workflows and Automation
- Profiles
- Browser Extension
- Terminal UI (TUI)
- External Integrations
- Infrastructure
- Pipelines
- Local Development
- MSI Distribution
- Documentation
Trust model
tsafe's core design principle is declared authority over ambient access. Every secret injection is intentional, scoped, and auditable — not a side effect of a shell variable that happens to be set.
Default scope (no contract /
--keys/--ns):tsafe execinjects every non-alias secret in the active vault into the child env. Parent ambient credentials are still stripped via the strip list and dangerous-name vault entries (LD_PRELOAD,NODE_OPTIONS, …) still abort, but the vault scope is the whole vault until you adopt one of the levers below. Use a contract,--keys, or--nsto narrow.
| Concept | What it does |
|---|---|
| Profile | A named, independently encrypted vault. A process can only receive secrets from the profile it is explicitly given (--profile or TSAFE_PROFILE). Profiles are a security boundary, not just an organisational one. |
Authority contract (--contract) |
A named policy loaded from the nearest .tsafe.yml / .tsafe.json that bundles profile, namespace, allowed secrets, required secrets, allowed targets, and trust posture into a single reusable, auditable surface. Flags can narrow a contract but cannot widen it. Prefer contracts in scripts and CI over ad-hoc flag re-composition. |
Namespace (--ns) |
Scopes injection to one sub-tree of keys. --ns api means the child process sees only api/* keys, with the prefix stripped. Everything else in the vault is withheld. |
Key selection (--keys) |
Narrows injection to a named subset. Fails fast if any selected key is missing — no silent empty-string injection. |
Require (--require) |
Asserts minimum key presence before the child starts. Useful in CI to catch misconfigured vaults before a deployment begins. |
Trust mode (--mode) |
hardened — minimal inherited env, redacted child output, dangerous-name deny. standard — compatibility preset. custom — persisted per-profile settings. |
| Strip list | tsafe exec removes parent process credentials (TSAFE_PASSWORD, AZURE_CLIENT_SECRET, VAULT_TOKEN, GITHUB_TOKEN, etc.) from the child environment before injecting vault secrets. Parent ambient credentials cannot reach the child. |
| Audit log | Every vault open, secret read, and exec invocation is appended to a per-profile JSONL log. Operators use tsafe audit and tsafe audit --explain for readable receipts; JSON consumers use --explain --json. Forwarding to a SIEM: tsafe audit-export. Details: docs/cli-reference.md. |
| Session scope | The agent holds a session token with a configurable idle TTL. Vault access requires a live session — there is no "always-on" ambient access mode. |
For the full trust model and threat analysis, see docs/features/security.md and docs/features/export-and-exec.md.
Architecture
The repo is intentionally split into one compiled standalone tsafe graph plus
separate companion and gated non-core surfaces:
- The compiled standalone
tsafeinstall path is thetsafe-cligraph overtsafe-core. In the frozen core-only line that graph includestsafe-azure, and it includestsafe-tuionly for the named standalone shapes that ship the TUI. tsafe-agentis a separate companion binary for local-session reuse (tsafe agent unlock). A standalonetsafeinstall does not imply it.tsafe-nativehostis a separate non-core companion for browser/native-host workflows. It stays outside the core-only baseline unless a broader release channel explicitly ships it.- Additional provider surfaces such as AWS and GCP remain gated
non-core additions. They are not part of the baseline standalone
tsafeclaim, whether they ship as separate crates or as feature-gated inlined modules.
compiled standalone tsafe graph
tsafe-cli
-> tsafe-core
-> tsafe-azure (core provider path in the frozen core-only line)
-> tsafe-tui (only when the named standalone shape includes TUI)
separate companion binary
tsafe-agent (required for agent-backed session reuse; not implied by tsafe alone)
separate non-core companion
tsafe-nativehost (browser/native-host workflow only when that channel ships it)
gated non-core provider surfaces
AWS / GCP / ... (only when a broader channel explicitly enables them)
All vault I/O still flows through tsafe-core, but install truth is
shape-specific: one cargo install tsafe-cli claim covers only the compiled
standalone tsafe graph for the named release shape, not the separate
companion binaries or gated non-core provider surfaces.
Repository Structure
tsafe/
├── Cargo.toml # Rust workspace root (6 crates)
├── justfile # Common local tasks (build, test, docs, release)
├── rust-toolchain.toml # Pinned stable toolchain
├── crates/
│ ├── tsafe-core/ # Vault engine, crypto, audit, snapshots, env, TOTP, gen
│ ├── tsafe-cli/ # tsafe binary (clap CLI, 30+ subcommands)
│ ├── tsafe-tui/ # Interactive terminal UI (ratatui)
│ ├── tsafe-agent/ # Background unlock/session agent
│ ├── tsafe-azure/ # Azure Key Vault integration
│ └── tsafe-nativehost/ # Browser extension native messaging host
├── contracts/
│ ├── events/ # Event and CloudEvents schemas
│ ├── integrations/ # External integration contracts
│ ├── snap/ # One-time secret (OTS) HTTP payload contracts
│ ├── vault/ # Vault file JSON schema (v1) + example
│ └── errors/ # Error envelope schema + reason codes
├── docs/ # Guides, feature deep-dives, and generated man pages
│ ├── README.md # Documentation hub
│ ├── man/ # Generated Unix man pages (`tsafe.1`, etc.)
│ └── features/ # Detailed feature guides
├── extension/ # TypeScript browser extension (Manifest V3)
├── packaging/ # Tracked package-manager manifests (Homebrew, Scoop, WinGet, AUR, Nix)
├── scripts/
│ ├── __main__.py # Cross-platform task entry point (`python -m scripts ...`)
│ ├── build/ # Release, extension, and MSI packaging helpers
│ ├── setup/ # Local tooling/bootstrap helpers
│ └── proget/ # ProGet publish helpers
├── terraform/ # Azure Key Vault + Log Analytics IaC
├── .pipelines/ # Azure DevOps: quality, build-and-publish, deploy
├── docker/ # Multi-stage build + dev container
├── docker-compose.yml # Dev environment with cargo-watch
├── .githooks/ # Pre-commit (fmt) + pre-push (clippy, test, TF)
├── tests/ # Integration and contract validation
└── assets/ # Icons (ICO/PNG), MSI license (RTF)
Security
| Property | Implementation |
|---|---|
| Key derivation | Argon2id — m=64 MiB, t=3, p=4 (OWASP-aligned; configurable 8–128 MiB) |
| Cipher | XChaCha20-Poly1305 — 256-bit key, 192-bit random nonce per secret |
| Key verification | Encrypted known-plaintext challenge — wrong password fails fast |
| Memory safety | zeroize::ZeroizeOnDrop on all key material; manual wipe in Vault::drop |
| Vault atomicity | Write to .vault.tmp then atomic rename — no partial writes on crash |
| Audit trail | Append-only JSONL receipt per profile (under the state audit/ directory) |
| Concurrent access | Advisory file lock (.vault.lock); stale lock timeout at 10 min |
| Auto-healing | Vault::open() restores from latest snapshot if file is missing or corrupt |
| Snapshot retention | Encrypted copies under snapshots/<profile>/ in the vault data dir; keeps last 10 |
| Browser isolation | Extension holds only a short-lived session token; all vault I/O stays in native host |
| HTTPS enforcement | AKV URLs and OTS (TSAFE_OTS_BASE_URL) must use https:// |
For tracked security findings and their status, see docs/security/findings.md.
Installation
Supported formats and channels vary by release (e.g. Windows MSI / portable ZIP; macOS and Linux .tar.gz on GitHub Releases; optional package managers; cargo install only once the relevant crates are actually published to crates.io for that release line). For the frozen core-only Cargo path, crates.io must contain the exact compiled standalone tsafe graph for the release shape you are claiming: default-core-cli and default-core-cli+agent need tsafe-cli, tsafe-core, and tsafe-azure, while default-core-cli+tui and default-core-full additionally need tsafe-tui. Any +agent claim still needs separate tsafe-agent publish/install truth, and browser/nativehost or broader provider claims need their own non-core channel proof. Canonical install matrix — including shaped tsafe-<version>-default-core-full-<target> release archives when a tagged GitHub release channel actually exists, TSAFE_INSTALL_* overrides, private repos, just install, and companion binaries such as tsafe-agent / tsafe-nativehost when a release claims those workflows — is docs/quick-start.md. Tracked packaging manifests and tap/bootstrap notes: packaging/README.md. Use whatever install method matches the artifacts you actually publish for your tag.
Developer Quick Start
1. Install tooling (once, for development)
# On Windows, follow any printed install-tooling.ps1 guidance for WiX/MSVC.
2. Install git hooks (once)
3. Build and run
# Unix: ./target/release/tsafe --help
# Windows: .\target\release\tsafe.exe --help
# Repo-local manual: man -l docs/man/tsafe.1
Or with Podman/Docker (no local Rust required):
4. Create your first vault
tsafe init # create 'default' profile
tsafe set DB_URL "postgres://localhost/mydb"
tsafe set API_KEY --tag env=dev # prompted securely for value
tsafe list
tsafe exec --dry-run # preview env var names only (no subprocess)
tsafe exec -- dotnet run # inject secrets as env vars
5. Migrate from .env files
tsafe import --from .env.local --overwrite
Broader password-manager/browser CSV imports are a gated lane that require a
build or release channel with pm-import-extended:
tsafe import --from bitwarden --file ~/Downloads/export.csv
tsafe import --from chrome --file ~/Downloads/chrome_passwords.csv
For a full walkthrough, see docs/getting-started.md. For the full docs index, see docs/index.md. For Unix manual generation and install notes, see docs/man/README.md.
CLI Reference
tsafe [--profile <name>] <command>
The --profile flag selects a named vault (default: default). Override with the TSAFE_PROFILE env var.
Core Commands
| Command | Description |
|---|---|
init |
Create a new encrypted vault for the current profile; on a TTY, optionally set up OS quick unlock (Touch ID / Hello / PIN) or defer with biometric enable later |
config show, config set-backup-vault … |
Global config.json: optionally copy each new vault's master password into a hub vault at profile-passwords/<profile> |
set <KEY> [VALUE] |
Store a secret (prompts securely if value omitted). --tag KEY=VALUE to attach metadata, --overwrite to skip confirmation |
get <KEY> |
Print a secret. --copy copies to clipboard and auto-clears after 30 s |
delete <KEY> |
Remove a secret (snapshot taken automatically) |
list |
List all secret keys. --tag env=prod to filter |
export |
Print secrets to stdout. --format env|dotenv|powershell|json|github-actions. --tag to filter |
exec -- <cmd> |
Run a command with secrets as env vars. Propagates exit code and Ctrl-C |
import |
Migrate from .env files by default. Broader CSV exports such as bitwarden, 1password, lastpass, chrome, edge, or firefox require a build/channel with pm-import-extended. --overwrite to replace existing |
rotate |
Re-encrypt all secrets with a new master password |
Generation and TOTP
| Command | Description |
|---|---|
gen <KEY> |
Generate a CSPRNG random secret. --length 64 --charset hex --print |
totp add <KEY> <SECRET> |
Store a TOTP seed (base32 or otpauth:// URI) |
totp get <KEY> |
Print current 6-digit TOTP code + seconds remaining |
qr <KEY> |
Render a secret as a QR code in the terminal |
Profiles and Organization
| Command | Description |
|---|---|
profile list |
List all vault profiles |
profile delete <name> |
Delete a profile vault. --force to skip confirmation |
diff |
Show key-level changes since last snapshot |
compare <profile> |
Highlight missing/mismatched keys between two profiles |
pin <KEY> / unpin <KEY> |
Pin secrets to the top of lists |
alias <TARGET> <ALIAS> |
Create an alias (tsafe get ALIAS resolves to TARGET). --list to view all |
External Integrations
| Command | Description |
|---|---|
kv-pull |
Pull secrets from Azure Key Vault. --prefix MYAPP_ --overwrite |
vault-pull |
Pull from HashiCorp Vault KV v2. --addr, --token, --mount, --prefix |
op-pull <ITEM> |
Pull fields from a 1Password item via the op CLI |
share-once <KEY> |
Share via a configurable OTS one-time link (snap alias). --ttl string is server-specific |
Audit and Diagnostics
| Command | Description |
|---|---|
audit |
Show recent audit log entries. --limit 100 |
audit-export |
Export audit log. --format json|splunk --output audit.jsonl |
snapshot list |
List local vault snapshots |
snapshot restore |
Restore the most recent snapshot |
doctor |
Diagnose vault health, env vars, expired secrets, orphaned snapshots |
Utilities
| Command | Description |
|---|---|
git <subcommand> [args] |
Gated non-core git-helper lane: run git with ADO_PAT injected as http.extraHeader |
hook-install |
Install a secret-scanning git pre-commit hook. Auto-detects nearest .git |
completions <SHELL> |
Print shell completions (powershell, bash, zsh, fish) |
browser-profile add|list|remove |
Map browser domains to vault profiles |
browser-native-host register|unregister |
Register or remove the browser native messaging host bridge |
ui |
Launch the interactive terminal UI |
For the full reference with examples, see docs/cli-reference.md.
Git Integration
tsafe git is an implemented gated non-core helper lane. When the current
binary and release channel actually include it, it wraps any git command and
injects vault credentials via http.extraHeader — no URL mutation, no temp
files, no $env: cleanup:
# Push to Azure DevOps with ADO_PAT from vault
tsafe -p main git push ado main
# Or use the push script
python scripts/push.py
The agent + master-password bridge means zero prompts in your daily workflow
for commands that are actually present in your compiled build. For tsafe git,
that still requires the gated git-helper lane:
tsafe agent unlock --ttl 8h # once; copy the printed TSAFE_AGENT_SOCK line into your shell
tsafe exec -- cargo test # any time
See docs/features/git-integration.md for the full reference.
Workflows and Automation
tsafe is designed to replace every pattern that involves setting $env:SECRET = ... manually:
For repeated local automation, the feature you want is tsafe agent unlock, not tsafe unlock. tsafe unlock only removes a stale .vault.lock file after a crash. tsafe agent unlock starts the background session agent, prints a TSAFE_AGENT_SOCK export line, and every later tsafe command that inherits that env var can open the vault without re-prompting for the password.
tsafe agent unlock --ttl 8h
# copy the printed line into the parent shell:
# $env:TSAFE_AGENT_SOCK = "..."
tsafe exec -- cargo test
tsafe exec -- terraform apply
# If the gated git-helper lane is compiled in, this can use the same session:
# tsafe git push ado main
That is the right pattern for local scripts, long-lived dev shells, and parent processes that spawn several tsafe commands. Headless CI should still set TSAFE_PASSWORD from the platform secret store instead of relying on the agent socket.
# Run anything with secrets injected
tsafe exec -- dotnet run
tsafe exec -- cargo test
tsafe exec -- docker-compose up
tsafe exec -- terraform apply
# Replace complex credential setup scripts
tsafe exec -- powershell -File ./deploy.ps1
See docs/features/workflows.md for patterns covering:
- Session-based unlock (one password, all day)
- Replacing
$env:patterns - CI/CD pipelines with minimal secret surface
- Multi-profile automation
Profiles
Each profile is an independent encrypted vault file under the platform data vaults/ directory (~/.local/share/tsafe/ on typical Linux installs).
tsafe --profile dev set DB_URL "postgres://dev-host/mydb"
tsafe --profile prod set DB_URL "postgres://prod-host/mydb"
tsafe compare prod # show what dev has that prod doesn't
Override the vault directory for CI: $env:TSAFE_VAULT_DIR = "C:\ci\vaults"
Observability
tsafe uses the tracing crate for structured logging. Controlled via the TSAFE_LOG environment variable:
# Disabled by default (zero overhead when unset)
# Enable debug logging to stderr
TSAFE_LOG=debug
# Info-level only
TSAFE_LOG=info
# Per-target granularity (tracing env-filter syntax)
TSAFE_LOG=tsafe_core=debug,tsafe_cli=info
# JSON lines on stderr (aggregators / CI). Uses span-close records for `#[instrument]`d work (vault, KDF, …).
TSAFE_LOG=info TSAFE_LOG_FORMAT=json
Key operations are instrumented with timing spans: vault open/save, Argon2id KDF, XChaCha20 encrypt/decrypt, secret set/get, rotation. Auto-heal events (snapshot restore on corruption) are logged at warn level.
Browser Extension
tsafe includes a browser extension (Edge, Chrome, Firefox) that autofills credentials without any cloud relay.
This lane is implemented but gated non-core in the current product model. It is not part of the default-on core story, and it depends on the separate native host/runtime topology described in docs/features/browser-extension.md.
Browser popup / content script
↕ Native Messaging (stdio JSON-RPC)
tsafe-nativehost (registered by `tsafe browser-native-host ...`)
↕
`<vaults-dir>/<profile>.vault`
Features (v1 — see docs/features/browser-extension.md for full scope and out-of-v1 items):
- Autofill on
<input type="password">detection (user-initiated) - Domain-to-profile mapping (
tsafe browser-profile add github.com) - HTTPS-only content script — autofill UI does not run on
http:pages; advanced lookalike / homoglyph guards per roadmap, not a v1 bar - Context menu: right-click any password field to fill
- Popup UI: search, copy, reveal, lock
Build:
cd extension && npm install && npm run build
# or
python scripts/build/build_extension.py
Terminal UI (TUI)
Launch with tsafe ui or tsafe --profile prod ui.
Key bindings:
| Key | Action |
|---|---|
| Arrow keys | Navigate secret list |
| Enter | Unlock vault / confirm action |
s |
Set (add/edit) a secret |
| Delete | Remove selected secret |
r |
Rotate master password |
/ |
Search filter |
? |
Help overlay |
| Tab | Switch profiles |
| Ctrl-C | Quit |
Screens: Login, Dashboard, Edit Modal, Audit Log, Snapshot Restore, Help.
External Integrations
Within the current product boundary, Azure Key Vault is the only default-on core provider path here. The other provider bridges below remain implemented but gated non-core and should not be read as part of the default release tier.
Azure Key Vault
$env:TSAFE_AKV_URL = "https://myvault.vault.azure.net"
$env:AZURE_TENANT_ID = "..."
$env:AZURE_CLIENT_ID = "..."
$env:AZURE_CLIENT_SECRET = "..."
tsafe kv-pull --prefix MYAPP_ --overwrite
Falls back to IMDS managed identity when service principal credentials are not set (for Azure VMs / ACI).
HashiCorp Vault
$env:TSAFE_HCP_URL = "http://vault:8200"
$env:VAULT_TOKEN = "hvs.xxx"
tsafe vault-pull --mount secret --prefix myapp/
1Password
tsafe op-pull "Database Credentials" --op-vault Personal
Requires the op CLI installed and authenticated.
Infrastructure
Optional Azure infrastructure for audit log ingestion and cloud-backed secrets.
# Plan
cd terraform
terraform init -backend-config=../envVars/backend-config.azurerm.hcl
terraform plan -var-file=../envVars/<your-tsafe-env>.tfvars
# Apply
terraform apply -var-file=../envVars/<your-tsafe-env>.tfvars
Resources provisioned:
- Azure Resource Group
- Azure Key Vault (RBAC, purge protection, soft delete)
- Log Analytics Workspace (90-day retention)
- Diagnostic settings routing KV audit events to LAW
Pipelines
| Pipeline | Trigger | Purpose |
|---|---|---|
.pipelines/quality.yaml |
PR + main push | fmt, clippy, test, contract validation, TF validate (Ubuntu) |
.pipelines/build-and-publish.yaml |
Tag push (tsafe-v*) / manual |
Multi-platform build (Windows/macOS/Linux tickboxes), MSI, ProGet |
.pipelines/deploy-infrastructure.yaml |
Manual | Terraform Plan / Apply / Destroy |
.github/workflows/release.yml |
Tag push (tsafe-v*) / manual |
GitHub Actions: multi-platform build + GitHub Release |
Local Development
# Run all quality checks (same as CI)
just ci
# Regenerate Unix man pages from the live clap command tree
just manpages
# Watch and re-run tests on file change (Podman)
podman compose up
# Build a portable release archive for the current platform
python scripts/build/build_release.py --shape default-core-full
# Build browser extension packages
python scripts/build/build_extension.py
# Run secret scanner
python scripts/secret_scan.py
Windows MSI packaging and ProGet publish flows are wired through the build pipelines and helper scripts under scripts/build/ and scripts/proget/.
Git: fork and upstream
If you fork tsafe, add the canonical repo as upstream so you can sync main:
Use SSH URLs if you prefer. Do not embed personal access tokens in remote URLs (rotate any token that was ever committed or copied into a URL). origin is your fork; upstream is the public source you pull from.
Homebrew tap packaging (separate public repo) is documented in packaging/README.md; bootstrap with just homebrew-tap-bootstrap.
Dev container
VS Code / GitHub Codespaces: open the repo in a container using .devcontainer/devcontainer.json (Rust bookworm image, rustfmt + clippy installed on create).
Docs vs code
When you change CLI behavior, env vars, or extension UX, keep README.md, docs/, and .env.example accurate. Cursor picks up .cursor/rules/docs-truth.mdc for touched paths.
Crate versions and pre-push
Workspace crates (tsafe-agent, tsafe-azure, tsafe-aws, tsafe-cli, tsafe-core, tsafe-gcp, tsafe-nativehost, tsafe-tui) each have their own [package].version in crates/<name>/Cargo.toml. Release tarball and extension versioning follow tsafe-cli (see scripts/_util.py). Do not read that list as a claim that those crates are already live on crates.io for the current release line.
Crates.io publish/install truth follows the manifest graph, not just the top-level product tag. In the frozen core-only model, cargo install tsafe-cli is truthful only when the exact compiled standalone tsafe graph for the claimed release shape is live on crates.io: default-core-cli and default-core-cli+agent require tsafe-cli, tsafe-core, and tsafe-azure, while default-core-cli+tui and default-core-full additionally require tsafe-tui. That is enough for the compiled tsafe binary claim only. tsafe-agent and tsafe-nativehost remain separate published/runtime companions rather than transitive installs from tsafe-cli, and broader provider stacks remain outside that Cargo claim.
- Bump helper:
python3 scripts/version_set.py 1.0.2(all crates to a target),python3 scripts/version_set.py --crate tsafe-core --bump patch, orpython3 -m scripts version …. - Version policy: see
docs/release-versioning.mdfor when to use patch vs minor (0.6.0,0.7.0, etc.) while the repo is still pre-1.0. - Pre-push: With
git config core.hooksPath .githooks(set byjust setup), pushes tomainrequire a version bump on every crate whose files changed versus the remote tip (or all six if the workspace rootCargo.tomlchanged). Override refs withTSAFE_VERSION_HOOK_REFS(comma-separated,fnmatchallowed, e.g.refs/heads/main,refs/heads/release/*).
Docker
# Dev container (cargo-watch, full toolchain)
podman compose up --build
# Production image (~50 MB, non-root)
podman build -f docker/Dockerfile.build -t tsafe .
podman run --rm tsafe --help
MSI Distribution
cargo-wix currently generates the broader gated-broader-dev MSI artifact
tsafe-<version>-x86_64.msi that:
- Installs
tsafe.exeandtsafe-nativehost.exeunderC:\Program Files\tsafe\(seecrates/tsafe-cli/wix/main.wxs) - Ships
tsafe-nativehost.exe, but browser/native-host registration is still a manual post-install step: runtsafe browser-native-host register --extension-id <chromium-id>after installing the matching Chromium-family browser extension on Windows - Preserves an existing browser/native-host registration across in-place MSI upgrades; initial registration is still a manual post-install step
- Adds the install directory to
PATH - Creates Start Menu and desktop shortcuts
- Registers an uninstaller in Add/Remove Programs
- Upgrades in-place using a stable GUID
That Windows MSI path is a channel-specific broader install shape, not proof
that every default-core tag or cargo install tsafe-cli path includes
browser/nativehost coverage. The current default-core tagged release channel
remains portable ZIP-only.
Windows portable ZIP names are shape-specific. When a tagged default-core
release channel exists, it uses tsafe-<version>-default-core-full-x86_64-pc-windows-msvc.zip;
local Windows packaging helpers may also emit tsafe-portable-<shape>-<target>.zip
for copy-anywhere deployment without elevation.
The MSI is published to a ProGet Universal feed (e.g. tsafe) via the build-and-publish pipeline.
Contracts
tsafe follows a contracts-first design. Schemas are defined before implementation.
- Vault format — contracts/vault/vault.schema.json (
tsafe/vault/v1) - Error envelope — contracts/errors/error-envelope.schema.json
- Reason codes — contracts/errors/reason-codes.json (13 machine-readable codes)
Principles
- Contracts first — vault schema and error envelope defined before implementation
- TDD — every module has
#[cfg(test)]tests; hooks enforcecargo testbefore push - DRY — shared logic in
tsafe-core; CLI is a thin dispatch layer - Idempotent —
tsafe setis safely re-runnable; vault rotation is atomic - Shift left — pre-commit (fmt), pre-push (clippy + test + TF validate), CI quality gate
- Right tool — Rust core (memory safety,
zeroize, static binary), Python/Pwsh automation, Terraform IaC - Extensible —
tsafe-coreis a standalone crate; new workspace members bolt on cleanly
Documentation
Feature index and docs hub: docs/index.md.
Guides
| Document | Description |
|---|---|
| Feature Index | Hub for guides, feature docs, and the fastest path to deeper product documentation |
| Getting Started | Prerequisites, install, first vault, CI integration |
| CLI Reference | Full command reference with flags, env vars, and examples |
| Man Pages | How to regenerate, install, and use tsafe.1 and subcommand manuals |
| Security Findings | Tracked security review findings and their status |
| Internal Pitch | Why tsafe + AKV, the PowerShell memory problem, addressing security team concerns |
| Competitive Analysis | Feature and security comparison vs 12 alternatives, gap analysis |
| Feature Maturity | Every feature: current state, MVP path, AAA path |
| Roadmap | Feature tracking — shipped and planned |
Feature Deep-Dives
| Document | Description |
|---|---|
| Vault Basics | Init, set, get, delete, list — with key naming rules and password management |
| Export and Exec | Output formats (env, dotenv, powershell, json, github-actions) and secret injection |
| Import | Migrate from .env files by default; broader Bitwarden, 1Password, LastPass, and browser CSV imports require pm-import-extended |
| Profiles | Multi-profile workflows, compare, and diff |
| Secret Generation | CSPRNG random secrets with configurable length and charset |
| TOTP | Time-based one-time passwords — use tsafe as an authenticator |
| Audit Logging | Append-only JSONL audit trail with Splunk/JSON export |
| Snapshots | Automatic backups, manual restore, and auto-healing |
| Cloud Integrations | Azure Key Vault core path plus gated HashiCorp Vault / 1Password / OTS surfaces |
| Browser Extension | Gated browser/native-host flows — v1 scope and roadmap |
| Terminal UI | Interactive dashboard with keyboard shortcuts and screen reference |
| Organization | Tags, pins, and aliases for managing secrets at scale |
| Developer Tools | Doctor, secret scanning hooks, shell completions, QR codes, SSH keys, rotation policies |
| Security | Encryption model, memory safety, atomic writes, and threat model |
| Team Vaults | Multi-recipient age encryption — shared vaults for teams |
| Biometric Unlock | Touch ID, Windows Hello, Linux Secret Service — password-free vault access |
About
tsafe is part of the algol.cc ecosystem — small, composable tools for explicit authority in software systems.