use clap::{Parser, Subcommand, ValueEnum};
pub use clap_complete::Shell;
const ROOT_LONG_ABOUT: &str = "Manage secrets in a local encrypted vault instead of scattering them across `.env` files, shell history, and ad-hoc runtime exports.\n\nThe core-only release family centers on local encrypted vault CRUD, `exec`/contracts, profiles, snapshots, audit, and `doctor`, plus the default-core Azure Key Vault pull, biometric/quick-unlock, and team workflows when they are compiled into this `tsafe` binary. Some named stack shapes also include the terminal UI and/or the `agent` workflow as explicit companion/runtime claims. Broader gated non-core lanes such as AWS, GCP, browser/nativehost, plugins, and other additive surfaces appear only when they are compiled into this binary and shipped by the chosen stack. Companion runtimes such as `tsafe-agent` are installed and released separately. Use `tsafe build-info` when you need the compiled truth for the running binary.";
const ROOT_AFTER_HELP: &str = "Compiled truth:\n tsafe build-info\n\nCompanion note:\n `tsafe-agent` is installed and released separately from the `tsafe` CLI binary.\n\nSee also:\n man tsafe\n tsafe explain\n tsafe <command> --help\n docs/index.md in the repository";
const DOCTOR_LONG_ABOUT: &str = "Diagnose vault health: file presence, snapshots, env vars, secret expiry, and operator-facing health hints.\n\nPrints a colour-coded report. Use `--json` for machine-readable monitoring output.";
const DOCTOR_AFTER_HELP: &str =
"Examples:\n tsafe doctor\n tsafe doctor --json\n tsafe --profile prod doctor";
const AUDIT_AFTER_HELP: &str = "Examples:\n tsafe audit\n tsafe audit --limit 100\n tsafe audit --explain\n tsafe audit --explain --json\n tsafe audit --cell-id doom-cell-001\n tsafe audit-verify\n tsafe audit-verify --json";
const ROTATE_DUE_AFTER_HELP: &str =
"Examples:\n tsafe rotate-due\n tsafe rotate-due --json\n tsafe rotate-due --fail # for scripts: non-zero if overdue";
const BUILD_INFO_AFTER_HELP: &str = "Examples:\n tsafe build-info\n tsafe build-info --json";
#[derive(Parser)]
#[command(
name = "tsafe",
about = "tsafe — local encrypted secret runtime for vaults, exec/contracts, and operator workflows",
long_about = ROOT_LONG_ABOUT,
version,
arg_required_else_help = true,
after_help = ROOT_AFTER_HELP
)]
pub struct Cli {
/// Named vault / profile. Defaults to the persisted default (or 'default'). Override with TSAFE_PROFILE env var.
#[arg(short, long, global = true, env = "TSAFE_PROFILE")]
pub profile: Option<String>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Initialise a new encrypted vault for the current profile.
///
/// Creates the vault file for this profile under the platform data directory. Prompts for a master password twice.
///
/// On an interactive terminal, after the vault is created you may be offered "quick unlock":
/// storing the password in the OS credential store (Touch ID / Face ID / Windows Hello / device PIN
/// where the OS supports it). You can accept, defer, or skip; run `tsafe biometric enable` anytime.
///
/// If `tsafe config set-backup-vault main` (or `default`) is set, the new vault's master password is
/// also stored under `profile-passwords/<profile>` in that vault when possible.
///
#[command(after_help = "Examples:\n tsafe init\n tsafe --profile prod init")]
Init,
/// View or change global settings (config.json): password backup target, default profile, etc.
///
/// Use `config set-backup-vault main` so every new vault's master password is also stored under
/// `profile-passwords/<profile>` in the `main` vault (requires that vault to exist and be unlockable when you create more profiles).
#[command(
after_help = "Examples:\n tsafe config show\n tsafe config set-backup-vault main\n tsafe config set-backup-vault default\n tsafe config set-backup-vault off\n tsafe config set-exec-mode hardened\n tsafe config set-exec-redact-output on\n tsafe config add-exec-extra-strip OPENAI_API_KEY"
)]
Config {
#[command(subcommand)]
action: ConfigAction,
},
/// Store or update a secret in the vault.
///
/// If VALUE is omitted on a TTY, you are prompted with masked input (typically `*` per character).
/// Piped / non-interactive stdin reads a single line.
/// Keys may be namespaced with `.` or `-` (e.g. `github.com.token`, `db-prod.PASSWORD`).
///
/// If the key already exists the command will prompt for confirmation (on a TTY)
/// or exit with an error (non-TTY). Pass --overwrite to skip the check.
///
#[command(
after_help = "Examples:\n tsafe set DB_PASSWORD supersecret\n tsafe set github.com.token ghp_xxx --tag env=prod\n tsafe set API_KEY --overwrite # replace existing without prompt"
)]
Set {
/// Secret key (e.g. DB_PASSWORD, github.com.token).
key: String,
/// Secret value. Omit for a masked TTY prompt or a line from stdin when piped.
value: Option<String>,
/// Attach tags as KEY=VALUE pairs (repeatable).
#[arg(short, long = "tag", value_name = "KEY=VALUE")]
tags: Vec<String>,
/// Overwrite the key if it already exists — skips the confirmation prompt.
#[arg(long)]
overwrite: bool,
},
/// Retrieve a secret and print its plaintext value.
///
/// Use --copy to copy to clipboard instead of printing; the clipboard is cleared after 30 s.
/// Use --version to retrieve a previous version (0=current, 1=previous, etc.).
///
#[command(
after_help = "Examples:\n tsafe get DB_PASSWORD\n tsafe get API_KEY --copy\n tsafe get DB_PASSWORD --version 1"
)]
Get {
/// Secret key.
key: String,
/// Copy value to clipboard and clear after 30 seconds (does not print).
#[arg(short, long)]
copy: bool,
/// Retrieve a previous version (0=current, 1=previous, etc.).
#[arg(long)]
version: Option<usize>,
},
/// Permanently remove a secret from the vault.
///
/// The deletion is recorded in the audit log and a snapshot is taken before removal.
///
#[command(after_help = "Examples:\n tsafe delete OLD_TOKEN")]
Delete {
/// Secret key.
key: String,
},
/// List all secret key names stored in the vault.
///
/// Use --tag to filter by attached metadata.
/// Use --ns to filter to a specific namespace (e.g. "cds-adf").
///
#[command(
after_help = "Examples:\n tsafe list\n tsafe list --tag env=prod\n tsafe list --ns cds-adf"
)]
List {
/// Filter to secrets with this tag (KEY=VALUE). Repeatable.
#[arg(short, long = "tag", value_name = "KEY=VALUE")]
tags: Vec<String>,
/// Filter to keys in this namespace (stored as "<ns>/<KEY>").
#[arg(long)]
ns: Option<String>,
},
/// Print secrets to stdout in the chosen format.
///
/// Formats: env (default), dotenv, powershell, json, github-actions, yaml, docker-env.
/// Use --ns to export only keys from a namespace; the prefix is stripped
/// so the output contains plain KEY=VALUE (e.g. APP_PW not cds-adf/APP_PW).
///
#[command(
after_help = "Examples:\n tsafe export\n tsafe export --format powershell > secrets.ps1\n tsafe export --format github-actions --tag env=ci\n tsafe export --ns cds-adf --format dotenv > .env\n tsafe export --format yaml > secrets.yaml\n tsafe export --format docker-env > .env"
)]
Export {
/// Output format.
#[arg(short, long, default_value = "env")]
format: ExportFormat,
/// Limit to specific keys (all keys if omitted).
keys: Vec<String>,
/// Filter to secrets with this tag (KEY=VALUE). Repeatable.
#[arg(short, long = "tag", value_name = "KEY=VALUE")]
tags: Vec<String>,
/// Filter to keys in this namespace; prefix is stripped in output.
#[arg(long)]
ns: Option<String>,
},
/// Execute a command with secrets injected into its environment.
///
/// Secrets are injected as env vars; the child inherits all other env vars.
/// Ctrl-C is forwarded to the child and tsafe exits with the child's exit code.
/// Use --ns to inject only secrets from a namespace (prefix stripped from var names).
///
/// Use --contract to load a named authority contract from the nearest .tsafe.yml manifest.
/// A contract declares profile, namespace, allowed secrets, required secrets, allowed targets,
/// and trust posture as a reusable, auditable policy. Explicit flags still override contract values.
#[command(
name = "exec",
after_help = "Examples:\n tsafe exec -- dotnet run\n tsafe exec -- docker-compose up\n tsafe exec --ns cds-adf -- python pipeline.py\n tsafe exec --dry-run\n tsafe exec --plan -- npm start\n tsafe exec --require API_KEY,DB_URL -- npm test\n tsafe exec --no-inherit -- node index.js\n tsafe exec --only PATH,HOME -- python script.py\n tsafe exec --minimal -- pytest\n tsafe exec --mode hardened -- npm test\n tsafe exec --keys OPENAI_API_KEY,DB_URL -- npm test\n tsafe exec --env MY_API_KEY=VAULT_API_KEY -- npm test\n tsafe exec --preset minimal -- npm test\n tsafe exec --timeout 30 -- npm test\n tsafe exec --redact-output -- npm test\n tsafe exec --contract deploy -- terraform apply\n tsafe exec --contract ci-tests --dry-run"
)]
Exec {
/// Load a named authority contract from the nearest .tsafe.yml (or .tsafe.json) manifest.
/// The contract sets the profile, namespace, allowed/required secrets, allowed targets, and
/// trust posture. Explicit flags (--ns, --keys, --mode, etc.) still override contract values.
#[arg(long, value_name = "NAME")]
contract: Option<String>,
/// Inject only secrets from this namespace; prefix is stripped from env var names.
#[arg(long)]
ns: Option<String>,
/// Inject only these vault keys (after `--ns` prefix stripping). Comma-separated or repeat flag.
/// Missing selected keys abort the run so narrower injection does not silently degrade.
#[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append)]
keys: Vec<String>,
/// Trust preset for this run. `standard` keeps broad compatibility, `hardened` applies a stricter preset,
/// and `custom` uses your persisted exec trust settings. Explicit flags still override the preset.
#[arg(long)]
mode: Option<ExecModeSetting>,
/// Kill the child process after this many seconds and exit non-zero. Default: no timeout.
#[arg(long, value_name = "SECONDS")]
timeout: Option<u64>,
/// Preset for inherited parent environment. `minimal` keeps only PATH and a safe core set
/// (equivalent to --minimal). `full` inherits the full parent environment minus the strip list
/// (equivalent to the default). Explicit --no-inherit, --minimal, and --only override this.
#[arg(long, value_name = "PRESET")]
preset: Option<ExecPresetSetting>,
/// List env var names that would be injected (sorted, one per line) and exit 0; no command is run.
#[arg(long)]
dry_run: bool,
/// Show a human-readable plan: profile, namespace, injected names, --require checks,
/// parent env strips, and a copy-paste run line. Exit 0; no command is run.
#[arg(long)]
plan: bool,
/// Start from a clean environment: no parent env vars are inherited. Only vault secrets
/// (and any --only keys) are visible to the child. Mutually exclusive with --only and --minimal.
#[arg(long, conflicts_with_all = ["only", "minimal"])]
no_inherit: bool,
/// Inherit only a safe minimal set of parent env vars (PATH, HOME, USER, TMPDIR, LANG,
/// TERM, SSH_AUTH_SOCK, etc.) plus vault secrets. No tokens or credentials leak through.
/// Mutually exclusive with --no-inherit and --only.
#[arg(long, conflicts_with_all = ["no_inherit", "only"])]
minimal: bool,
/// Inherit only these parent env vars (comma-separated or repeat flag); all others are
/// stripped. Vault secrets are then added on top. Mutually exclusive with --no-inherit and --minimal.
#[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append, conflicts_with_all = ["no_inherit", "minimal"])]
only: Vec<String>,
/// Require these vault keys (after --ns mapping) to be present. Comma-separated or repeat flag.
#[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append)]
require: Vec<String>,
/// Map a vault key to a different env var name in the child process.
/// Format: ENV_VAR=VAULT_KEY (e.g. --env MY_DB=PROD_SECRET injects the vault value of
/// PROD_SECRET under the name MY_DB). When --keys is also given, only vault keys that are
/// in the --keys allowlist may be referenced; other vault keys are rejected with an error.
/// Repeat the flag for multiple mappings.
#[arg(long = "env", value_name = "ENV_VAR=VAULT_KEY", action = clap::ArgAction::Append)]
env_mappings: Vec<String>,
/// Abort if any injected name is a known high-risk env var (e.g. NODE_OPTIONS, LD_PRELOAD).
/// Redundant: this is now the default. Kept for backwards compatibility.
#[arg(long, conflicts_with = "allow_dangerous_env")]
deny_dangerous_env: bool,
/// Allow injection of known high-risk env var names (e.g. LD_PRELOAD, NODE_OPTIONS).
/// By default, dangerous names abort exec. Use this flag to inject them with a warning instead.
#[arg(long, conflicts_with = "deny_dangerous_env")]
allow_dangerous_env: bool,
/// Replace exact vault secret values in the child's stdout/stderr with [REDACTED].
/// Useful for agent/tool wrappers where you trust the command less than the vault.
#[arg(long, conflicts_with = "no_redact_output")]
redact_output: bool,
/// Force raw child stdout/stderr even if config enables exec output redaction by default.
#[arg(long, conflicts_with = "redact_output")]
no_redact_output: bool,
/// Command and its arguments (omit when using --dry-run or --plan).
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
cmd: Vec<String>,
},
/// Import secrets from a `.env` file or another supported export source.
///
/// `.env` paths work in every build. Some builds may also accept additional
/// source names for password-manager or browser CSV exports.
///
/// When `--from` is a named export source, `--file` is required.
/// Skips keys that already exist unless --overwrite is passed.
///
/// Use --ns to prefix all imported keys with a namespace, e.g. "cds-adf".
/// Keys are stored as "<ns>/<KEY>" allowing multiple projects in one vault
/// without collision (e.g. cds-adf/APP_PW vs mail-automation/APP_PW).
///
#[command(
after_help = "Examples:\n tsafe import --from .env\n tsafe import --from .env.production --overwrite\n tsafe import --from ../cds-adf/.env --ns cds-adf\n tsafe import --from .env --dry-run"
)]
/// If `--from` is a **relative** path that does not exist, the error includes extra hints and
/// searches **downward** from the current directory (bounded depth; skips `target/`, `node_modules/`, `.git/`, etc.)
/// for files with the **same name** (e.g. `.env`) so you can copy-paste a suggested `tsafe import --from '…'` line.
Import {
/// `.env` file path or another supported source name for this build.
#[arg(long, default_value = ".env")]
from: String,
/// Export file path (required when `--from` is a named export source).
#[arg(long)]
file: Option<String>,
/// Overwrite existing keys (skip by default).
#[arg(long)]
overwrite: bool,
/// Skip duplicate keys silently instead of erroring (applies to both
/// within-file duplicates and keys already in the vault).
#[arg(long)]
skip_duplicates: bool,
/// Namespace prefix to prepend to imported keys (e.g. "cds-adf").
/// Keys are stored as "<ns>/<KEY>", preventing collisions across projects.
#[arg(long)]
ns: Option<String>,
/// Show what would be imported without writing any secrets to the vault.
/// Prints each key and whether it would be skipped (existing) or imported.
#[arg(long)]
dry_run: bool,
},
/// Map browser domains to vault profiles for the browser extension.
///
/// The extension uses these mappings to choose which vault profile to
/// query when filling credentials on a given domain.
///
#[command(
after_help = "Examples:\n tsafe browser-profile add github.com\n tsafe browser-profile add paypal.com --profile finance\n tsafe browser-profile list\n tsafe browser-profile remove paypal.com"
)]
#[cfg(feature = "browser")]
#[command(name = "browser-profile")]
BrowserProfile {
#[command(subcommand)]
action: BrowserProfileAction,
},
/// Register or unregister the native messaging host for the browser extension.
///
/// Writes the per-user manifest/registration files needed by supported browsers on the
/// current OS. This command does not require browser-profile mappings or vault access.
///
#[command(
after_help = "Examples:\n tsafe browser-native-host detect\n tsafe browser-native-host register --extension-id <chromium-id>\n tsafe browser-native-host unregister"
)]
#[cfg(feature = "nativehost")]
#[command(name = "browser-native-host")]
BrowserNativeHost {
#[command(subcommand)]
action: BrowserNativeHostAction,
},
/// Re-encrypt all secrets with a new master password (vault re-key).
///
/// Prompts for the current password, then the new password twice (unless non-interactive).
/// For automation / CI, set `TSAFE_PASSWORD` (current) and `TSAFE_NEW_MASTER_PASSWORD` (new);
/// confirmation is skipped when both are set (no OS keychain prompt in that case — run `biometric enable` after).
/// After interactive rotation, you are offered an OS keychain update so quick unlock matches the new password.
/// A snapshot is taken automatically before rotation. `tsafe doctor` suggests periodic rotation.
///
#[command(after_help = "Examples:\n tsafe rotate\n tsafe --profile prod rotate")]
Rotate,
/// Re-encrypt the vault with a new master password and update the biometric credential.
///
/// Prompts for the current password (or reads from TSAFE_PASSWORD), then the new password
/// twice (or reads from TSAFE_NEW_MASTER_PASSWORD). The vault is written atomically via a
/// temp-file rename. If biometric quick-unlock is active, the stored credential is re-stored
/// under the new password so subsequent unlocks continue to work.
///
/// If the vault re-encryption succeeds but the biometric re-store fails, a warning is emitted
/// directing the user to `tsafe biometric re-enroll`.
///
#[command(
name = "rotate-key",
after_help = "Examples:\n tsafe rotate-key\n tsafe --profile prod rotate-key"
)]
RotateKey {
/// Profile to re-key (defaults to the active profile).
#[arg(short, long)]
profile: Option<String>,
},
/// Manage profiles (named vaults).
///
/// Each profile is an independent vault file under the platform data `vaults/` directory.
///
#[command(
after_help = "Examples:\n tsafe profile list\n tsafe profile delete staging\n tsafe profile delete staging --force"
)]
Profile {
#[command(subcommand)]
action: ProfileAction,
},
/// Display recent audit log entries for the current profile in human-readable form.
#[command(after_help = AUDIT_AFTER_HELP)]
Audit {
/// Number of entries to display.
#[arg(short, long, default_value_t = 20)]
limit: usize,
/// Check all secret values against Have I Been Pwned (k-anonymity, no full hash sent).
#[arg(long, conflicts_with = "explain")]
hibp: bool,
/// Show a session-style explanation (grouped operations, exec authority summaries).
#[arg(long)]
explain: bool,
/// With `--explain`, print JSON instead of human text.
#[arg(long, requires = "explain")]
json: bool,
/// Filter entries to those with this CellOS cell ID in their audit context.
#[arg(long, value_name = "CELL_ID")]
cell_id: Option<String>,
},
/// Cross-check authority contracts against a CellOS policy pack.
///
/// Loads authority contracts from the nearest `.tsafe.yml` and compares each
/// contract's `allowed_secrets` against `allowedSecretRefs` in the CellOS
/// policy pack JSON. Reports mismatches and exits non-zero if any are found.
///
/// Use `--policy-file` as an alias for `--cellos-policy` (both accepted).
///
#[command(
after_help = "Examples:\n tsafe validate --cellos-policy doom-airgapped-policy.json\n tsafe validate --policy-file policy.json\n tsafe validate --policy-file policy.json --json"
)]
Validate {
/// Path to the CellOS policy pack JSON file.
#[arg(long, value_name = "PATH", conflicts_with = "policy_file")]
cellos_policy: Option<std::path::PathBuf>,
/// Alias for --cellos-policy. Path to the policy pack JSON file.
#[arg(long, value_name = "PATH", conflicts_with = "cellos_policy")]
policy_file: Option<std::path::PathBuf>,
/// Emit machine-readable JSON output (exit codes are preserved).
#[arg(long)]
json: bool,
},
/// Manage local vault snapshots.
///
/// Snapshots are encrypted copies of the vault file, taken automatically before
/// every write operation. Use them to recover from accidental changes.
///
#[command(after_help = "Examples:\n tsafe snapshot list\n tsafe snapshot restore")]
Snapshot {
#[command(subcommand)]
action: SnapshotAction,
},
/// Pull secrets from Azure Key Vault into the local vault.
///
/// Requires TSAFE_AKV_URL and either a service principal
/// (AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET) or
/// a managed identity (IMDS, automatic inside Azure VMs / ACI).
///
#[command(
after_help = "Examples:\n tsafe kv-pull\n tsafe kv-pull --prefix MYAPP_ --overwrite"
)]
#[cfg(feature = "akv-pull")]
KvPull {
/// Only import secrets whose names start with this prefix (case-insensitive).
/// Omit to pull all secrets.
#[arg(long)]
prefix: Option<String>,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
/// Failure handling mode for provider/network errors.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PullOnError,
},
/// Push local vault secrets to Azure Key Vault (upsert semantics).
///
/// Requires TSAFE_AKV_URL and either a service principal
/// (AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET) or
/// a managed identity (IMDS, automatic inside Azure VMs / ACI).
///
/// Local keys are reverse-normalised to Azure Key Vault format:
/// MY_SECRET → my-secret. Two local keys that normalise to the same
/// provider name are detected as a collision and abort pre-flight.
///
/// Remote-only keys are left untouched unless --delete-missing is passed.
/// A pre-flight diff is always shown before writing. No secret values
/// are printed — only key names and 12-char SHA-256 hash prefixes.
///
#[command(
after_help = "Examples:\n tsafe kv-push --dry-run\n tsafe kv-push --yes\n tsafe kv-push --prefix MYAPP_ --yes\n tsafe kv-push --delete-missing --yes"
)]
#[cfg(feature = "akv-pull")]
KvPush {
/// Only push secrets whose local key names start with this prefix (case-insensitive).
#[arg(long)]
prefix: Option<String>,
/// Only push secrets in this namespace (stored as `<ns>/KEY`).
#[arg(long)]
ns: Option<String>,
/// Show the diff without writing anything (always exits 0).
#[arg(long)]
dry_run: bool,
/// Skip the confirmation prompt (required in non-TTY / CI contexts).
#[arg(long)]
yes: bool,
/// Also delete remote secrets that are absent locally within the filtered scope.
/// Off by default — opt-in to avoid accidental mass deletion.
/// AKV uses soft-delete (30-day recoverable window).
#[arg(long)]
delete_missing: bool,
},
/// Share a vault secret as a one-time HTTPS link via a configured OTS (one-time secret) service.
///
/// Set `TSAFE_OTS_BASE_URL` to your service HTTPS origin (no default). The CLI POSTs JSON
/// `{"secret","ttl"}` to `{base}{TSAFE_OTS_CREATE_PATH}` (default path `/create`) and prints the returned `url`.
///
/// The one-time URL is printed to stdout — never the secret value. Use any server that implements this contract.
///
#[command(
after_help = "Examples:\n tsafe share-once DB_PASSWORD\n tsafe share-once API_KEY --ttl 10m"
)]
#[cfg(feature = "ots-sharing")]
#[command(name = "share-once")]
ShareOnce {
/// Secret key to share.
key: String,
/// Link expiry (sent to the service; many accept 10m, 1h, 24h).
#[arg(short, long, default_value = "1h")]
ttl: String,
},
/// Receive a secret from a one-time link (from `share-once` or any compatible OTS server).
///
/// POSTs to the exact HTTPS URL. The response may be JSON (`secret`, `plaintext`, or `value`) or HTML with
/// `<div id="secret-content">...</div>`.
///
/// Optionally store the retrieved value directly into the vault with --store.
///
#[command(
after_help = "Examples:\n tsafe receive-once 'https://ots.example.com/s/abc123'\n tsafe receive-once '<URL>' --store DB_PASSWORD"
)]
#[cfg(feature = "ots-sharing")]
#[command(name = "receive-once", visible_alias = "snap-receive")]
ReceiveOnce {
/// The full one-time URL (fragment `#...` is ignored).
url: String,
/// Store the received secret in the vault under this key name instead of printing it.
#[arg(long)]
store: Option<String>,
},
/// Generate a cryptographically random secret and store it in the vault.
///
/// Uses a CSPRNG. Default length 32, character set 'alnum'.
///
#[command(
after_help = "Examples:\n tsafe gen DB_PASSWORD\n tsafe gen SESSION_KEY --length 64 --charset hex --print\n tsafe gen TEMP_PASSWORD --exclude-ambiguous --print"
)]
Gen {
/// Key name to store the generated secret under.
key: String,
/// Length of the generated secret in characters (ignored if --words is set).
#[arg(short = 'l', long, default_value_t = 32)]
length: usize,
/// Character set: alnum (default), alpha, numeric, hex, symbol.
#[arg(short = 'c', long, default_value = "alnum")]
charset: String,
/// Generate a passphrase of N random words instead of a random string.
#[arg(short = 'w', long)]
words: Option<usize>,
/// Attach tags as KEY=VALUE pairs (repeatable).
#[arg(short = 't', long = "tag", value_name = "KEY=VALUE")]
tags: Vec<String>,
/// Print the generated value to stdout (otherwise the value is only in the vault).
#[arg(long)]
print: bool,
/// Remove visually ambiguous characters (0, O, l, 1, I) from the charset.
/// Useful when the secret will be read aloud or transcribed manually.
#[arg(long)]
exclude_ambiguous: bool,
},
/// Show key-level changes between the current vault and its most-recent snapshot.
///
/// Highlights added, removed, and modified keys — values are never shown.
///
#[command(after_help = "Examples:\n tsafe diff\n tsafe --profile staging diff")]
Diff,
/// Compare key names across two profiles without decrypting any values.
///
/// Highlights keys present in one profile but missing from the other.
///
#[command(
after_help = "Examples:\n tsafe compare staging\n tsafe --profile dev compare prod"
)]
Compare {
/// Second profile to compare against the active --profile.
profile_b: String,
},
/// Show version history for a secret.
///
/// Lists all stored versions with timestamps. Version 0 is the current
/// value; higher numbers are older. Use `tsafe get KEY --version N` to
/// retrieve a specific version.
///
#[command(after_help = "Examples:\n tsafe history DB_PASSWORD")]
History {
/// Secret key.
key: String,
},
/// Move or rename a secret within the vault, or to a different profile.
///
/// Within a profile this is an atomic rename: key name, namespace prefix,
/// tags and full version history are all preserved.
///
#[command(
after_help = "Examples:\n tsafe mv DB_HOST infra/DB_HOST (add namespace)\n tsafe mv infra/DB_HOST DB_HOST (remove namespace)\n tsafe mv DB_HOST --to-profile prod (move to other profile, same key)\n tsafe mv DB_HOST --to-profile prod NEW_NAME (move + rename)"
)]
Mv {
/// Source secret key.
source: String,
/// Destination key name. Omit when using --to-profile to keep the same key name.
dest: Option<String>,
/// Move the secret to this profile (cross-profile move).
#[arg(long, value_name = "PROFILE")]
to_profile: Option<String>,
/// Overwrite the destination key if it already exists.
#[arg(long, short = 'f')]
force: bool,
},
/// Install a secret-scanning git pre-commit hook in the current repo.
///
/// Scans staged files for hardcoded secrets on every `git commit`.
///
#[command(
after_help = "Examples:\n tsafe hook-install\n tsafe hook-install --dir /path/to/repo"
)]
#[cfg(feature = "git-helpers")]
HookInstall {
/// Repo root directory; defaults to walking up from the current directory.
#[arg(long)]
dir: Option<String>,
},
/// Export audit log entries to stdout or a file as JSON or Splunk HEC events.
#[command(
after_help = "Examples:\n tsafe audit-export --format json --output audit.jsonl\n tsafe audit-export --format splunk"
)]
AuditExport {
/// Output format.
#[arg(short, long, value_enum, default_value = "json")]
format: AuditExportFormat,
/// Write to a file instead of stdout.
#[arg(short, long)]
output: Option<String>,
},
/// Report HMAC chain coverage for the audit log of the current profile.
///
/// Reads all entries from the audit log file and counts how many carry a
/// `prev_entry_hmac` field (written by a C8-capable tsafe build) versus
/// how many are unchained (written before C8 or at a session boundary).
///
/// IMPORTANT — ephemeral-key limitation: the HMAC chain key is generated
/// fresh on every tsafe session and is never persisted. This command
/// cannot perform cryptographic verification of entries from a closed
/// session; it can only report chain coverage (presence of the field).
/// To detect within-session tampering, use AuditLog::verify_chain() from
/// a live session handle.
///
/// Exit codes: 0 = log is structurally valid (or empty), 2 = at least one
/// entry could not be parsed as JSON.
#[command(
after_help = "Examples:\n tsafe audit-verify\n tsafe audit-verify --json\n tsafe --profile prod audit-verify"
)]
AuditVerify {
/// Emit machine-readable JSON output.
#[arg(long)]
json: bool,
},
/// Set or remove a rotation policy on a secret.
///
/// Policies are stored as tags and checked by `tsafe doctor` and `tsafe rotate-due`.
///
#[command(
after_help = "Examples:\n tsafe policy set DB_PASSWORD --rotate-every 90d\n tsafe policy remove DB_PASSWORD"
)]
Policy {
#[command(subcommand)]
action: PolicyAction,
},
/// List secrets that are overdue for rotation (per `rotate_policy` tags).
///
/// Checks the `rotate_policy` tag against the secret's `updated_at` timestamp.
/// Use `--json` for automation; `--fail` exits with status 1 when anything is overdue (CI/cron).
///
/// Set policies with: `tsafe policy set KEY --rotate-every 90d`
#[command(after_help = ROTATE_DUE_AFTER_HELP)]
RotateDue {
/// Print JSON to stdout (`overdue_count` + `items` with key, days_overdue, policy).
#[arg(long)]
json: bool,
/// Exit with status 1 when one or more secrets are overdue.
#[arg(long)]
fail: bool,
},
/// Pull secrets from a HashiCorp Vault KV v2 store.
///
/// Requires TSAFE_HCP_URL or --addr and VAULT_TOKEN (or --token).
///
#[command(
after_help = "Examples:\n tsafe vault-pull --addr http://vault:8200 --prefix myapp/\n tsafe vault-pull # uses TSAFE_HCP_URL + VAULT_TOKEN"
)]
#[cfg(feature = "cloud-pull-vault")]
VaultPull {
/// HashiCorp Vault address. Defaults to TSAFE_HCP_URL or http://127.0.0.1:8200.
#[arg(long)]
addr: Option<String>,
/// Vault token. Defaults to VAULT_TOKEN env var.
/// Deprecated: passing the token as a CLI argument exposes it in the process
/// table. Store the token in tsafe and use `tsafe exec -- tsafe vault-pull`
/// so the token is injected securely without appearing in the process table.
#[arg(long)]
token: Option<String>,
/// KV v2 mount path. Defaults to "secret".
#[arg(long)]
mount: Option<String>,
/// Only import secrets under this path prefix.
#[arg(long)]
prefix: Option<String>,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
},
/// Pull fields from a 1Password item via the `op` CLI.
///
/// Requires the 1Password CLI (`op`) installed and authenticated.
///
#[command(
after_help = "Examples:\n tsafe op-pull 'Database Credentials'\n tsafe op-pull abc123xyz --op-vault Personal"
)]
#[cfg(feature = "cloud-pull-1password")]
OpPull {
/// Item title or ID.
item: String,
/// 1Password vault name (uses the default vault if omitted).
#[arg(long = "op-vault")]
op_vault: Option<String>,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
},
/// Import Login items from Bitwarden into the local vault via the `bw` CLI.
///
/// Bitwarden REST API ciphers are always E2E encrypted client-side. This command
/// shells to the `bw` CLI (which handles local decryption) rather than calling
/// the REST API directly — the same pattern as `tsafe op-pull` for 1Password.
///
/// Requires TSAFE_BW_CLIENT_ID, TSAFE_BW_CLIENT_SECRET, and TSAFE_BW_PASSWORD
/// (master password for `bw unlock`). The `bw` CLI must be installed and on PATH.
///
/// Item names are normalised: spaces and hyphens become underscores, uppercase.
/// Login.Username → ITEM_NAME_USERNAME, Login.Password → ITEM_NAME_PASSWORD.
/// Custom text/hidden fields → ITEM_NAME_<FIELD_NAME>. Boolean fields are skipped.
///
#[command(
name = "bw-pull",
after_help = "Examples:\n tsafe bw-pull\n tsafe bw-pull --bw-folder my-folder-id --overwrite\n tsafe bw-pull --bw-client-id org.abc --bw-password-env MY_BW_PW"
)]
#[cfg(feature = "cloud-pull-bitwarden")]
BwPull {
/// Bitwarden API client ID. Reads TSAFE_BW_CLIENT_ID if not set.
#[arg(long = "bw-client-id")]
bw_client_id: Option<String>,
/// Bitwarden API client secret. Reads TSAFE_BW_CLIENT_SECRET if not set.
#[arg(long = "bw-client-secret")]
bw_client_secret: Option<String>,
/// Bitwarden API base URL (for self-hosted / Vaultwarden).
/// Default: https://api.bitwarden.com
#[arg(long = "bw-api-url")]
bw_api_url: Option<String>,
/// Bitwarden identity base URL (for self-hosted / Vaultwarden).
/// Default: https://identity.bitwarden.com
#[arg(long = "bw-identity-url")]
bw_identity_url: Option<String>,
/// Bitwarden folder ID to filter items. Imports all items when omitted.
#[arg(long = "bw-folder")]
bw_folder: Option<String>,
/// Name of the env var holding the Bitwarden master password for `bw unlock`.
/// Default: TSAFE_BW_PASSWORD
#[arg(long = "bw-password-env")]
bw_password_env: Option<String>,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
/// Failure handling mode for provider/network errors.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PullOnError,
/// Show which items would be imported without writing any secrets.
#[arg(long)]
dry_run: bool,
},
/// Import secrets from a KeePass `.kdbx` file into the local vault.
///
/// Opens a local KeePass database using the master password (from the env var
/// named by --kp-password-env, default TSAFE_KP_PASSWORD) and/or a key file.
///
/// Entry titles are used as key prefixes. Standard fields (UserName, Password, URL)
/// map to TITLE_USERNAME, TITLE_PASSWORD, TITLE_URL. Custom fields map to
/// TITLE_<FIELD_NAME_NORMALISED>. Notes are skipped.
///
#[command(
after_help = "Examples:\n tsafe kp-pull --kp-path /home/user/vault.kdbx\n tsafe kp-pull --kp-path ~/db.kdbx --kp-password-env MY_KP_PW --kp-group Infra\n tsafe kp-pull --kp-path db.kdbx --kp-keyfile ~/my.keyx"
)]
#[cfg(feature = "cloud-pull-keepass")]
KpPull {
/// Absolute path to the `.kdbx` database file.
#[arg(long = "kp-path")]
kp_path: String,
/// Name of the env var that holds the master password.
/// Defaults to TSAFE_KP_PASSWORD.
#[arg(long = "kp-password-env", default_value = "TSAFE_KP_PASSWORD")]
kp_password_env: String,
/// Path to a KeePass key file (optional).
#[arg(long = "kp-keyfile")]
kp_keyfile: Option<String>,
/// Only import entries from this group name (case-insensitive).
#[arg(long = "kp-group")]
kp_group: Option<String>,
/// When set, also traverse descendant groups under the matched group.
#[arg(long = "kp-recursive")]
kp_recursive: bool,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
/// Failure handling mode for provider/network errors.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PullOnError,
},
/// Import secrets from AWS Secrets Manager into the local vault.
///
/// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
/// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
///
/// Region is read from AWS_DEFAULT_REGION / AWS_REGION or --region.
///
/// Secret names are normalised: slashes and hyphens become underscores and
/// the result is uppercased (e.g. `myapp/db-password` → `MYAPP_DB_PASSWORD`).
///
#[command(
after_help = "Examples:\n tsafe aws-pull --region us-east-1\n tsafe aws-pull --prefix myapp/ --overwrite"
)]
#[cfg(feature = "cloud-pull-aws")]
AwsPull {
/// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
#[arg(long)]
region: Option<String>,
/// Only import secrets whose names start with this prefix.
#[arg(long)]
prefix: Option<String>,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
/// Failure handling mode for provider/network errors.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PullOnError,
},
/// Import secrets from GCP Secret Manager into the local vault.
///
/// Authenticates via (in order): GOOGLE_OAUTH_TOKEN env var, GCE/Cloud Run/GKE
/// metadata server, or ADC file (gcloud auth application-default login).
///
/// Project is read from GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT or --project.
///
/// Secret names are normalised: hyphens and dots become underscores and
/// the result is uppercased (e.g. `db-password` → `DB_PASSWORD`).
///
#[command(
after_help = "Examples:\n tsafe gcp-pull --project my-gcp-project\n tsafe gcp-pull --prefix myapp- --overwrite"
)]
#[cfg(feature = "cloud-pull-gcp")]
GcpPull {
/// GCP project ID (overrides GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT).
#[arg(long)]
project: Option<String>,
/// Only import secrets whose names start with this prefix.
#[arg(long)]
prefix: Option<String>,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
/// Failure handling mode for provider/network errors.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PullOnError,
},
/// Push local vault secrets to GCP Secret Manager (upsert semantics).
///
/// Authenticates via (in order): GOOGLE_OAUTH_TOKEN env var, GCE/Cloud Run/GKE
/// metadata server, or ADC file (gcloud auth application-default login).
///
/// Project is read from GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT or --project.
///
/// GCP Secret Manager uses a two-call pattern for new secrets: create the
/// secret resource, then add a version. Existing secrets only need a new version.
///
/// Local keys are reverse-normalised to GCP format:
/// MY_SECRET → my-secret. Two local keys that normalise to the same
/// provider name are detected as a collision and abort pre-flight.
///
/// Remote-only keys are left untouched unless --delete-missing is passed.
/// A pre-flight diff is always shown before writing. No secret values
/// are printed — only key names and 12-char SHA-256 hash prefixes.
///
#[command(
after_help = "Examples:\n tsafe gcp-push --project my-project --dry-run\n tsafe gcp-push --project my-project --yes\n tsafe gcp-push --prefix MYAPP_ --yes\n tsafe gcp-push --delete-missing --yes"
)]
#[cfg(feature = "cloud-pull-gcp")]
GcpPush {
/// GCP project ID (overrides GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT).
#[arg(long)]
project: Option<String>,
/// Only push secrets whose local key names start with this prefix (case-insensitive).
#[arg(long)]
prefix: Option<String>,
/// Only push secrets in this namespace (stored as `<ns>/KEY`).
#[arg(long)]
ns: Option<String>,
/// Show the diff without writing anything (always exits 0).
#[arg(long)]
dry_run: bool,
/// Skip the confirmation prompt (required in non-TTY / CI contexts).
#[arg(long)]
yes: bool,
/// Also delete remote secrets absent locally within the filtered scope.
/// Off by default — opt-in to avoid accidental mass deletion.
/// Note: GCP Secret Manager deletion requires the Secret Manager Admin API.
#[arg(long)]
delete_missing: bool,
},
/// Import parameters from AWS SSM Parameter Store into the local vault.
///
/// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
/// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
///
/// Region is read from AWS_DEFAULT_REGION / AWS_REGION or --region.
/// Parameters are fetched recursively under the given path.
/// SecureString parameters are decrypted automatically (WithDecryption=true).
///
/// Parameter names are normalised: leading `/` stripped, remaining `/` and `-`
/// become `_`, uppercased (e.g. `/myapp/db-password` → `MYAPP_DB_PASSWORD`).
///
#[command(
after_help = "Examples:\n tsafe ssm-pull --region us-east-1 --path /myapp/prod/\n tsafe ssm-pull --path /shared/ --overwrite"
)]
#[cfg(feature = "cloud-pull-aws")]
SsmPull {
/// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
#[arg(long)]
region: Option<String>,
/// Parameter path prefix (e.g. `/myapp/prod/`). Defaults to `/` (all parameters).
#[arg(long)]
path: Option<String>,
/// Overwrite existing local secrets (skip conflicts by default).
#[arg(long)]
overwrite: bool,
/// Failure handling mode for provider/network errors.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PullOnError,
},
/// Push local vault secrets to AWS Secrets Manager (upsert semantics).
///
/// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
/// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
///
/// Local keys are reverse-normalised to AWS Secrets Manager format:
/// MY_SECRET → my-secret. Two local keys that normalise to the same
/// provider name are detected as a collision and abort pre-flight.
///
/// Remote-only secrets are left untouched unless --delete-missing is passed.
/// A pre-flight diff is always shown before writing. No secret values
/// are printed — only key names and 12-char SHA-256 hash prefixes.
///
#[command(
after_help = "Examples:\n tsafe aws-push --dry-run\n tsafe aws-push --yes\n tsafe aws-push --prefix myapp/ --yes\n tsafe aws-push --delete-missing --yes"
)]
#[cfg(feature = "cloud-pull-aws")]
AwsPush {
/// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
#[arg(long)]
region: Option<String>,
/// Only push secrets whose local key names start with this prefix (case-insensitive).
#[arg(long)]
prefix: Option<String>,
/// Show the diff without writing anything (always exits 0).
#[arg(long)]
dry_run: bool,
/// Skip the confirmation prompt (required in non-TTY / CI contexts).
#[arg(long)]
yes: bool,
/// Also delete remote secrets absent locally within the filtered scope.
/// Off by default — opt-in to avoid accidental mass deletion.
#[arg(long)]
delete_missing: bool,
},
/// Push local vault secrets to AWS SSM Parameter Store (upsert semantics).
///
/// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
/// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
///
/// Local keys are reverse-normalised to SSM parameter names:
/// given `--path /myapp/`, MYAPP_DB_PASSWORD → /myapp/db-password.
///
/// Remote-only parameters are left untouched unless --delete-missing is passed.
/// A pre-flight diff is always shown before writing. No secret values
/// are printed — only key names and 12-char SHA-256 hash prefixes.
///
#[command(
after_help = "Examples:\n tsafe ssm-push --path /myapp/ --dry-run\n tsafe ssm-push --path /myapp/ --yes\n tsafe ssm-push --path /myapp/ --delete-missing --yes"
)]
#[cfg(feature = "cloud-pull-aws")]
SsmPush {
/// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
#[arg(long)]
region: Option<String>,
/// SSM path prefix that scopes the push (e.g. `/myapp/`).
#[arg(long)]
path: Option<String>,
/// Show the diff without writing anything (always exits 0).
#[arg(long)]
dry_run: bool,
/// Skip the confirmation prompt (required in non-TTY / CI contexts).
#[arg(long)]
yes: bool,
/// Also delete remote parameters absent locally within the path scope.
/// Off by default — opt-in to avoid accidental mass deletion.
#[arg(long)]
delete_missing: bool,
},
/// Print a shell completion script and exit.
///
#[command(
after_help = "Examples:\n tsafe completions powershell | Out-String | Invoke-Expression"
)]
Completions {
/// Shell to generate completions for.
shell: Shell,
},
/// Output completion candidates for use by shell completion scripts (internal).
///
/// Called by the patched completion scripts generated by `tsafe completions`.
/// Not intended for direct use.
#[command(name = "_completions-data", hide = true)]
CompletionsData {
/// Type of completion data to emit: `profiles` or `contracts`.
data_type: String,
},
/// Diagnose vault health: file presence, snapshots, env vars, secret expiry, and operator-facing health hints.
#[command(long_about = DOCTOR_LONG_ABOUT, after_help = DOCTOR_AFTER_HELP)]
Doctor {
/// Emit machine-readable JSON and use health exit codes (0=healthy, 1=warning, 2=critical).
#[arg(long)]
json: bool,
},
/// Explain a concept in the terminal (`exec`, namespaces, compiled agent/browser pull lanes, …).
///
/// Omit the topic to list available explanations.
///
#[command(
after_help = "Examples:\n tsafe explain\n tsafe explain exec\n tsafe explain exec-security"
)]
Explain {
/// Topic to print (omit to list all topics).
#[arg(value_name = "TOPIC")]
topic: Option<crate::explain::ExplainTopic>,
},
/// Remove a stale vault lock file (use after a crash leaves the vault locked).
///
/// Deletes `<profile>.vault.lock` if it exists. Safe to run — the lock is
/// advisory only. Use when `tsafe` reports "vault is locked by another process"
/// but no other process is actually running.
///
#[command(after_help = "Examples:\n tsafe unlock\n tsafe --profile prod unlock")]
Unlock,
/// Launch the full-screen interactive terminal UI.
///
/// Supports add/edit/delete/reveal/rotate/snapshot restore and audit log viewing.
/// Press ? inside the TUI for a contextual keyboard reference.
///
#[command(after_help = "Examples:\n tsafe ui\n tsafe --profile prod ui")]
#[cfg(feature = "tui")]
Ui,
/// Render a secret value as a QR code in the terminal.
///
/// Opens the vault, retrieves KEY, prints the QR code to stdout, then waits
/// for Enter before clearing — so the value is never left on-screen.
///
#[command(after_help = "Examples:\n tsafe qr WIFI_PASSWORD\n tsafe qr API_KEY")]
Qr {
/// Secret key whose value to render as a QR code.
key: String,
},
/// Store a TOTP secret and retrieve live codes.
///
/// add: store a TOTP seed for the given key
/// get: compute and print the current 6-digit code
///
#[command(
after_help = "Examples:\n tsafe totp add GITHUB_2FA JBSWY3DPEHPK3PXP\n tsafe totp get GITHUB_2FA"
)]
Totp {
#[command(subcommand)]
action: TotpAction,
},
/// Pin a secret to the top of lists.
///
#[command(
after_help = "Examples:\n tsafe pin DB_PASSWORD\n tsafe --profile prod pin API_KEY"
)]
Pin { key: String },
/// Remove pin from a secret.
///
#[command(after_help = "Examples:\n tsafe unpin DB_PASSWORD")]
Unpin { key: String },
/// Create an alias: ALIAS_NAME resolves to an existing KEY.
///
/// tsafe get ALIAS_NAME returns the value of KEY.
/// Use tsafe alias --list to view all aliases.
///
#[command(
after_help = "Examples:\n tsafe alias DB_PASS DATABASE_PASSWORD\n tsafe alias --list"
)]
Alias {
/// Key this alias should resolve to (omit with --list to view all aliases).
target_key: Option<String>,
/// Name of the alias to create.
alias_name: Option<String>,
/// List all aliases in the vault.
#[arg(long)]
list: bool,
},
/// Replace `{{KEY}}` placeholders in a file with vault secret values.
///
/// Reads the input file, replaces each `{{KEY}}` with the corresponding
/// vault secret, and writes to stdout (or `--output PATH`).
///
#[command(
after_help = "Examples:\n tsafe template config.yml.tmpl > config.yml\n tsafe template app.conf.tmpl --output app.conf"
)]
Template {
/// Input template file containing {{KEY}} placeholders.
file: String,
/// Write output to a file instead of stdout.
#[arg(short, long)]
output: Option<String>,
/// Ignore missing keys instead of failing.
#[arg(long)]
ignore_missing: bool,
},
/// Read stdin and replace any vault secret values with [REDACTED].
///
/// Useful for piping logs through to scrub sensitive values.
///
#[command(
after_help = "Examples:\n cargo test 2>&1 | tsafe redact\n tsafe exec -- myapp | tsafe redact"
)]
Redact,
/// Show the active build profile label and compile-time capabilities.
///
/// This reports the compiled truth for the running `tsafe` binary only.
/// Companion runtimes such as `tsafe-agent` have separate install and release truth.
#[command(name = "build-info", after_help = BUILD_INFO_AFTER_HELP)]
BuildInfo {
/// Emit machine-readable JSON output.
#[arg(long)]
json: bool,
},
#[cfg(feature = "plugins")]
/// Run a tool with its required vault secrets injected automatically.
///
/// Each plugin knows which vault keys map to which environment variables for the
/// named tool. Run `tsafe plugin` (no args) to list available plugins.
///
/// Missing optional keys are silently skipped; missing required keys abort with an error.
///
#[command(
after_help = "Examples:\n tsafe plugin gh repo list\n tsafe plugin aws s3 ls --bucket my-bucket\n tsafe plugin az group list --subscription my-sub\n tsafe plugin (list all available plugins)"
)]
Plugin {
/// Tool name (e.g. gh, aws, az, docker, npm, pypi, terraform). Omit to list.
tool: Option<String>,
/// Arguments to pass to the tool.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Act as a git credential helper (install/get/store/erase protocol).
///
/// Run `tsafe credential-helper install` once to configure git to use tsafe
/// as the credential store. Git will then call `tsafe credential-helper get`,
/// `store`, or `erase` automatically.
///
/// In `get` mode, reads protocol/host from stdin and returns username/password
/// from the vault. Keys are matched by `<HOST>_USERNAME` / `<HOST>_PASSWORD`
/// pattern, or by tags `host=<HOST>`.
#[cfg(feature = "git-helpers")]
#[command(
name = "credential-helper",
after_help = "Examples:\n tsafe credential-helper install\n tsafe credential-helper install --global\n git credential fill # (git calls tsafe automatically after install)"
)]
CredentialHelper {
/// Git credential helper action.
#[arg(value_enum, default_value = "install")]
action: CredentialHelperOperation,
/// For `install`: configure at the --global level (user-wide).
/// By default, configures the local repository git config.
#[arg(long)]
global: bool,
},
/// Collaboration service commands (team membership, DEK delivery, recovery).
///
/// Scaffolding only in Tranche 2 — no network calls are made.
/// Enable with `--features collab`.
///
#[command(
after_help = "Examples:\n tsafe collab join <team-id>\n tsafe collab status <team-id>"
)]
#[cfg(feature = "collab")]
Collab {
#[command(subcommand)]
action: CollabAction,
},
/// Add an SSH key from the vault to the running ssh-agent.
///
/// The key is passed via stdin to `ssh-add -` so it never touches disk.
///
#[command(after_help = "Examples:\n tsafe ssh-add SSH_KEY\n tsafe ssh-add id_ed25519")]
#[cfg(feature = "ssh")]
SshAdd {
/// Vault key name containing the SSH private key.
key: String,
},
/// Import an SSH private key file into the vault.
///
#[command(
after_help = "Examples:\n tsafe ssh-import ~/.ssh/id_ed25519\n tsafe ssh-import ~/.ssh/id_rsa --name SSH_RSA_KEY"
)]
#[cfg(feature = "ssh")]
SshImport {
/// Path to the SSH private key file.
path: String,
/// Vault key name to store under (defaults to filename).
#[arg(long)]
name: Option<String>,
/// Attach tags as KEY=VALUE pairs (repeatable).
#[arg(short, long = "tag", value_name = "KEY=VALUE")]
tags: Vec<String>,
},
/// SSH key inventory and operations.
///
/// Subcommands: list, public-key, generate, config, agent
///
#[command(
after_help = "Examples:\n tsafe ssh list\n tsafe ssh public-key my_ed25519_key\n tsafe ssh generate my_key\n tsafe ssh generate my_key --type rsa\n tsafe ssh config\n eval $(tsafe ssh-agent)"
)]
#[cfg(feature = "ssh")]
Ssh {
#[command(subcommand)]
action: SshAction,
},
/// List namespaces or copy/move all keys under one prefix to another.
///
/// A namespace is any key-prefix of the form "<name>/KEY". They are not stored
/// explicitly — this command introspects the key names in the vault.
///
#[command(
after_help = "Examples:\n tsafe ns list\n tsafe ns copy prod staging\n tsafe ns move oldapp newapp --force"
)]
Ns {
#[command(subcommand)]
action: NsAction,
},
/// Pull secrets from all sources defined in `.tsafe.yml`.
///
/// Searches upward from the current directory for `.tsafe.yml` or `.tsafe.json`
/// and executes each pull source in manifest order (sequential; see ADR-012).
///
/// Use --dry-run to preview which sources would be invoked without making any
/// live API calls. Note: collision detection is not available in dry-run mode —
/// detecting key conflicts requires fetching keys from each provider.
///
/// Use --source to narrow execution to one or more named sources. Sources are
/// named with the `name` field in the manifest. Multiple --source flags are OR'd.
///
#[command(
after_help = "Examples:\n tsafe pull\n tsafe pull --config path/to/.tsafe.yml\n tsafe pull --dry-run\n tsafe pull --source prod-akv\n tsafe pull --source prod-akv --source staging-aws"
)]
#[cfg(feature = "multi-pull")]
Pull {
/// Path to config file (auto-detected if omitted).
#[arg(long)]
config: Option<String>,
/// Overwrite all existing secrets (overrides per-source settings).
#[arg(long)]
overwrite: bool,
/// Failure handling mode for source errors in multi-source pull.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PullOnError,
/// Preview which sources would be invoked without making any live API calls.
/// Collision detection is not available in dry-run mode.
#[arg(long)]
dry_run: bool,
/// Narrow execution to sources with this `name` label (repeatable).
/// Sources without a `name` field are excluded when any --source filter is active.
#[arg(long = "source", value_name = "LABEL", action = clap::ArgAction::Append)]
sources: Vec<String>,
},
/// Push local vault secrets to all destinations defined in `.tsafe.yml`.
///
/// Searches upward from the current directory for `.tsafe.yml` or `.tsafe.json`
/// and executes each push destination in manifest order (sequential; see ADR-030).
///
/// Use --dry-run to preview which destinations would be invoked without making
/// any live API calls or writes.
///
/// Use --source to narrow execution to one or more named destinations. Destinations
/// are named with the `name` field in the manifest. Multiple --source flags are OR'd.
///
/// A pre-flight diff is shown before any writes. Secret values are never printed —
/// only key names and 12-char SHA-256 hash prefixes are shown (ADR-030).
///
#[command(
after_help = "Examples:\n tsafe push\n tsafe push --config path/to/.tsafe.yml\n tsafe push --dry-run\n tsafe push --source prod-akv\n tsafe push --yes\n tsafe push --on-error skip-failed"
)]
#[cfg(feature = "akv-pull")]
Push {
/// Path to config file (auto-detected if omitted).
#[arg(long, value_name = "PATH")]
config: Option<std::path::PathBuf>,
/// Narrow execution to destinations with this `name` label (repeatable).
/// Destinations without a `name` field are excluded when any --source filter is active.
#[arg(long = "source", value_name = "LABEL", action = clap::ArgAction::Append)]
source: Vec<String>,
/// Show the diff without writing anything (always exits 0).
#[arg(long)]
dry_run: bool,
/// Skip confirmation prompts (required in non-TTY / CI contexts).
#[arg(long)]
yes: bool,
/// Also delete remote secrets that are absent locally within each destination's scope.
/// Off by default — opt-in to avoid accidental mass deletion (ADR-030).
#[arg(long)]
delete_missing: bool,
/// Failure handling mode for destination errors.
#[arg(long, value_enum, default_value = "fail-all")]
on_error: PushOnError,
},
/// Synchronise a vault file with a git remote.
///
/// Fetches the remote branch, performs a per-key three-way merge between
/// the common ancestor, the local vault, and the remote vault, then commits
/// and pushes the merged result.
///
/// Conflicts (both sides edited the same key) are resolved by last-write-wins
/// using the secret's `updated_at` timestamp. Conflicts are reported but do
/// not block the sync.
///
#[command(
after_help = "Examples:\n tsafe sync\n tsafe sync --remote origin --branch main\n tsafe sync --dry-run"
)]
#[cfg(feature = "git-helpers")]
#[command(name = "sync")]
Sync {
/// Git remote name.
#[arg(long, default_value = "origin")]
remote: String,
/// Git branch to sync with.
#[arg(long, default_value = "main")]
branch: String,
/// Vault file path relative to repo root (auto-detected if omitted).
#[arg(long)]
file: Option<String>,
/// Show what would change without modifying anything.
#[arg(long)]
dry_run: bool,
},
/// Manage team vaults (multi-recipient age encryption).
///
/// Team vaults use X25519 (age) keypairs so multiple people can decrypt
/// the same vault without sharing a password.
///
#[command(
after_help = "Examples:\n tsafe team init --identity ~/.age/key.txt\n tsafe team add-member age1qyqszqgpqyqszqgpqyqszqgpqyqszqgp...\n tsafe team members"
)]
#[cfg(feature = "team-core")]
Team {
#[command(subcommand)]
action: TeamAction,
},
/// Enable or disable biometric / keyring unlock for the current profile.
///
/// When enabled, the vault password is stored in the OS credential store
/// (macOS Keychain, Windows Credential Manager, Linux Secret Service).
/// The credential store is itself protected by biometric or PIN.
///
/// After `tsafe init`, the CLI may offer the same setup interactively ("quick unlock").
/// You can always run `biometric enable` later if you skipped it.
///
#[command(
after_help = "Examples:\n tsafe biometric enable\n tsafe biometric disable\n tsafe biometric status"
)]
#[cfg(feature = "biometric")]
Biometric {
#[command(subcommand)]
action: BiometricAction,
},
/// Manage the per-process vault unlock agent.
///
/// `tsafe agent unlock` prints terminal approval text, may show an OS notification,
/// then prompts for the vault password once and starts a background agent that holds
/// it in memory. The token it prints must be set in the calling process's environment
/// as `TSAFE_AGENT_SOCK` — all subsequent `tsafe` invocations that inherit that
/// env var will be granted vault access without re-entering the password.
///
/// Requests must present the session token and come from a live OS-reported peer
/// PID; the unlock process PID is recorded for audit/context, not as the only
/// process allowed to use the session.
///
#[command(
after_help = "Examples:\n tsafe agent unlock # unlock for 30 minutes (default)\n tsafe agent unlock --ttl 8h # unlock for 8 hours\n tsafe agent unlock --ttl 30m --absolute-ttl 8h\n tsafe agent status # check whether the current agent socket is reachable\n tsafe agent lock # immediately revoke the session"
)]
#[cfg(feature = "agent")]
Agent {
#[command(subcommand)]
action: AgentAction,
},
/// Run a git command with vault credentials injected automatically.
///
/// Opens the vault, reads `ADO_PAT` (or the key named by TSAFE_GIT_PAT_KEY),
/// and injects it as a git `http.extraHeader` so HTTPS remotes authenticate
/// without embedding tokens in URLs.
///
/// Detects the nearest `.git` directory automatically — no repo flags needed.
/// Exits with git's exit code.
///
/// Override the PAT key name: $env:TSAFE_GIT_PAT_KEY = "MY_GIT_PAT"
///
#[command(
after_help = "Examples:\n tsafe git push ado main\n tsafe git pull\n tsafe git fetch --all\n tsafe -p work git push origin main"
)]
#[cfg(feature = "git-helpers")]
#[command(name = "git")]
Git {
/// git subcommand and its arguments (e.g. `push ado main`).
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}
/// Collab subcommands — scaffolding for D3.5 (Tranche 2).
/// No network calls in this release; real implementation is Tranche 3+.
#[derive(Subcommand)]
#[cfg(feature = "collab")]
pub enum CollabAction {
/// Join a collaboration team (scaffolding — prints status and exits 0).
Join {
/// Team ID to join.
team_id: String,
},
/// Show collaboration status for a team (scaffolding — prints status and exits 0).
Status {
/// Team ID to query.
team_id: String,
},
}
#[derive(Subcommand)]
pub enum SnapshotAction {
/// List snapshots for the current profile.
List,
/// Restore the most-recent snapshot, overwriting the current vault.
Restore,
}
/// Global config.json settings (not tied to `--profile`).
#[derive(Subcommand)]
pub enum ConfigAction {
/// Show config file path, default profile, exec trust settings, and password-backup settings.
Show,
/// After each new password vault is created, copy its master password into this vault at `profile-passwords/<new-profile>` (recovery / main-vault bridging). Common values: `main`, `default`. Use `off` to disable.
#[command(name = "set-backup-vault")]
SetBackupVault {
/// Target vault profile (`main`, `default`) or `off` to clear.
target: String,
},
/// Persist whether normal vault opens should automatically try OS quick unlock.
#[command(name = "set-auto-quick-unlock")]
SetAutoQuickUnlock {
/// `on` to allow automatic keychain reads, `off` to require agent / env / typed password instead.
mode: ToggleSetting,
},
/// Persist the retry cooldown, in seconds, after an automatic quick-unlock failure.
#[command(name = "set-quick-unlock-retry-cooldown")]
SetQuickUnlockRetryCooldown {
/// Seconds to wait before the next automatic keychain attempt. Use `0` to disable the cooldown.
seconds: u64,
},
/// Persist the default exec trust mode.
#[command(name = "set-exec-mode")]
SetExecMode {
/// One of: `standard`, `hardened`, `custom`.
mode: ExecModeSetting,
},
/// Persist whether `tsafe exec` should redact child stdout/stderr by default.
#[command(name = "set-exec-redact-output")]
SetExecRedactOutput {
/// `on` to redact child output by default, `off` to leave it raw unless `--redact-output` is passed.
mode: ToggleSetting,
},
/// Persist the inherit strategy used when exec mode is `custom`.
#[command(name = "set-exec-custom-inherit")]
SetExecCustomInherit {
/// One of: `full`, `minimal`, `clean`.
mode: ExecCustomInheritSetting,
},
/// Persist whether dangerous injected env names should abort exec when mode is `custom`.
#[command(name = "set-exec-custom-deny-dangerous-env")]
SetExecCustomDenyDangerousEnv {
/// `on` to abort, `off` to warn only.
mode: ToggleSetting,
},
/// Add a parent environment variable name to the extra strip list for `tsafe exec`.
#[command(name = "add-exec-extra-strip")]
AddExecExtraStrip {
/// Environment variable name, e.g. OPENAI_API_KEY.
name: String,
},
/// Remove a parent environment variable name from the extra strip list for `tsafe exec`.
#[command(name = "remove-exec-extra-strip")]
RemoveExecExtraStrip {
/// Environment variable name, e.g. OPENAI_API_KEY.
name: String,
},
}
#[derive(Clone, Copy, ValueEnum)]
pub enum ToggleSetting {
/// Enable the setting.
On,
/// Disable the setting.
Off,
}
#[derive(Clone, Copy, ValueEnum)]
pub enum ExecModeSetting {
/// Broad compatibility: full inherited env (minus strip list), raw output, and abort on dangerous injected names by default.
Standard,
/// Stricter preset: minimal inherited env, redacted output, and deny dangerous injected names.
Hardened,
/// Use persisted custom exec trust settings from config.json.
Custom,
}
/// Controls which host environment variables the child process inherits.
#[derive(Clone, Copy, ValueEnum)]
pub enum ExecPresetSetting {
/// Inherit only PATH and a safe core set (HOME, USER, TMPDIR, LANG, TERM, SSH_AUTH_SOCK, etc.)
/// plus vault secrets. No tokens or credentials from the parent environment leak through.
Minimal,
/// Inherit the full parent environment minus the known-sensitive strip list. This is the
/// current default behavior when no preset or inheritance flag is given.
Full,
}
#[derive(Clone, Copy, ValueEnum)]
pub enum ExecCustomInheritSetting {
/// Full inherited parent env (minus strip list).
Full,
/// Minimal inherited env plus vault secrets.
Minimal,
/// No inherited parent env; only vault secrets.
Clean,
}
#[derive(Subcommand)]
pub enum ProfileAction {
/// List all profiles that have an existing vault.
List,
/// Permanently delete a profile vault.
Delete {
name: String,
/// Skip the confirmation prompt.
#[arg(long)]
force: bool,
},
/// Set the default profile used when -p / TSAFE_PROFILE is not specified.
///
#[command(after_help = "Examples:\n tsafe profile set-default work")]
SetDefault {
/// Profile name to use as the new default.
name: String,
},
/// Rename a profile (renames the vault file and updates the default if needed).
///
#[command(after_help = "Examples:\n tsafe profile rename old new")]
Rename {
/// Existing profile name.
from: String,
/// New profile name.
to: String,
},
}
#[derive(Clone, ValueEnum)]
pub enum ExportFormat {
/// KEY=VALUE (posix, one per line)
Env,
/// export KEY="VALUE" (bash/zsh source-able)
Dotenv,
/// $env:KEY = "VALUE" (PowerShell source-able)
Powershell,
/// JSON object
Json,
/// ::add-mask::VALUE + KEY=VALUE (GitHub Actions GITHUB_ENV format)
GithubActions,
/// YAML mapping (KEY: "VALUE" per entry)
Yaml,
/// KEY=VALUE per line suitable for Docker --env-file (alias for env, Docker-compatible)
DockerEnv,
/// TOML flat top-level table (KEY = "VALUE" per entry)
Toml,
}
#[derive(Clone, ValueEnum)]
pub enum AuditExportFormat {
/// JSONL (one JSON object per line, same as stored on disk)
Json,
/// Splunk HEC-compatible JSON events
Splunk,
/// CloudEvents 1.0 JSONL (application/cloudevents+json per line)
CloudEvents,
}
/// Actions for `tsafe audit` subcommand.
///
/// `Rotate` is a stub reserved for the audit-log rotation handler implemented
/// in `cmd_audit_cmd.rs` by a separate agent. The variant is declared here so
/// that `cli.rs` is the single source of truth for the CLI surface.
#[derive(Subcommand)]
#[allow(dead_code)]
pub enum AuditAction {
/// Rotate (trim) the audit log to keep only the most-recent entries.
///
/// Reserved — handler implemented in `cmd_audit_cmd.rs`.
#[command(
after_help = "Examples:\n tsafe audit rotate --keep 1000\n tsafe audit rotate --max-size-mb 10"
)]
Rotate {
/// Maximum audit log size in megabytes before trimming.
#[arg(long, default_value_t = 50)]
max_size_mb: u64,
/// Number of most-recent entries to keep after trimming.
#[arg(long, default_value_t = 5000)]
keep: u32,
},
}
#[derive(Clone, Copy, ValueEnum)]
pub enum CredentialHelperOperation {
/// Install tsafe as the git credential helper in git config.
Install,
Get,
Store,
Erase,
}
#[cfg(feature = "ssh")]
#[derive(Subcommand)]
pub enum SshAction {
/// List SSH keys stored in the vault (tagged type=ssh or containing PRIVATE KEY).
#[command(after_help = "Examples:\n tsafe ssh list")]
List,
/// Extract the public key from a stored SSH private key.
///
/// Prints the OpenSSH public key in authorized_keys format to stdout.
#[command(
name = "public-key",
after_help = "Examples:\n tsafe ssh public-key my_ed25519_key\n tsafe ssh public-key SSH_ID_ED25519"
)]
PublicKey {
/// Vault key name containing the SSH private key.
key: String,
},
/// Generate a new SSH key pair and store the private key in the vault.
///
/// Uses a CSPRNG (no subprocess). The private key is stored encrypted in
/// the vault; the public key is printed to stdout.
#[command(
after_help = "Examples:\n tsafe ssh generate my_deploy_key\n tsafe ssh generate my_rsa_key --type rsa --bits 4096\n tsafe ssh generate ci_key --comment \"ci@example.com\" --print"
)]
Generate {
/// Vault key name to store the generated private key under.
key: String,
/// Key type: ed25519 (default, recommended) or rsa.
#[arg(long, value_name = "TYPE", default_value = "ed25519")]
r#type: SshKeyType,
/// RSA key size in bits (only used with --type rsa; default 4096).
#[arg(long, value_name = "BITS", default_value = "4096")]
bits: u32,
/// Comment to embed in the key (e.g. an email address).
#[arg(long, value_name = "COMMENT")]
comment: Option<String>,
/// Print the public key to stdout after storing the private key.
#[arg(long)]
print: bool,
},
/// Print an ~/.ssh/config snippet that points IdentityAgent at tsafe.
///
/// Pipe or append the output to ~/.ssh/config.
#[command(
name = "config",
after_help = "Examples:\n tsafe ssh config\n tsafe ssh config --host '*.corp.example'\n tsafe ssh config >> ~/.ssh/config"
)]
Config {
/// SSH Host pattern (defaults to `*`).
#[arg(long, value_name = "PATTERN")]
host: Option<String>,
},
/// Start a persistent SSH agent serving vault keys on a Unix socket.
///
/// Keys are loaded once at startup and served for the configured TTL.
/// On Windows this subcommand prints a clear error — Unix socket required.
///
/// Eval idiom: eval $(tsafe ssh-agent)
#[command(
name = "agent",
after_help = "Examples:\n eval $(tsafe ssh agent)\n tsafe ssh agent --ttl 4h\n tsafe ssh agent --sock /run/user/1000/tsafe.sock"
)]
Agent {
/// How long loaded keys remain valid (e.g. 8h, 30m, 1h30m). Default 8h.
#[arg(long, value_name = "DURATION")]
ttl: Option<String>,
/// Override the Unix socket path.
#[arg(long, value_name = "PATH")]
sock: Option<String>,
},
}
#[cfg(feature = "ssh")]
#[derive(Clone, Copy, ValueEnum)]
pub enum SshKeyType {
Ed25519,
Rsa,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum PullOnError {
/// Abort immediately on first source/provider error.
FailAll,
/// Skip failed source and continue remaining sources.
SkipFailed,
/// Continue and only warn on source/provider errors.
WarnOnly,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum PushOnError {
/// Abort immediately on first source error.
FailAll,
/// Log the error, skip the failed source, and continue with the next.
SkipFailed,
}
#[derive(Subcommand)]
pub enum BrowserProfileAction {
/// Add or update a domain → vault-profile mapping.
///
/// DOMAIN may be an exact hostname or a wildcard pattern (e.g. *.corp.example).
/// Defaults to the active --profile if --profile is omitted.
///
#[command(
after_help = "Examples:\n tsafe browser-profile add github.com\n tsafe browser-profile add corp.example --profile work"
)]
Add {
/// Domain or pattern (e.g. github.com, *.corp.example).
domain: String,
/// Vault profile to use for this domain. Defaults to the active --profile.
#[arg(long)]
profile: Option<String>,
},
/// List all domain → vault-profile mappings.
///
#[command(after_help = "Examples:\n tsafe browser-profile list")]
List,
/// Remove the mapping for DOMAIN.
///
#[command(after_help = "Examples:\n tsafe browser-profile remove github.com")]
Remove {
/// Domain or pattern to remove.
domain: String,
},
}
#[derive(Subcommand)]
pub enum BrowserNativeHostAction {
/// Write the per-browser native-messaging-host manifest pointing at
/// `tsafe-nativehost`. Per-user; never elevates. On Windows it writes
/// Chromium-family HKCU registry keys for a 32-char extension ID, or a
/// Firefox filesystem manifest under `%APPDATA%\Mozilla\NativeMessagingHosts\`
/// for an email-style or UUID-style Firefox addon ID. On macOS/Linux it
/// skips browsers that are not installed.
///
/// `--extension-id` is REQUIRED — defaulting to a known development ID
/// would let any installed extension with that ID talk to your vault.
/// Chromium ID: 32-char string at `chrome://extensions` (Developer mode).
/// Firefox ID: the `gecko.id` value from `browser_specific_settings` in
/// the extension manifest (e.g. `tsafe@tsafe.dev`).
Register {
/// The extension ID to allow. Chromium-family: 32-character lowercase
/// ID from `chrome://extensions`. Firefox: email-style addon ID
/// (e.g. `tsafe@tsafe.dev`) or UUID in curly braces.
#[arg(long)]
extension_id: String,
},
/// Remove the per-browser manifest files (and HKCU keys on Windows).
Unregister,
/// Detect the native-host binary location and print the manifest paths that
/// `register` would write for each installed browser — without writing
/// anything. Use this when you do not yet know your extension ID:
///
/// 1. Run `tsafe browser-native-host detect` to confirm the binary is
/// found and see which browsers are detected.
/// 2. Load the extension in your browser, find your extension ID at
/// `chrome://extensions` (Developer mode), then run:
/// tsafe browser-native-host register --extension-id <id>
///
/// On Windows, prints the HKCU registry keys and manifest directory that
/// would be written; never modifies registry or filesystem.
Detect,
}
#[derive(Clone, Copy, ValueEnum)]
pub enum TotpAlgorithm {
/// HMAC-SHA1 (default; most compatible)
Sha1,
/// HMAC-SHA256
Sha256,
/// HMAC-SHA512
Sha512,
}
impl TotpAlgorithm {
pub fn as_uri_str(self) -> &'static str {
match self {
Self::Sha1 => "SHA1",
Self::Sha256 => "SHA256",
Self::Sha512 => "SHA512",
}
}
}
#[derive(Subcommand)]
pub enum TotpAction {
/// Store a TOTP seed for KEY. Accepts a raw base32 secret or an otpauth:// URI.
///
#[command(
after_help = "Examples:\n tsafe totp add GITHUB_2FA JBSWY3DPEHPK3PXP\n tsafe totp add CORP_2FA JBSWY3DPEHPK3PXP --digits 8 --period 60\n tsafe totp add CORP_2FA JBSWY3DPEHPK3PXP --algorithm sha256"
)]
Add {
/// Vault key name to store under.
key: String,
/// Base32-encoded TOTP secret or full otpauth:// URI.
secret: String,
/// HMAC algorithm to use (default: sha1, most widely supported).
#[arg(long, default_value = "sha1")]
algorithm: TotpAlgorithm,
/// Number of digits in each OTP code (default: 6; some services use 8).
#[arg(long, default_value_t = 6)]
digits: u32,
/// Time step in seconds (default: 30; some services use 60).
#[arg(long, default_value_t = 30)]
period: u64,
},
/// Print the current TOTP code + seconds remaining.
///
#[command(after_help = "Examples:\n tsafe totp get GITHUB_2FA")]
Get {
/// Vault key name where the TOTP seed is stored.
key: String,
},
}
#[derive(Subcommand)]
pub enum NsAction {
/// List all namespaces present in the vault (inferred from key prefixes).
List,
/// Copy every secret under FROM/ to TO/ (same suffix). Source keys stay.
///
#[command(after_help = "Examples:\n tsafe ns copy prod staging")]
Copy {
/// Namespace prefix to read from (keys must be `FROM/...`).
from: String,
/// Namespace prefix to write to (`TO/<same-suffix>`).
to: String,
/// Overwrite destination keys if they already exist.
#[arg(long)]
force: bool,
},
/// Move every secret under FROM/ to TO/ (same suffix). Source keys are removed.
///
#[command(after_help = "Examples:\n tsafe ns move prod staging")]
Move {
/// Namespace prefix to read from and delete after rename.
from: String,
/// Namespace prefix to write to (`TO/<same-suffix>`).
to: String,
/// Overwrite destination keys if they already exist.
#[arg(long)]
force: bool,
},
}
#[derive(Subcommand)]
pub enum AgentAction {
/// Prompt for approval + vault password, then start the background agent daemon.
///
/// Prints a shell export line to stdout:
/// $env:TSAFE_AGENT_SOCK = "..." # PowerShell
/// export TSAFE_AGENT_SOCK="..." # bash/zsh
///
/// Copy-paste or eval this line in the calling shell/process that needs access.
Unlock {
/// Idle TTL — how long the agent stays alive without a vault request.
/// Resets on each vault access. Common values include 15m, 1h, and 4h. Default: 30m.
#[arg(long, default_value = "30m")]
ttl: String,
/// Absolute TTL — hard cap regardless of activity. Default: 8h.
/// Must be >= idle TTL. Common values include 8h, 12h, and 24h.
#[arg(long, default_value = "8h")]
absolute_ttl: String,
},
/// Immediately revoke the current session and stop the agent.
Lock,
/// Show whether the current agent socket is reachable.
///
/// Use `--json` for a stable machine-readable output (ADR-029). Consumers such
/// as the VS Code extension and tray agent depend on this flag. The schema
/// `version` field must be checked before reading any other field.
#[command(
after_help = "Examples:\n tsafe agent status\n tsafe agent status --json\n tsafe --profile prod agent status --json"
)]
Status {
/// Emit a stable JSON object to stdout (schema version \"1\").
/// See docs/decisions/agent-status-json-contract.md (ADR-029) for the full schema.
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand)]
pub enum TeamAction {
/// Create a new team vault encrypted to your age identity.
///
#[command(after_help = "Examples:\n tsafe team init --identity ~/.age/key.txt")]
Init {
/// Path to your age identity file (contains AGE-SECRET-KEY-1...).
#[arg(long)]
identity: String,
},
/// Add a team member by their age public key.
///
#[command(after_help = "Examples:\n tsafe team add-member age1qyqszqgp...")]
AddMember {
/// age X25519 public key (starts with "age1...").
public_key: String,
/// Path to your age identity file (for re-wrapping the DEK).
#[arg(long)]
identity: String,
},
/// Remove a team member and re-encrypt all secrets with a new key.
///
#[command(after_help = "Examples:\n tsafe team remove-member age1qyqszqgp...")]
RemoveMember {
/// age X25519 public key to remove.
public_key: String,
/// Path to your age identity file (for re-keying).
#[arg(long)]
identity: String,
},
/// List current team members (public keys).
Members,
/// Generate a new age identity (keypair) and print the JSON block to add
/// to `.tsafe/team-keys.json` via a PR.
///
/// The private key is saved to `~/.age/tsafe-<profile>.txt`.
/// The public key is printed as a ready-to-paste JSON entry.
///
#[command(
after_help = "Examples:\n tsafe team keygen\n tsafe team keygen --name \"Alice Smith\" --email alice@corp.example"
)]
Keygen {
/// Your display name for the team-keys entry.
#[arg(long)]
name: Option<String>,
/// Your email for the team-keys entry.
#[arg(long)]
email: Option<String>,
},
/// Print your age public key from an existing identity file.
///
#[command(
after_help = "Examples:\n tsafe team show-key\n tsafe team show-key --identity ~/.age/key.txt"
)]
ShowKey {
/// Path to identity file (default: ~/.age/tsafe-<profile>.txt).
#[arg(long)]
identity: Option<String>,
},
/// Reconcile vault recipients with `.tsafe/team-keys.json`.
///
/// Adds any new members found in the keys file. Removes members no longer
/// listed. Re-keys the vault if the member list changed.
///
#[command(after_help = "Examples:\n tsafe team sync-keys --identity ~/.age/key.txt")]
SyncKeys {
/// Path to your age identity file (required for re-wrapping the DEK).
#[arg(long)]
identity: String,
/// Path to team-keys.json (auto-detected if omitted).
#[arg(long)]
keys_file: Option<String>,
},
}
#[derive(Subcommand)]
pub enum BiometricAction {
/// Store the vault password in the OS credential store (same as accepting quick unlock after `tsafe init`).
Enable,
/// Remove the vault password from the OS credential store.
Disable,
/// Check if biometric/keyring unlock is configured for this profile.
Status,
/// Re-enroll biometric/keyring unlock after a stale-credential error.
///
/// Use this when `tsafe` reports "stale biometric credential" — for example after
/// rotating the vault password (`tsafe rotate`) or after enrolling a new fingerprint.
/// This is equivalent to `tsafe biometric disable` followed by `tsafe biometric enable`
/// but makes the recovery intent explicit and prints a confirmation.
#[command(name = "re-enroll")]
ReEnroll,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
fn has_subcommand(command: &clap::Command, name: &str) -> bool {
command
.get_subcommands()
.any(|subcommand| subcommand.get_name() == name)
}
#[test]
fn root_command_visibility_matches_feature_gates() {
let command = Cli::command();
assert_eq!(has_subcommand(&command, "ui"), cfg!(feature = "tui"));
assert_eq!(
has_subcommand(&command, "kv-pull"),
cfg!(feature = "akv-pull")
);
assert_eq!(
has_subcommand(&command, "aws-pull"),
cfg!(feature = "cloud-pull-aws")
);
assert_eq!(
has_subcommand(&command, "gcp-pull"),
cfg!(feature = "cloud-pull-gcp")
);
assert_eq!(
has_subcommand(&command, "gcp-push"),
cfg!(feature = "cloud-pull-gcp")
);
assert_eq!(
has_subcommand(&command, "ssm-pull"),
cfg!(feature = "cloud-pull-aws")
);
assert_eq!(
has_subcommand(&command, "aws-push"),
cfg!(feature = "cloud-pull-aws")
);
assert_eq!(
has_subcommand(&command, "ssm-push"),
cfg!(feature = "cloud-pull-aws")
);
assert_eq!(
has_subcommand(&command, "vault-pull"),
cfg!(feature = "cloud-pull-vault")
);
assert_eq!(
has_subcommand(&command, "op-pull"),
cfg!(feature = "cloud-pull-1password")
);
assert_eq!(
has_subcommand(&command, "pull"),
cfg!(feature = "multi-pull")
);
assert_eq!(
has_subcommand(&command, "share-once"),
cfg!(feature = "ots-sharing")
);
assert_eq!(
has_subcommand(&command, "receive-once"),
cfg!(feature = "ots-sharing")
);
assert_eq!(
has_subcommand(&command, "browser-profile"),
cfg!(feature = "browser")
);
assert_eq!(
has_subcommand(&command, "browser-native-host"),
cfg!(feature = "nativehost")
);
assert_eq!(has_subcommand(&command, "ssh-add"), cfg!(feature = "ssh"));
assert_eq!(
has_subcommand(&command, "ssh-import"),
cfg!(feature = "ssh")
);
assert_eq!(
has_subcommand(&command, "plugin"),
cfg!(feature = "plugins")
);
assert_eq!(
has_subcommand(&command, "hook-install"),
cfg!(feature = "git-helpers")
);
assert_eq!(
has_subcommand(&command, "git"),
cfg!(feature = "git-helpers")
);
assert_eq!(
has_subcommand(&command, "sync"),
cfg!(feature = "git-helpers")
);
assert_eq!(
has_subcommand(&command, "credential-helper"),
cfg!(feature = "git-helpers")
);
assert_eq!(
has_subcommand(&command, "biometric"),
cfg!(feature = "biometric")
);
assert_eq!(
has_subcommand(&command, "team"),
cfg!(feature = "team-core")
);
assert_eq!(has_subcommand(&command, "agent"), cfg!(feature = "agent"));
}
}
#[derive(Subcommand)]
pub enum PolicyAction {
/// Set a rotation policy on a secret.
///
#[command(after_help = "Examples:\n tsafe policy set DB_PASSWORD --rotate-every 90d")]
Set {
/// Secret key.
key: String,
/// Rotation interval (e.g. 90d, 30d, 7d).
#[arg(long)]
rotate_every: String,
},
/// Remove the rotation policy from a secret.
///
#[command(after_help = "Examples:\n tsafe policy remove DB_PASSWORD")]
Remove {
/// Secret key.
key: String,
},
}