Skip to content

CLI Reference

app/env/provider are always --options that fall back to the context saved by raaz init/raaz ctx use - the one exception is init itself, which establishes that context for the first time and has nothing to fall back to. raaz use <env> is the one positional shortcut, kept because switching environments is the common case.

Global flags

raaz --version           # print the installed version and exit
raaz --debug <command>   # show detailed/debug logging for this run
raaz --no-log <command>  # disable the local audit log for this run (see Audit log below)

Output is quiet by default - --debug (placed before the subcommand) surfaces internal logging that's otherwise suppressed, including from the underlying cloud SDKs (e.g. OCI's own connection-setup logging).

Context

raaz init <app> <env> <provider> [--dir <name>] [--force] [--push] [--keep-existing]  # establish context (positional - no fallback yet)
raaz use <env> [--app] [--provider] [--force] [--push]                                # shortcut for `ctx use --env <env>`

raaz ctx use [--app] [--env] [--provider] [--dir <name>] [--force] [--push] [--keep-existing]  # general form; omitted fields keep their saved value
raaz ctx show [--plain]                                                                        # print the current context
raaz ctx clear                                                                                 # forget the saved context

raaz init

raaz init myapp dev aws
# On a true first init (nothing saved yet), this also prompts:
#   Store .env files under .raaz/ instead of the project root? [y/N]:

By default, raaz init leaves your .env files exactly where most projects already keep them - the project root. Answering yes (or passing --dir directly) is what opts you into a .raaz/<name>/ subfolder instead.

