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:
- Validates the provider name first, before writing anything - an unknown provider fails cleanly with
Unknown provider '<name>'. Installed: ...and leaves no.raaz/context.jsonbehind, rather than writing the context and only then discovering the provider doesn't exist. - If re-initializing over an already-saved context that differs from the new one, runs the same
unpushed-local-changes safety check
ctx usedoes (see below) ---forceskips it. - Resolves
--dir- the subfolder under.raaz/holding this project's.envfiles, if any. On a true first init (no saved context yet) and no--dirgiven, asks whether to opt into a subfolder at all (default no - keep the project root); saying yes then prompts for a name (defaultdots, i.e..raaz/dots/). Re-initializing keeps whatever was already saved unless--diroverrides it. A non-interactive session (no TTY to answer the confirm) also defaults to the project root, same as declining. - 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.envat 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.--forceskips every ask (the move itself, and any conflict) and overwrites conflicts outright - consistent with every other--forcein this CLI (skip the confirmation, proceed with the action - now including overwriting). Pass--keep-existingtoo 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-existingis 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.exampleat 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.--forceis 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. - Writes
.raaz/context.json, then ensures the project's.gitignoreexcludes.raaz/- creating.gitignoreif 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 everyinit/re-init, not just the first..raaz/holds real project state (backups, and your actual.envfiles) that should never end up committed. - Loads that provider's
provider.envinto 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_KEYshares just that one entry (repeat--keyfor more); omit--keyand pick from an interactive menu instead.- Any other file - a
.pemcert, a raw token, akubeconfig- shares whole, no menu. --allskips 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:
- Check Windows Security → Virus & threat protection → Protection history - if
ngrok.exewas quarantined there, restore/allow it. - Add an exclusion so it doesn't get blocked again: Windows Security → Virus & threat protection → Manage
settings → Exclusions → Add an exclusion → Folder →
%LOCALAPPDATA%\ngrok\(wherepyngrokdownloads the binary - confirm the exact path withpython -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.