The tsafe exec post covered the surface: wrap your command, secrets get injected, nothing in history. But exec is built on top of a trust model with more structure than that. This post covers the model itself — profiles, namespaces, modes, the strip list, and how they compose.
Profiles are security boundaries
Most developers think of profiles as organizational grouping — dev, staging, prod. In tsafe, a profile is a security boundary. A process can only receive secrets from the profile it is explicitly given. There is no way to access another profile's secrets without explicitly specifying that profile.
| Property | What it means in practice |
|---|---|
| Independently encrypted | Each profile has its own vault file, its own Argon2id-derived key. Compromising one profile's password does not expose others. |
| Explicit grant only | A process only receives secrets from the profile specified via --profile or TSAFE_PROFILE. No ambient cross-profile access. |
| CI isolation | A CI job with TSAFE_PROFILE=ci can only see the ci profile's secrets, even if the vault file contains dev and prod profiles too. |
| Compare without decrypting | tsafe compare dev prod shows key-name differences across profiles without decrypting any values. |
Namespaces scope injection
Within a profile, namespaces are key prefixes — api/KEY, db/KEY, build/KEY. The --ns flag scopes injection to one sub-tree and strips the prefix from the env var names.
Everything outside the declared namespace is withheld. The child process cannot see db/DATABASE_URL by trying to read DATABASE_URL — it isn't there. This is the same model as CellOS secretRefs: declare what you need, get exactly that.
| Flag | Behaviour | On missing key |
|---|---|---|
| (none) | Inject all secrets from the active profile | — |
| --ns <name> | Inject only keys under <name>/; prefix stripped | Silent — namespace may be empty |
| --keys k1,k2 | Inject exactly these named keys | Fails fast — aborts before exec |
| --require k1,k2 | Assert these keys exist before exec; does not restrict injection | Fails fast — aborts before exec |
| --mode hardened | Minimal inherited env, redacted child output, dangerous-name deny | Rejects keys matching credential name patterns |
The strip list
Before injecting vault secrets into the child environment, tsafe exec explicitly removes a set of parent process credentials. The child cannot inherit your pipeline credentials by accident.
- — the vault master password
- , — service principal credentials
- — HashiCorp Vault token
- — GitHub Actions identity token
The strip happens before injection. Your application code cannot access the credentials that tsafe itself used to open the vault, even if they were in the parent environment.
Trust modes
| Mode | What changes | Use case |
|---|---|---|
| standard | Default. Inherits most parent env; no output redaction. | Development, general CI use |
| hardened | Minimal inherited env; child output redacted before logging; dangerous key names denied (keys with names like *_SECRET, *_PASSWORD blocked from injection). | Hostile environments, compliance-sensitive pipelines, agent-driven automation |
| custom | Persisted per-profile settings, configured via tsafe config. | Team-standard posture without per-command flags |
Session scope and the agent
Vault access requires a live session — there is no always-on ambient access mode. The agent holds a session token with a configurable idle TTL.
The TTL is idle-based. Activity resets the timer. When the TTL expires, the session ends and the next vault operation requires re-authentication. The agent holds the vault password in memory for the TTL — it does not persist to disk.
The cryptographic layer
| Property | Implementation | Why this choice |
|---|---|---|
| KDF | Argon2id — 64 MiB memory, 3 iterations, 4 parallel lanes | OWASP recommended; memory-hardness defeats GPU/ASIC attacks |
| Cipher | XChaCha20-Poly1305 (IETF standard) | Same as Signal, WireGuard, age; well-audited; 192-bit nonce eliminates nonce-reuse risk |
| RNG | OS-level CSPRNG (BCryptGenRandom / /dev/urandom) | No userspace entropy pool; hardware-seeded |
| Key zeroization | zeroize crate — ZeroizeOnDrop on VaultKey and all secret values | Deterministic zeroing at drop time, not at GC mercy |
| Master password | rpassword for masked input; zeroed after key derivation | Never in shell history; never on heap longer than KDF call |
Multi-source sync
The local vault is the execution surface. The cloud vault is the source of truth. tsafe bridges them.
| Source | Command | Auth method |
|---|---|---|
| Azure Key Vault | tsafe kv-pull --prefix MYAPP_ | Managed identity (VMs/ACI), service principal, or interactive az login |
| HashiCorp Vault | tsafe vault-pull --mount secret --prefix myapp/ | VAULT_TOKEN in env |
| 1Password | tsafe op-pull 'Database Credentials' --op-vault Personal | 1Password CLI (op) authentication |
The pull populates the local vault. From there, exec injects. The source of truth (AKV, 1Password, wherever) is never accessed at runtime by your application — only by the explicit kv-pull sync step.
Putting it together
A fully configured tsafe workflow for a team looks like: one profile per environment, namespaces per service or role, kv-pull in the morning to sync from AKV, exec with --ns to run services, hardened mode in CI. The vault file is in the repo (encrypted ciphertext). The only pipeline secret is TSAFE_PASSWORD.