What happens, in order:

  1. Validates the provider name first, before writing anything - an unknown provider fails cleanly with Unknown provider '<name>'. Installed: ... and leaves no .raaz/context.json behind, rather than writing the context and only then discovering the provider doesn't exist.
  2. If re-initializing over an already-saved context that differs from the new one, runs the same unpushed-local-changes safety check ctx use does (see below) - --force skips it.
  3. Resolves --dir - the subfolder under .raaz/ holding this project's .env files, if any. On a true first init (no saved context yet) and no --dir given, asks whether to opt into a subfolder at all (default no - keep the project root); saying yes then prompts for a name (default dots, i.e. .raaz/dots/). Re-initializing keeps whatever was already saved unless --dir overrides it. A non-interactive session (no TTY to answer the confirm) also defaults to the project root, same as declining.
  4. If a dir ended up set, offers to move any .env-style file already sitting in the project root into that folder - handles the common case of a project that already has a .env at its root from before you opted into a subfolder. Lists what it found, one per line, and asks (default yes). If a same-named file already exists at the destination, that's asked about separately, per file (default: skip, since overwriting is the more destructive option) - so one run can overwrite some names and leave others alone. --force skips every ask (the move itself, and any conflict) and overwrites conflicts outright - consistent with every other --force in this CLI (skip the confirmation, proceed with the action - now including overwriting). Pass --keep-existing too if you don't want that: it always leaves a conflicting file alone instead, whether or not --force/a terminal is present - --dir --force --keep-existing is the "safe sync" combination for unattended CI (move what's new, never touch what's already there, no prompts either way). Without --force, a non-interactive session (no TTY to answer the prompt) skips the whole thing too, but leaves the files in place instead, since nobody actually said yes - so a repo with a committed .env.example at the root doesn't abort a CI run just because there was nobody to answer it, but it also won't silently reorganize files a script never asked for. --force is the one to rely on explicitly in scripts that do want the move: TTY auto-detection is a bonus safety net, not guaranteed on every platform - it's known to report a TTY even for closed/redirected stdin on Windows.
  5. Writes .raaz/context.json, then ensures the project's .gitignore excludes .raaz/ - creating .gitignore if it doesn't exist, or appending a .raaz/ line if nothing already there covers it (an existing .raaz/.raaz///.raaz//.raaz/ line counts as already covered). No-ops silently, every time, once it's there - safe on every init/re-init, not just the first. .raaz/ holds real project state (backups, and your actual .env files) that should never end up committed.
  6. Loads that provider's provider.env into the process environment, and prints the resulting app/env/provider/dir table.

raaz context ... is a hidden alias of raaz ctx .... ctx show --plain prints just app/env/provider on one bare line (nothing if no context is set) instead of the decorated table - meant for scripting/embedding, see Shell prompt integration below.

raaz ctx use / raaz use

Before actually switching, ctx use/use check whether any local .env-style file differs from what's currently pushed under the context you're switching away from (or was never pushed at all). The warning reports these as two separate lines - files never pushed at all, and files edited since they were last pushed - rather than one blanket "differs," since a single combined line reads ambiguously (easy to misread as comparing against the context you're switching to, which isn't what's happening - nothing about the new provider is checked here at all). It then asks whether to push these to the old context right now before continuing (default yes - this is the safe, recommended choice, since it means nothing stays stranded). Declining that - or the push itself failing - falls back to asking whether to continue anyway, leaving them unpushed (default no); declining that aborts the switch entirely. So there are three outcomes: push and continue, leave them unpushed and continue, or abort.

--push answers the first question automatically instead of asking - for a CI script that wants the safe path without a human present. If the push itself then fails, it aborts immediately rather than falling back to the second question, which would just hang forever with no TTY to answer it. This is a different, and complementary, kind of "unattended" to --force: --force skips this whole check so nothing local is ever touched; --push still runs the check, it just settles the one real decision on its own instead of prompting. --force and --push can't be used together - since --force already skips this check entirely, --push would never get a chance to do anything, so the combination is rejected outright (exit code 1) rather than silently letting --force win.

Any local file that's already identical to what's pushed for the old context - whether it always was, or just got pushed by the step above - gets cleared automatically once the check passes, backed up first into .raaz/backups/ (recoverable with raaz rollback) the same way pull already protects files before overwriting them. Without this, a .env left over from the context you just switched away from just sits there: silently readable by your app even though raaz now thinks you're somewhere else, or liable to get accidentally re-pushed to the new context if you run push before pulling. Only files provably safe to lose are ever touched this way - anything never pushed or edited since its last push is left alone completely, confirmed past the warning or not, since there'd be no way to recover it. --force skips this whole check - the warning and the auto-clear together, not just the confirmation prompt - so scripts that already rely on --force meaning "switch immediately, touch nothing else" keep that guarantee. Nothing runs on your very first ctx use (there's no prior context yet to compare against), and a check that can't reach the old provider (e.g. stale credentials) fails open rather than blocking the switch or clearing anything - "can't verify" is never treated as "safe to delete". init runs this exact same check when re-initializing (step 2 above), for the same reason.

Passing --dir <name> makes the same offer init does (including --keep-existing for conflicts above): it lists any .env-style file sitting at the project root, and asks before moving it into the new folder - even if <name> matches what's already saved, since a file can land back at the project root after dir's already configured (a fresh clone, habit, a copy-paste). Renaming to a genuinely different folder also checks the old one for anything left there. --force skips every confirm and overwrites conflicts outright; add --keep-existing for the CI-safe version that never overwrites. Declining still saves the new --dir value - only the file move itself is skipped. Nothing happens here at all if --dir isn't passed on that command.

init and ctx use both load that provider's ~/.raaz/providers/<provider>/provider.env into the process environment as a side effect, so credentials are ready as soon as you switch context.

Shell prompt integration

raaz shell init --shell powershell|bash|zsh [--profile-path]

Adds the current raaz context to your shell prompt, e.g. [myapp/dev/aws] PS D:\project> - since switching context (raaz ctx use) doesn't change anything visible otherwise, this is the fix for losing track of which app/env/provider you're pointed at. It appends a clearly-marked block to your shell's profile/rc file (auto-detected per shell - $PROFILE for PowerShell, asked from an actual pwsh/powershell binary since the path differs by version and OS; ~/.bash_profile on macOS or ~/.bashrc on Linux for bash; ~/.zshrc for zsh) that calls raaz ctx show --plain fresh on every prompt render, so it always reflects whatever's currently saved. It wraps whatever prompt was already active rather than replacing it, so an existing custom prompt (oh-my-posh, a themed PS1, etc.) keeps working. Safe to re-run - it's idempotent (does nothing if already installed) and backs up the profile file (timestamped) before its first write. Restart your shell (or re-source/dot-source the profile) afterward to see it take effect.

Running raaz in CI

push/pull/run/doctor/diff/status/list */ctx show/ctx clear never prompt for anything - just resolve context via --app/--env/--provider (or a pre-saved .raaz/context.json) and supply provider credentials the normal way for that SDK (real environment variables, an IAM role, workload identity, etc.). raaz provider configure is a local convenience only - skip it entirely in CI, since the same cloud SDKs already read the standard credential env vars directly, with or without provider.env. Every other command that prompts (init re-init, ctx use, rollback, dotfiles remove) has a --force to skip its confirmation - pass it explicitly in scripts rather than relying on TTY detection (dotfiles remove --force also skips its interactive file picker, which needs a real TTY and would otherwise hang; raaz share <file> --all does the same for its own key picker). raaz share still isn't fully automatable even with --all, though - it's inherently a live, human-in-the-loop flow on the receiving end (waits for someone to open the link, or --timeout), which no flag removes.

Push / Pull

raaz push [filename] [--app] [--env] [--provider] [--pattern <regex>] [--dir <name>] [--no-examples]
raaz pull [filename] [--app] [--env] [--provider] [--pattern <regex>] [--dir <name>]

push finds every top-level .env-style file in the configured dotenv dir (the subfolder under .raaz/ chosen at raaz init, not nested subdirectories) and pushes each one under a secret name derived from its filename. pull fetches everything under the current app/env and writes each one back to that same dir by that same filename, overwriting whatever file is already there. --dir <name> overrides the dir for a single run without touching the saved context. If no local file matches (wrong directory, or nothing to push yet), push prints a clear Nothing to push - ... warning instead of claiming success - and for the local provider specifically, it won't create an empty <app>/<env> directory in that case either.

After a successful push, any <file>.example template that already exists next to a pushed file (see Generating .env.example files) is refreshed to match the file's current content - pass --no-examples to skip that for one push.

raaz push <filename> / raaz pull <filename> scope either command to just that one file by exact name - e.g. raaz push .env.prod - without needing to write a regex for the common "just this one" case. It's sugar for --pattern '^<filename>$'; passing both a filename and --pattern together is rejected as a contradiction. For pull specifically, giving a filename that doesn't match anything under the current app/env errors clearly (Nothing to pull - no secret matched '<filename>' for <app>/<env>) rather than silently succeeding with nothing written.

What counts as ".env-style" is a regex checked against the filename (via re.search), not a glob - every built-in provider defaults to the same shared raaz/env_files.py::ENV_FILENAME_PATTERN: a filename that starts with .env (.env, .env.local, .env.production, .environment, .envrc - anything after .env counts too), or ends in .env (foo.env, .local.env) - only a filename that merely has .env somewhere in the middle (notes.env.txt) is excluded. Pass --pattern to override this for a single command, e.g. raaz push --pattern '\.env\.(local|prod)$' to push a couple of specific files at once (a single file is simpler as raaz push <filename>, above). An invalid regex is rejected with a clear error instead of being attempted.

A plain raaz pull (no filename, no --pattern) still fetches and writes everything under the current app/env, for every provider, exactly as before - that part is unchanged. Give it a filename or --pattern and it now genuinely scopes what gets written too, for every provider including the cloud ones - previously --pattern on pull only changed what got backed up locally before the overwrite for aws/azure/gcp/oci (what actually got restored was always everything stored under the app/env, regardless of pattern); local was the only provider where it ever changed what got pulled, since it reads straight off a local directory. Now a filtered pull writes only the matching secret(s) for every provider.

Before overwriting anything, pull backs up whatever local .env-style files already exist into .raaz/backups/<timestamp>/ - see Rollback below to restore from one.

Status

raaz status [--app] [--env] [--provider] [--dir <name>]

The one to reach for day to day - a quick "am I safe to push/pull right now" glance. Prints the current context, then calls the exact same check doctor does (config hints + connectivity - exits early if that fails), then a summary of diff on top: how many files are changed/local-only/remote-only, or "in sync" if nothing differs. doctor and diff remain available as their own commands too - for scripting, or when you only want the one piece - status is just both together, in the order you'd actually want to know it.

raaz status                 # everything about the current context, at a glance
raaz status --provider aws  # check a different provider without switching context

Dotfiles

raaz dotfiles [--app] [--env] [--provider] [--dir <name>]

Lists which local dotfiles the current provider's env_file_pattern actually matches (what push/pull would pick up) - a pure filesystem check, no working credentials or network access needed to answer "what would match right now." Also lists dotfiles that don't match, checking both the configured .env dir and the actual project root when a .raaz/<dir>/ subfolder is configured and they differ - a stray near-miss (a misspelled .env, a .gitignore, anything) is most likely to land at the real project root, not inside that subfolder. Never touches doctor or diff at all, unlike status - this is a debugging aid for "why did/didn't this file get picked up," not a health check.

raaz dotfiles                 # which local dotfiles match/don't match the pattern
raaz dotfiles --provider aws  # check a different provider's pattern without switching context

Removing local dotfiles

raaz dotfiles remove [--force]

Deletes the local .env-style files for the current saved context (no --app/--env/--provider/--dir overrides - raaz ctx use to switch context first). This only ever touches the project's own local files - never anything stored remotely, whether that's a real cloud secret or local's own on-disk store under ~/.raaz. Useful for "I pulled prod secrets locally to debug something, now I want them off this disk" - the actual secret (wherever it's stored) is completely unaffected either way.

With no match, there's nothing to do. With exactly one match, it's selected automatically. With more than one and no --force, you get an interactive picker (space to toggle, enter to confirm, esc to cancel) - the same one raaz share uses to pick which keys to share. --force skips the picker and removes everything matched, which is also required for non-interactive/CI use, since the picker needs a real terminal. Removed files are backed up first, the same way pull backs up before overwriting - see Rollback to undo it.

raaz dotfiles remove          # pick which matched files to delete
raaz dotfiles remove --force  # delete every matched file, no prompts

Generating .env.example files

raaz dotfiles example [--app] [--env] [--provider] [--dir <name>] [--all]

Writes a <file>.example next to a selected local .env-style file - the same keys, with every value blanked (API_KEY= instead of API_KEY=sk-live-...), so a teammate cloning the repo can see exactly what a project needs without ever seeing a real value. Comments and blank lines are kept as-is, so any annotations in the real file (# get this from the Stripe dashboard) carry over.

Selection works exactly like dotfiles remove: no match, nothing to do; exactly one match, generated directly; more than one, the same interactive picker. --all skips that picker and generates a template for every matched file, non-interactively (a script or CI, say) - deliberately named --all, not --force: generating a template is non-destructive, so there's nothing to confirm, just a picker to skip. Each written filename prints on its own line.

Once a .example file exists, raaz push keeps it up to date automatically, every push, for as long as it exists - there's no separate setting to track or maintain; the file's own presence next to the real one is the only signal. Skip that refresh for a single push with raaz push --no-examples; delete the .example file to stop tracking it for good.

raaz dotfiles example    # pick which matched files to generate a template for
raaz push                # any existing .example files get refreshed automatically
raaz push --no-examples  # skip the refresh for this one push

.env.example files are meant to be committed to git (unlike the real .env, which usually isn't) - raaz doesn't manage your .gitignore rules for them, only for its own .raaz/ folder.

Diff

raaz diff [--app] [--env] [--provider] [--against <env>] [--pattern <regex>] [--dir <name>] [--verbose|-v]

Compares local .env-style files against what's currently stored remotely, without pulling or pushing anything. Reports three kinds of difference: files that exist only locally (never pushed), files that exist only remotely (never pulled), and files that exist on both sides but differ. If everything matches, it just prints "in sync" instead of an empty table.

--pattern works the same as on push/pull (a regex overriding the provider's default for this run). For cloud providers, diff is comparing against whatever secrets already exist remotely - it doesn't call push or pull, so nothing changes as a side effect of running it.

--verbose/-v prints every compared file, not just the changed ones: a file that's identical on both sides prints no diff; a file that only exists on one side prints a plain status line (local only - not pushed, ... / remote only - not pulled, ...) instead of a diff, since there's nothing on the other side to diff against. This is opt-in, since it prints secret values - the same convention raaz run --verbose uses.

A changed file gets a key-level diff, not a line-level one - both sides are parsed as KEY=VALUE pairs and compared by key, so reordering lines or editing a comment never shows up as a diff:

--- .env ---
+ NEW_KEY=value            # added locally, not yet pushed
- OLD_KEY=value            # removed locally, still stored remotely
~ CHANGED_KEY: old -> new  # same key, different value on each side

If a file's content genuinely differs but every key/value is identical (a comment or whitespace-only edit, or just reordered keys), it prints no key-level differences (only formatting/comments changed) instead of a diff. If a file doesn't parse as KEY=VALUE content at all - e.g. syncing something other than a real .env file via a custom --pattern - it falls back to a plain line-by-line diff instead, since a key-level diff would be meaningless there.

raaz diff                           # what's different, if anything
raaz diff --verbose                 # + every file's diff (or status, if there's nothing to diff)
raaz diff --pattern '\.env\.prod$'  # scope the comparison to one file

Comparing two environments

raaz diff --against <env> [--app] [--provider] [--pattern <regex>] [--verbose|-v]

Compares the current context's env against another env's remote content, under the same app/provider - a remote-vs-remote comparison, with no local files involved at all. Useful for catching drift between, say, prod and staging (a key present in one but not the other) without ever pulling anything down. Everything else - the summary table, --verbose's per-file detail, the key-level diff - works exactly the same as the default local-vs-remote mode, just comparing two remote sides instead of local-vs-remote. --dir doesn't apply here (there's no local directory in this mode) and is rejected if combined with --against; comparing an env against itself is rejected too, since there'd be nothing to compare.

raaz diff --against staging            # what differs between the current env and staging
raaz diff --against staging --verbose

Matrix

raaz matrix [filename] [--app] [--provider] [--envs env1,env2,...] [--all]

diff --against only ever compares two environments at a time. matrix compares one file's keys across every environment at once, in a single table - the thing to reach for when the real question isn't "what changed between staging and prod" but "does every environment actually have this key at all." filename defaults to .env; a bare positional name or a pasted/tab-completed path both work (only the filename itself is used - raaz matrix ./.raaz/dots/.env and raaz matrix .env are identical).

Each cell is presence, not value comparison: OK (green) if that key exists in that env's file, - (red) if it doesn't. There's no "changed" state and no baseline/reference environment - an earlier version compared every env against an arbitrary first one and flagged value differences, but that fired constantly on exactly the keys where a difference is normal and expected (a DATABASE_URL is supposed to differ per env) and rarely on what actually matters. The only signal worth showing is whether a key silently never made it to one environment - the "pushed a new secret to dev, forgot to push it to prod" case.

raaz matrix                          # .env's keys across every discovered env
raaz matrix .env.prod                # a different file
raaz matrix --envs dev,staging,prod  # only these envs, in this order
raaz matrix --all                    # every .env-style file, one consolidated table

Without --envs, the env list is discovered the same way list envs does (every env found for the resolved app/provider, sorted alphabetically) - pass --envs explicitly if discovery misses one, or to control the column order. At least two envs are required; fewer than that is rejected with a clear error rather than printing a table with nothing to compare.

--all discovers every .env-style file present across the resolved envs (the union, not just what one env happens to have) and renders one table, not one per file - a FILE column groups each file's keys together (with a divider line between files, and the filename shown only once per group, on its first row) so it reads as one fleet-wide view instead of several disconnected reports.

If filename doesn't exist at all in one of the resolved envs, that's called out separately below the table ('.env' not found at all in: staging) - different from an individual missing key, since it means the whole file was never pushed there, not that one secret was dropped from an otherwise-present file.

Fetching runs with a status spinner (beep boop... fetching <env> (i/n)) since it's making one real provider call per environment - for --all, every file's table is built from data fetched once per env, not re-fetched per file.

Sync

raaz sync --to-provider <provider> [--to-app <app>] [--to-env <env>]
          [--app] [--env] [--provider] [--pattern <regex>] [--dry-run] [--force]

Copies secrets from the current context into a different app/env/provider - moving off local onto a real backend, migrating between two cloud providers, or seeding a new environment from an existing one (--to-env preprod off of staging). This is the one command that writes to somewhere other than the resolved source context. --to-provider is required; --to-app/--to-env default to the source's own app/env, so cloning the same app/env to a different provider only needs --to-provider.

Never deletes anything at the destination, and never touches the saved context on either side - same as diff --against, this is a one-shot operation, not a context switch. A file that exists at the destination but not at the source is left alone; a file that's identical on both sides is skipped rather than rewritten. Rejected outright if the resolved destination is identical to the source (same app/env/provider) - nothing to sync.

raaz sync --to-provider aws                          # local -> aws, same app/env
raaz sync --to-provider aws --to-env prod            # local's dev secrets, seeding aws's prod
raaz sync --provider aws --to-provider gcp           # migrate providers
raaz sync --to-provider aws --to-env preprod --dry-run  # preview only, nothing written

Before writing anything, it prints a table of what would change - new files, files that would be overwritten, and (below the table) files present only at the destination, which sync leaves untouched. --dry-run stops there. Otherwise, it asks for confirmation (default no, since this can write to a real production destination) unless --force is passed. --pattern scopes the source side the same way it does for push/pull/diff.

Nothing matching the pattern (or nothing to sync at all) prints a plain message and exits cleanly rather than writing an empty change. A plain raaz sync with everything already matching at the destination prints "already in sync" and writes nothing.

Share

raaz share <file> [--key <KEY>]... [--all] [--timeout <secs>] [--copy/--no-copy]
raaz share configure

raaz share .env is the only form there is - there's no separate verb to type. (raaz share configure always runs the setup command below rather than sharing a file named configure; qualify the path - raaz share ./configure - on the rare chance you have a file by that exact name.)

Hands a single secret to someone with a one-time-read link. Unlike every other command here, the recipient needs no cloud credentials and no raaz install - they just open the link in any browser. It's a separate feature from the rest of raaz (no app/env/provider context, no push/pull) and lives in its own [share] install extra:

uv tool install "raaz[share]"  # or: pipx install / pip install - see Installation in the README
raaz share configure           # one-time: needs a free account at https://dashboard.ngrok.com
  • .env-style file (.env, .env.local, foo.env, ...): --key API_KEY shares just that one entry (repeat --key for more); omit --key and pick from an interactive menu instead.
  • Any other file - a .pem cert, a raw token, a kubeconfig - shares whole, no menu.
  • --all skips the menu and shares the whole file either way - for a script or CI with no TTY to pick from. Can't combine with --key.

The file is read from disk right now - it doesn't need to have been pushed anywhere, and sharing it doesn't push or affect it. You never need to type the .raaz/<dir>/ path yourself: if <file> isn't found where given, raaz also checks the currently-configured subfolder before giving up. On the recipient's end, every value is masked until "Show" is clicked, and "Copy" grabs the real value without needing to reveal it first.

How it's secured: the secret is encrypted locally before anything is uploaded, and the decryption key never leaves your machine - it only ever lives in the URL's fragment (after #), which browsers never send to a server. Whatever relays the link (ngrok, currently) only ever sees ciphertext. The link dies the instant it's opened once, or after --timeout seconds (default 600), whichever's first.

The catch: there's no persistent server behind this - raaz share runs a local server and tunnels it out via ngrok for as long as the command keeps running. Your machine and that terminal both have to stay open for the whole window the link is valid - the one raaz command that only works while you're at your desk running it.

raaz share .env.production --key DATABASE_PASSWORD --timeout 300

Troubleshooting: OSError: [WinError 225] ... contains a virus or potentially unwanted software - this is Windows Defender (or another antivirus) blocking ngrok.exe from running, not an actual problem with the file or with raaz. Tunnel/reverse-proxy tools like ngrok get flagged by heuristic AV scanning fairly often, since that's also the shape of tool malware C2 frameworks use. Fix:

  1. Check Windows Security → Virus & threat protection → Protection history - if ngrok.exe was quarantined there, restore/allow it.
  2. Add an exclusion so it doesn't get blocked again: Windows Security → Virus & threat protection → Manage settings → Exclusions → Add an exclusion → Folder%LOCALAPPDATA%\ngrok\ (where pyngrok downloads the binary - confirm the exact path with python -c "from pyngrok import conf; print(conf.get_default().ngrok_path)").

Then retry raaz share <file>. This is a pyngrok/Windows interaction, not something raaz's own code can work around.

Rollback

raaz rollback [--list]                      # list available backup snapshots
raaz rollback [--to <timestamp>] [--force]  # restore one (defaults to the most recent)

Every pull that would overwrite an existing local file backs it up first (see Push / Pull above) - rollback restores from one of those snapshots, copying its files back into the configured dotenv dir and overwriting whatever's there now. --list shows the available snapshots (by timestamp) without restoring anything; --to <timestamp> picks a specific one instead of the most recent. Prompts for confirmation unless --force is passed. Snapshots live in .raaz/backups/ - the oldest ones are pruned automatically once there are more than 20, so this doesn't grow without bound.

Audit log

raaz audit [--app] [--env] [--provider] [--limit N]  # show local audit log entries, newest first

Every push, pull, remove (env/app/provider), rollback, and .raaz/<dir>/ file move gets one line appended to ~/.raaz/audit.log - timestamp, action, app/env/provider, and whether it succeeded, never a secret name, key, or value. Read-only commands (list, run, diff, status, doctor) never touch this file, since nothing they do changes anything. raaz audit reads it back as a table, filterable by --app/--env/ --provider, newest first.

This complements each provider's own real audit trail (AWS CloudTrail, Vault's audit devices, and so on) - those are tamper-resistant and provider-specific; this is local, plain text, and the one place that sees across every provider at once, tagged with raaz's own app/env/provider context those provider-side logs don't know about. On by default - pass the global --no-log flag (see Global flags) to disable it for a single run, which prints a warning each time so disabling it is always a deliberate choice, not a silent one. Useful in CI, where a shared/ephemeral filesystem makes a local log pointless anyway.

Run

raaz run [--app] [--env] [--provider] [--verbose|-v] -- <command> [args...]

Runs <command> with every secret for the current app/env injected as environment variables - fetched fresh each time and merged into the child process's environment, never written to disk. Put -- before the command so its own flags aren't mistaken for raaz's:

raaz run -- java -jar app.jar
raaz run --env prod -- node server.js --port 8080

The child process's stdin/stdout/stderr are connected straight to your terminal, and its exit code becomes raaz run's own exit code, so it composes normally in scripts and CI (raaz run -- pytest && echo ok).

--verbose/-v prints a table of every injected key, its value, and which secret (file) it came from before running the command - useful for confirming what's actually being injected. It's opt-in and off by default, so plain raaz run never prints secret values to the terminal/logs.

Whether your app actually sees the injected variables depends on how it reads config: System.getenv(...) in Java (or anything a framework like Spring Boot binds from the environment) picks them up with no changes. System.getProperty(...) (JVM -D flags) is a different namespace from environment variables and won't see them. If your app instead reads a .env file directly off disk, run doesn't help - use pull for that.

Doctor

raaz doctor [--provider]                    # check a provider's credentials/config

Diagnoses a provider instead of letting the first real command fail with a raw SDK stack trace (status calls this directly, so this is just the credentials/config piece on its own). Defaults to the saved context's provider if --provider is omitted. It first prints non-fatal hints about likely-missing config (e.g. a missing AZURE_VAULT_URL, or an OCI_KEY_FILE pointing at a file that doesn't exist), then - for every provider except local, which needs no credentials - calls list() as a connectivity smoke test and reports success or a clear failure reason.

For local specifically, doctor also reports whether your OS keychain is currently reachable - secrets local stores under ~/.raaz/providers/local/ are encrypted at rest with a key kept in your OS's keychain (Windows Credential Manager / macOS Keychain / Linux Secret Service), automatically, no setup needed. If the keychain isn't reachable (rare - a headless machine with no keychain backend running), push falls back to storing plaintext instead of failing, with a one-time warning; anything already stored before you had this version of raaz (or from a moment the keychain was unreachable) stays readable too - it's picked back up as plaintext and only becomes encrypted the next time you push that same secret again. There's no separate migration step and no bulk re-encrypt command - it happens naturally on the next push, or not at all if you never push that secret again.

raaz doctor                 # check the current context's provider
raaz doctor --provider aws  # check a specific provider regardless of saved context

Providers

raaz provider list              # alias of `list providers`
raaz provider configure <name>  # interactively write provider.env

provider configure <name> prompts for each credential field a provider needs (masking secret fields like AWS_SECRET_ACCESS_KEY/AZURE_CLIENT_SECRET) and writes ~/.raaz/providers/<name>/provider.env for you, instead of you hand-editing that file. Re-running it against an already-configured provider pre-fills each prompt with the current value - press Enter to keep it. Any lines already in the file that aren't one of the known fields are left untouched. local needs no credentials, so it just prints guidance instead of prompting. It offers to run raaz doctor immediately afterward so you find out right away if something's wrong.

Raaz has no command that deletes a secret from where it's actually stored - not from a cloud provider's API, and not from local's own on-disk store under ~/.raaz, treated exactly the same way. Remove a cloud secret through the vendor's own tooling; to clean up local's store, delete ~/.raaz/providers/local/<app>/<env>/ by hand. See raaz dotfiles remove for deleting the project's own local .env files instead - a different, much more common operation this does support directly.

Listing secrets

raaz list secrets [prefix] [--app] [--env] [--provider]  # list secrets under the given app/env, optionally filtered
raaz list secrets --all                                  # list every secret in the provider, ignoring app/env entirely
raaz list apps                                           # list every distinct app found in the current provider
raaz list envs [--app]                                   # list every distinct env, optionally scoped to one app
raaz list providers                                      # list registered providers (aws/gcp/azure/oci/vault/1password/bitwarden/local) and whether each is installed

raaz list providers shows every provider raaz knows about alongside an Installed column - yes if that provider's SDK extra is actually present, no if it isn't. All 8 built-in providers always show up here regardless of what you've installed (a bare pip install raaz still lists all 8, with only local showing yes - it needs no SDK at all); this only checks whether the provider could be used, not whether it has working credentials configured - that's what raaz doctor --provider <name> checks.

--app/--env/--provider on list secrets override the saved context for just this command, same as push/pull/run/doctor - so raaz list secrets --app foo --env bar searches a different app/env without switching your saved context. A prefix, if given, filters by secret name within that scope - e.g. raaz list secrets env matches .env/.env.local/etc. under your current app/env, without needing to type the app/env yourself or switch anything. It's never a raw search across every app/env - use --all for that (optionally combined with --app/--env/--provider to search a different scope broadly). Passing both --all and a prefix is an error, since they're contradictory.

list apps/list envs solve a related problem: not remembering any app/env name at all to search by. Both fetch every secret in the current provider (like --all) and parse each name using that provider's own separator (/ for AWS and local, . for OCI, - for GCP/Azure) to recover the app and env - a best-effort positional split, fine for browsing/discovery but not exact recovery (push/pull use a stricter method elsewhere for that reason). This works uniformly across every provider, including local - local doesn't have real secret names, but synthesizes name-shaped strings from its own directory structure so it can share the same discovery logic as everything else.

list providers also has a noun-first alias, reachable from provider's own command group instead: raaz provider list - same output, same underlying code, just grouped the other way for whichever you reach for first. list apps/list envs have no such alias - there's no app/env command group at all.

Roar

raaz roar

Renders the Lion and Sun (Shir-o-Khorshid) alongside "Woman, Life, Freedom" / زن، زندگی، آزادی - in honor of the Iranian people. A real, visible command, listed in --help like any other, not a hidden easter egg. Falls back to a transliteration ("Zan, Zendegi, Azadi") if the terminal's code page can't render Persian, rather than crashing.