use clap::ValueEnum;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExplainTopicSurface {
AlwaysAvailable,
CoreDefault,
GatedNonCore,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum ExplainTopic {
Exec,
Export,
Namespaces,
#[cfg(feature = "agent")]
Agent,
#[value(name = "exec-security")]
ExecSecurity,
Contracts,
#[value(name = "vault-recovery")]
VaultRecovery,
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
Pull,
#[value(name = "pull-auth")]
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
PullAuth,
#[value(name = "pull-reliability")]
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
PullReliability,
}
impl ExplainTopic {
pub const fn surface(self) -> ExplainTopicSurface {
match self {
ExplainTopic::Exec
| ExplainTopic::Export
| ExplainTopic::Namespaces
| ExplainTopic::ExecSecurity
| ExplainTopic::Contracts
| ExplainTopic::VaultRecovery => ExplainTopicSurface::AlwaysAvailable,
#[cfg(feature = "agent")]
ExplainTopic::Agent => ExplainTopicSurface::CoreDefault,
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::Pull | ExplainTopic::PullAuth | ExplainTopic::PullReliability => {
pull_topic_surface()
}
}
}
pub const fn as_str(self) -> &'static str {
match self {
ExplainTopic::Exec => "exec",
ExplainTopic::Export => "export",
ExplainTopic::Namespaces => "namespaces",
#[cfg(feature = "agent")]
ExplainTopic::Agent => "agent",
ExplainTopic::ExecSecurity => "exec-security",
ExplainTopic::Contracts => "contracts",
ExplainTopic::VaultRecovery => "vault-recovery",
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::Pull => "pull",
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::PullAuth => "pull-auth",
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::PullReliability => "pull-reliability",
}
}
pub fn blurb(self) -> &'static str {
match self {
ExplainTopic::Exec => "inject secrets into one child process (primary workflow)",
ExplainTopic::Export => "print secrets to stdout / shell (secondary)",
ExplainTopic::Namespaces => "isolate keys per app or repo in one vault",
#[cfg(feature = "agent")]
ExplainTopic::Agent => "keep vault unlocked for a TTL; reuse across commands",
ExplainTopic::ExecSecurity => {
"inheritance, stripped vars, NODE_OPTIONS / LD_* warnings"
}
ExplainTopic::Contracts => {
"named exec policy in .tsafe.yml (allowed secrets, targets, trust level)"
}
ExplainTopic::VaultRecovery => "options when you forget the master password",
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::Pull => {
if has_non_azure_pull_family() {
"sync secrets from compiled-in pull sources into the local vault"
} else {
"sync secrets from Azure Key Vault into the local vault"
}
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::PullAuth => {
if has_non_azure_pull_family() {
"credential setup for compiled-in pull sources"
} else {
"credential setup for Azure Key Vault pull"
}
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::PullReliability => "retries, transient failures, and --on-error modes",
}
}
}
fn surface_label(surface: ExplainTopicSurface) -> &'static str {
match surface {
ExplainTopicSurface::AlwaysAvailable => "always",
ExplainTopicSurface::CoreDefault => "core/default",
ExplainTopicSurface::GatedNonCore => "gated non-core",
}
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
const fn pull_topic_surface() -> ExplainTopicSurface {
if cfg!(feature = "akv-pull")
&& !cfg!(feature = "cloud-pull-aws")
&& !cfg!(feature = "cloud-pull-gcp")
&& !cfg!(feature = "cloud-pull-vault")
&& !cfg!(feature = "cloud-pull-1password")
&& !cfg!(feature = "multi-pull")
{
ExplainTopicSurface::CoreDefault
} else {
ExplainTopicSurface::GatedNonCore
}
}
pub static ALL_TOPICS: &[ExplainTopic] = &[
ExplainTopic::Exec,
ExplainTopic::Export,
ExplainTopic::Namespaces,
#[cfg(feature = "agent")]
ExplainTopic::Agent,
ExplainTopic::ExecSecurity,
ExplainTopic::Contracts,
ExplainTopic::VaultRecovery,
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::Pull,
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::PullAuth,
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
ExplainTopic::PullReliability,
];
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
struct PullSourceDoc {
label: &'static str,
command: &'static str,
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
static PULL_SOURCES: &[PullSourceDoc] = &[
#[cfg(feature = "akv-pull")]
PullSourceDoc {
label: "Azure Key Vault",
command: "tsafe kv-pull",
},
#[cfg(feature = "cloud-pull-aws")]
PullSourceDoc {
label: "AWS Secrets Manager",
command: "tsafe aws-pull",
},
#[cfg(feature = "cloud-pull-aws")]
PullSourceDoc {
label: "AWS SSM Parameter Store",
command: "tsafe ssm-pull",
},
#[cfg(feature = "cloud-pull-gcp")]
PullSourceDoc {
label: "GCP Secret Manager",
command: "tsafe gcp-pull",
},
#[cfg(feature = "cloud-pull-vault")]
PullSourceDoc {
label: "HashiCorp Vault KV v2",
command: "tsafe vault-pull",
},
#[cfg(feature = "cloud-pull-1password")]
PullSourceDoc {
label: "1Password",
command: "tsafe op-pull",
},
];
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
struct PullAuthSection {
heading: &'static str,
body: &'static str,
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
static PULL_AUTH_SECTIONS: &[PullAuthSection] = &[
#[cfg(feature = "cloud-pull-aws")]
PullAuthSection {
heading: "AWS SECRETS MANAGER (tsafe aws-pull)",
body: r#" Static credentials (local dev):
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=us-east-1
Temporary credentials (assumed role / SSO):
eval $(aws sts assume-role ... --query Credentials --output text | ...)
# or: aws configure sso, then run tsafe — SDK picks up the profile
EC2 / ECS / Lambda — nothing to set; IMDSv2 / ECS task role used automatically."#,
},
#[cfg(feature = "cloud-pull-aws")]
PullAuthSection {
heading: "AWS SSM PARAMETER STORE (tsafe ssm-pull)",
body: r#" Same credentials as Secrets Manager. The IAM policy needs:
ssm:GetParametersByPath on arn:aws:ssm:REGION:ACCOUNT:parameter/PATH*
kms:Decrypt on the KMS key for SecureString parameters"#,
},
#[cfg(feature = "cloud-pull-gcp")]
PullAuthSection {
heading: "GCP SECRET MANAGER (tsafe gcp-pull)",
body: r#" Local dev (gcloud CLI):
gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT=my-project
CI / short-lived token:
export GOOGLE_OAUTH_TOKEN=$(gcloud auth print-access-token)
export GOOGLE_CLOUD_PROJECT=my-project
GKE / Cloud Run / GCE — metadata server used automatically.
Required IAM role: roles/secretmanager.secretAccessor on the project."#,
},
#[cfg(feature = "akv-pull")]
PullAuthSection {
heading: "AZURE KEY VAULT (tsafe kv-pull)",
body: r#" Service principal (CI):
export AZURE_TENANT_ID=...
export AZURE_CLIENT_ID=...
export AZURE_CLIENT_SECRET=...
export TSAFE_AKV_URL=https://myvault.vault.azure.net
Azure VM / ACI — IMDS managed identity used automatically.
Required role: Key Vault Secrets User on the vault."#,
},
#[cfg(feature = "cloud-pull-vault")]
PullAuthSection {
heading: "HASHICORP VAULT (tsafe vault-pull)",
body: r#" export VAULT_TOKEN=hvs....
export TSAFE_HCP_URL=http://vault:8200 # or use --addr"#,
},
#[cfg(feature = "cloud-pull-1password")]
PullAuthSection {
heading: "1PASSWORD (tsafe op-pull)",
body: r#" Install the op CLI and authenticate:
op signin
tsafe op-pull "Database Credentials""#,
},
];
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
fn has_non_azure_pull_family() -> bool {
cfg!(feature = "cloud-pull-aws")
|| cfg!(feature = "cloud-pull-gcp")
|| cfg!(feature = "cloud-pull-vault")
|| cfg!(feature = "cloud-pull-1password")
|| cfg!(feature = "multi-pull")
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
fn is_akv_only_pull_build() -> bool {
cfg!(feature = "akv-pull") && !has_non_azure_pull_family()
}
fn print_compiled_truth() {
println!("\nCompiled truth for this binary:\n");
#[cfg(feature = "agent")]
println!(
" - `agent` is a core/default workflow here, but the background `tsafe-agent` runtime is still a separate companion binary."
);
#[cfg(not(feature = "agent"))]
println!(" - `agent` help is omitted because this build does not compile the core `agent` workflow.");
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
{
if is_akv_only_pull_build() {
println!(
" - Cloud pull coverage is Azure Key Vault only (`tsafe kv-pull`); AWS/GCP/Vault/1Password pulls and multi-source `tsafe pull` are not compiled in."
);
} else {
println!(
" - Cloud pull topics describe only the providers and orchestration commands compiled into this binary."
);
}
}
#[cfg(not(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
)))]
println!(
" - Cloud pull help is omitted because this build has no pull providers compiled in."
);
#[cfg(not(feature = "nativehost"))]
println!(
" - Browser/native-host flows are not compiled into this `tsafe` binary; they require browser/nativehost-enabled builds plus the separate `tsafe-nativehost` artifact."
);
#[cfg(not(feature = "ssh"))]
println!(" - SSH helper commands (`tsafe ssh-add`, `tsafe ssh-import`) are not compiled into this build.");
#[cfg(not(feature = "plugins"))]
println!(" - The plugin command surface (`tsafe plugin`) is not compiled into this build.");
#[cfg(not(feature = "git-helpers"))]
println!(
" - Git helper flows such as `tsafe credential-helper` are not compiled into this build."
);
}
pub fn run(topic: Option<ExplainTopic>) {
match topic {
None => {
println!("Usage: tsafe explain <topic>\n");
println!("Topics:\n");
for t in ALL_TOPICS {
println!(
" {:<16} [{:<13}] {}",
t.as_str(),
surface_label(t.surface()),
t.blurb()
);
}
println!("\nExample: tsafe explain exec");
print_compiled_truth();
}
Some(ExplainTopic::Exec) => print_exec(),
Some(ExplainTopic::Export) => print_export(),
Some(ExplainTopic::Namespaces) => print_namespaces(),
#[cfg(feature = "agent")]
Some(ExplainTopic::Agent) => print_agent(),
Some(ExplainTopic::ExecSecurity) => print_exec_security(),
Some(ExplainTopic::Contracts) => print_contracts(),
Some(ExplainTopic::VaultRecovery) => print_vault_recovery(),
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
Some(ExplainTopic::Pull) => print_pull(),
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
Some(ExplainTopic::PullAuth) => print_pull_auth(),
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
Some(ExplainTopic::PullReliability) => print_pull_reliability(),
}
}
fn print_exec() {
print!(
r#"
exec — run a program with vault secrets as normal environment variables
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This is the default way to use tsafe day-to-day. Your app keeps reading
process.env / %ENV% like every tutorial; you do not need a plaintext .env
on disk for that run.
tsafe exec -- <command> [args...]
Default scope
Without --contract / --keys / --ns, exec injects every non-alias secret
in the active vault into the child environment. Parent ambient credentials
are still stripped via the SENSITIVE_VARS strip list and dangerous-name
vault entries (LD_PRELOAD, NODE_OPTIONS, ...) still abort the spawn — but
the *vault scope* is the whole vault until you adopt a contract or one of
the narrowing flags. For least-authority workflows, see `tsafe explain
contracts`.
Preview which names would be set (sorted, values never printed):
tsafe exec --dry-run
Fail before starting if required keys are missing (after --ns mapping):
tsafe exec --require API_KEY,DB_URL -- npm test
Namespaces — only inject keys under a prefix; child sees names without it:
tsafe exec --ns myapp -- python app.py
Shell: always put a bare "--" before the real command so flags are not parsed
by tsafe. Quote carefully so the shell does not expand $VAR before the child.
Layering: if you use direnv, Docker, or CI-injected env, you may have both
parent env and vault keys. Parent tokens (GITHUB_TOKEN, ADO_PAT, …) are
stripped from the child before injection so they do not leak accidentally.
Use `tsafe explain exec-security` for the shipped inheritance presets and
`--allow-dangerous-env` / `--no-inherit` / `--only` details.
Docs: docs/features/export-and-exec.md · tsafe explain exec-security
"#
);
}
fn print_export() {
print!(
r#"
export / get — when not to use exec
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Use `tsafe export` or `tsafe get` when something must read secrets from
stdout, a file, or the clipboard — not from a subprocess environment.
Examples:
· CI: tsafe export --format github-actions >> $GITHUB_ENV
· Shell session: eval "$(tsafe export --format dotenv)" (widens blast radius)
· One-off: tsafe get API_KEY --copy
Prefer `exec` when the consumer is a single command: no plaintext hop through
a file or your interactive shell.
"#
);
}
fn print_namespaces() {
print!(
r#"
namespaces — multiple logical apps in one vault
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Store keys as "ns/KEY" (e.g. web/DB_URL and batch/DB_URL). List and export
can filter with --ns.
With exec, --ns injects only matching keys and strips the prefix so the
child sees plain DB_URL.
tsafe set web/API_KEY --tag env=dev
tsafe exec --ns web -- npm run dev
Bulk copy or rename every key under a prefix (clone or migrate a namespace):
tsafe ns copy prod staging # prod/* also exists under staging/*
tsafe ns move oldapp newapp # oldapp/* → newapp/* (sources removed)
tsafe ns copy a b --force # overwrite destination keys if present
Single-key rename still uses: tsafe mv FROM_KEY TO_KEY
"#
);
}
#[cfg(feature = "agent")]
fn print_agent() {
print!(
r#"
agent — fewer password prompts
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`tsafe agent unlock` starts a session (TTL, e.g. 30m). While active, CLI and
tools that use the agent socket can open the vault without typing the master
password each time.
tsafe agent unlock --ttl 30m
tsafe exec -- npm test
tsafe agent lock
See: tsafe agent --help
"#
);
}
fn print_exec_security() {
print!(
r#"
exec-security — what exec does not guarantee
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Inheritance
`standard` starts from your current environment, strips sensitive parent
variables, then adds vault keys. `hardened` uses a minimal inherited env;
`--no-inherit` starts clean; `--only` whitelists explicit parent names.
Sensitive parent variables (TSAFE_PASSWORD, cloud tokens, PATs, etc.) are
removed before spawn — see SENSITIVE_VARS in crates/tsafe-core/src/env.rs.
High-risk names
If a vault key would set NODE_OPTIONS, LD_PRELOAD, or macOS DYLD_* (among
others), tsafe aborts exec by default. Use `--allow-dangerous-env` to inject
them with a warning instead. `--deny-dangerous-env` is now just an explicit
compatibility spelling of the default.
Same-user threat model
On Unix, environment is not a hard security boundary against malware or
debuggers running as you. exec scopes secrets to one process tree and
avoids argv and dotfiles; it does not replace OS-level isolation.
Docs: docs/features/security.md · docs/research/exec-as-product-2026-04.md
"#
);
}
fn print_contracts() {
print!(
r#"
contracts — named, reusable exec authority in .tsafe.yml
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
A contract replaces ad-hoc flag bundles with a named, auditable authority
definition committed to your repository. Contracts answer in one command:
"what exactly am I allowing this process to receive?"
MANIFEST (.tsafe.yml in your project root)
contracts:
deploy:
profile: prod # which vault to open (overridden by -p)
namespace: deploy # --ns equivalent
allowed_secrets: # injection ceiling — only these keys pass through
- ARM_SUBSCRIPTION_ID
- TF_BACKEND_KEY
required_secrets: # fail before spawn if any are missing
- TF_BACKEND_KEY
allowed_targets: # which binaries this contract may run
- terraform
trust_level: hardened # standard | hardened | custom
TRUST LEVELS
standard — full inherited env minus strip list; dangerous names denied
hardened — minimal env, dangerous names denied, child output redacted
custom — contract-defined `inherit`, `deny_dangerous_env`, and
`redact_output` settings in the manifest
USAGE
tsafe exec --contract deploy -- terraform apply
Without --contract, --keys, or --ns, exec injects EVERY non-alias secret
in the active vault (parent strip and dangerous-name guard still apply).
--contract is the recommended primary model and the only way to declare
the authorisation set up front; flags (--keys, --ns) are escape hatches.
PREVIEW AUTHORITY (no secrets ever printed)
tsafe exec --contract deploy --plan
WHAT GETS INJECTED
Intersection of: vault keys ∩ allowed_secrets (if set) ∩ --keys (if set)
Union of: required_secrets + --require
If a --keys name is not in allowed_secrets, exec fails with a clear error.
Manifest validation also requires every `required_secret` to appear in
`allowed_secrets`.
OVERRIDING CONTRACT VALUES
Explicit flags always win over contract values:
-p / --profile overrides contract profile
--ns overrides contract namespace
--keys narrows injection (must be subset of allowed_secrets)
--require adds to contract required_secrets
--mode / --minimal override trust_level
Those overrides can narrow or restyle execution, but they cannot widen the
contract ceiling for allowed secrets or allowed targets.
ERROR MESSAGES
"command 'X' is not in allowed_targets"
→ Add X (or a matching basename) to allowed_targets in .tsafe.yml, or run
without --contract.
"selected key(s) not allowed by contract"
→ The --keys list includes a name not in allowed_secrets; fix either.
"required secret 'X' not found"
→ tsafe set X <value>
Docs: docs/features/export-and-exec.md (Authority contracts section)
"#
);
}
fn print_vault_recovery() {
print!(
r#"
vault-recovery — options when you forget the master password
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tsafe vaults use Argon2id + XChaCha20-Poly1305. There is no backdoor.
Recovery depends on which of these you set up before forgetting.
1. Quick unlock / biometric (most common — you may not need your password)
tsafe biometric status
If enabled, the vault opens without a password:
tsafe get any-key
tsafe unlock # if the vault file is currently locked
2. Main vault bridging
If another vault stores this profile's password under profile-passwords/<name>:
tsafe -p main get profile-passwords/<this-profile>
3. Agent still running
If tsafe agent unlock is active in your session, the password is cached:
tsafe agent status
tsafe get any-key # will use agent if socket is alive
4. TSAFE_PASSWORD env var
If you set it in your shell or CI environment, read it from there.
(Remove it from dotfiles, scripts, and CI secrets after recovery.)
5. Password manager
Check if you stored the vault password during tsafe init.
If none of the above work, the vault is not recoverable. Encrypted data
cannot be decrypted without the key. To start over:
tsafe -p <profile> init # creates a new empty vault under that profile
re-pull or re-import from any source compiled into this build if configured
"#
);
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
fn print_pull() {
if is_akv_only_pull_build() {
print!(
r#"
pull — sync secrets from Azure Key Vault into the local vault
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This build ships the core/default Azure Key Vault pull lane only. tsafe still
acts as a local-first runtime authority: Azure is a read-only source, secrets
are synced into the local vault, and `tsafe exec` scopes them to one process.
Nothing is written back to Azure.
Additional provider pulls (AWS/GCP/Vault/1Password) and multi-source `tsafe pull`
orchestration are gated non-core surfaces and are not compiled into this binary.
AVAILABLE SOURCES IN THIS BUILD
"#
);
} else {
print!(
r#"
pull — sync secrets from cloud providers into the local vault
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tsafe acts as a local-first runtime authority. Cloud providers are read-only
sources — secrets are synced in, then tsafe exec scopes them to one process.
Nothing is written back to the cloud.
AVAILABLE SOURCES IN THIS BUILD
"#
);
}
for source in PULL_SOURCES {
println!(" {:<26} {}", source.label, source.command);
}
if cfg!(feature = "multi-pull") {
print!(
r#"
ONE COMMAND FOR ALL SOURCES (.tsafe.yml)
Define all your sources in .tsafe.yml at the repo root:
pulls:
- source: aws
region: us-east-1
prefix: myapp/
- source: gcp
project: my-project
- source: akv
vault_url: https://myvault.vault.azure.net
Then pull everything with one command:
tsafe pull
KEY NORMALISATION (all sources)
Cloud names → Local key
my-secret → MY_SECRET
myapp/db-password → MYAPP_DB_PASSWORD (AWS / SSM path)
/prod/myapp/api-key → PROD_MYAPP_API_KEY (SSM absolute path)
db.password → DB_PASSWORD (GCP dot separator)
OVERWRITE BEHAVIOUR
By default, existing local secrets are not overwritten:
tsafe pull # safe: new keys only
tsafe pull --overwrite # replace all matching keys
AFTER PULLING
Preview what would be injected (no secret values printed):
tsafe exec --dry-run
Run your app:
tsafe exec -- npm start
Credential setup: tsafe explain pull-auth
"#
);
} else {
print!(
r#"
KEY NORMALISATION
Provider names are normalised into local vault keys before storage.
OVERWRITE BEHAVIOUR
By default, existing local secrets are not overwritten:
tsafe kv-pull # safe: new keys only
tsafe kv-pull --overwrite # replace all matching keys
AFTER PULLING
Preview what would be injected (no secret values printed):
tsafe exec --dry-run
Run your app:
tsafe exec -- npm start
Credential setup: tsafe explain pull-auth
This page is intentionally build-scoped: if you need AWS/GCP/Vault/1Password
or multi-source `tsafe pull`, use a binary compiled with those gated features.
"#
);
}
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
fn print_pull_auth() {
if is_akv_only_pull_build() {
print!(
r#"
pull-auth — credential setup for compiled-in pull sources
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This build ships Azure Key Vault pull only. The provider sections below are
limited to commands that exist in this binary; broader cloud pulls are gated
non-core surfaces and are omitted here.
"#
);
} else {
print!(
r#"
pull-auth — credential setup for cloud pull sources
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Each provider has its own credential model. Set the env vars before running
the pull command compiled into this build.
"#
);
}
for section in PULL_AUTH_SECTIONS {
println!("\n{}\n{}", section.heading, section.body);
}
if cfg!(feature = "multi-pull") {
print!(
r#"
TESTING CREDENTIALS
Dry-run your pull config without writing to the vault:
tsafe pull --dry-run
Check what is in the vault after pulling:
tsafe list
"#
);
} else {
print!(
r#"
TESTING CREDENTIALS
Run the provider-specific pull command for this build, then inspect:
tsafe list
"#
);
}
}
#[cfg(any(
feature = "akv-pull",
feature = "cloud-pull-aws",
feature = "cloud-pull-gcp",
feature = "cloud-pull-vault",
feature = "cloud-pull-1password",
feature = "multi-pull"
))]
fn print_pull_reliability() {
print!(
r#"
pull-reliability — retries, transient errors, and continuation behavior
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PULL RETRIES
Cloud pull clients retry two classes of failures:
· HTTP 429 (throttling): up to 3 retries, honors Retry-After header when present
· transient transport failures: up to 5 retries with jittered exponential backoff
Transient transport examples:
timeout, connection refused, connection reset, temporary network faults
JITTERED BACKOFF
Retry delay uses exponential backoff with +0..25% jitter to reduce synchronized
retry storms when many jobs pull at once.
PULL COMMAND ERROR MODES
All pull commands now accept:
--on-error fail-all # default, abort on first provider/source error
--on-error skip-failed # continue remaining sources, skip failed source
--on-error warn-only # continue and warn; useful for best-effort sync
Examples:
"#
);
#[cfg(feature = "akv-pull")]
println!(" tsafe kv-pull --on-error warn-only");
#[cfg(feature = "cloud-pull-aws")]
println!(" tsafe aws-pull --on-error warn-only");
#[cfg(feature = "cloud-pull-gcp")]
println!(" tsafe gcp-pull --on-error skip-failed");
#[cfg(feature = "multi-pull")]
println!(" tsafe pull --config .tsafe.yml --on-error skip-failed");
if cfg!(feature = "multi-pull") {
print!(
r#"
MULTI-SOURCE BEHAVIOR (`tsafe pull`)
With skip-failed/warn-only, a failed source does not stop later sources.
The command prints per-source failures and a final failure count.
"#
);
}
print!(
r#"
OPERATOR GUIDANCE
Use fail-all for CI gates that require every source to succeed.
Use skip-failed for mixed environments where some sources are optional.
Use warn-only for local/dev best-effort hydration.
"#
);
if is_akv_only_pull_build() {
print!(
r#"
This binary only exposes the Azure Key Vault pull lane. Multi-source continuation
semantics for `tsafe pull` are a gated non-core surface and are not available here.
"#
);
}
}