CLI command reference
Every xecret command and flag — login, init, projects, secrets, versions, import, pull, export, run, audit, tokens and cache — with examples and the exact behaviour of each.
Updated 2026-10-20 · read this page as markdown
On this page
- Authentication
- xecret login
- xecret logout
- xecret whoami
- xecret orgs
- Project setup
- xecret init
- xecret projects
- xecret environments
- Secrets
- xecret secrets list
- xecret secrets get
- xecret secrets set
- xecret secrets annotate
- xecret secrets versions
- xecret secrets restore
- xecret secrets delete
- Moving secrets in bulk
- xecret import
- xecret pull
- xecret export
- xecret vault open-archive
- The golden path
- xecret run
- xecret overrides
- Administration
- xecret audit
- xecret members
- xecret tokens
- Housekeeping
- xecret cache clear
- xecret completion
- xecret doctor
- xecret upgrade
- The upgrade notice
- xecret version
- xecret help
- Quick reference
- Next
Every command the xecret binary accepts. Run xecret help for the same list
in your terminal, or xecret <command> --help for one command's flags.
Two flags recur on every command that reads secrets:
--project SLUG override .xecret.yaml
--environment SLUG override .xecret.yamlAuthentication
xecret login
xecret login [--api-url URL] [--name DEVICE]Authenticates this machine through your browser.
The CLI opens a consent page, listens on 127.0.0.1 on a random port for the
one-time code that page redirects back, and exchanges that code — together with
a PKCE verifier only this process knows — for a credential it stores in your OS
keychain. It never talks to the identity provider directly; the consent screen
and the token exchange are both xecret's own. If the browser is not signed in to
xecret yet, it signs in first and comes back to the consent screen.
| Flag | Effect |
|---|---|
--api-url URL | Which deployment to sign in to. Stored with the credential, so you pass it once. |
--name DEVICE | The device name shown on the consent screen and in the dashboard. Defaults to your hostname. |
xecret login --name "work laptop"
xecret login --api-url https://secrets.your-company.comThe device name is what you will look for later when revoking access from Tokens → Your devices, so make it something you will recognise.
Logging in again replaces the credential this machine held, and the CLI revokes
the old one on the server once the new one is saved — so re-running
xecret login does not leave a stale device behind. If that revocation fails
(the old deployment is unreachable, say), the login still succeeds and says so;
revoke the old device from Tokens → Your devices. Each plan limits how many
devices one person may have signed in to an organisation (two on Free); at the
limit, the consent screen says so and the way forward is to revoke one you no
longer use.
xecret logout
xecret logoutRevokes this device's credential server-side, clears the keychain entry,
and wipes the encrypted offline cache. Refused while XECRET_TOKEN is set: a
service token was not minted by this machine and is not this machine's to sign
out. Revoke one with xecret tokens revoke or in the
dashboard.
xecret whoami
xecret whoami [--json]Asks the server who you are, rather than reading the stored credential. That distinction matters: a credential revoked from the dashboard reads as signed out here, instead of confidently reporting a stale identity.
Signed in as ada@example.com
Organisation acme
This device work laptop
Server https://xecret.playxoft.comUnder XECRET_TOKEN it answers with the token's pinned scope instead of a
person:
Credential service token "ci-build" (read)
Organisation acme
Project checkout-api
Environment production
Server https://xecret.playxoft.comxecret orgs
xecret orgs [list] [--json]
xecret orgs use SLUGYour credential works in every organisation you are a member of. use chooses
which one the other commands address:
SLUG NAME ROLE
acme Acme Ltd owner current
side Side Project developeruse is a local change — one field stored beside the token. It makes no network
call beyond checking that you are actually a member, revokes nothing, and
switching back is another use. Refused under XECRET_TOKEN, which is pinned
to one organisation server-side.
Project setup
xecret init
xecret init [--project SLUG] [--environment SLUG] [--force]Interactive picker that writes .xecret.yaml in the current directory — two
slugs, never secrets, safe to commit.
| Flag | Effect |
|---|---|
--project SLUG | Skip the project prompt |
--environment SLUG | Skip the environment prompt |
--force | Overwrite an existing .xecret.yaml |
xecret init # prompts for both
xecret init --project checkout-api --environment developmentRefused under XECRET_TOKEN: a CI job needs no config file, because the
token's own scope already answers the question.
xecret projects
xecret projects [list] [--json]
xecret projects delete SLUG [--yes]Lists the projects you can see in your organisation, and removes one.
There is no create here — projects are created in the dashboard. A project
arrives with its default environments — development, staging and production —
and each is end-to-end encrypted under a key generated in the creator's browser
and sealed to them. This CLI opens grants and encrypts secrets; it cannot
produce that key material, so there is nothing it could send.
The slug you choose there is permanent: it appears in every URL, in
.xecret.yaml and in the CI configuration of everyone who consumes the project,
so renaming it would break every consumer that is not redeployed at the same
instant.
Deleting is soft, and asks you to type the slug back unless you pass --yes.
xecret environments
xecret environments [list] [--project SLUG] [--json]
xecret environments delete SLUG [--yes] [--project SLUG]Lists and removes the environments of one project. Without --project, uses
.xecret.yaml.
There is no create here either, for the same reason. An environment is
inseparable from its encryption key — one without a key could not hold a secret
— and that key is generated in a browser and sealed to its creator. Create the
environment in the dashboard, then fill it from here:
# a throwaway environment for one pull request, created in the dashboard as pr-412
xecret import .env.example --environment pr-412 --strategy overwrite
xecret environments delete pr-412 --yesWhether an environment is production is settled in the dashboard too. It narrows who can read the environment and makes deleting it ask for the slug, and reclassifying one that already holds production secrets is a different act from labelling an empty one.
Secrets
xecret secrets list
xecret secrets list [--project P] [--environment E] [--json]Names, value types, current version numbers, when each was last updated and by whom. No values — this endpoint never decrypts anything.
xecret secrets get
xecret secrets get NAME [--version N] [--plain] [--json]Masked by default:
Name DATABASE_URL
Type url
Version v4
Updated 2026-08-14 09:12
Note primary read-write connection--plain prints the decrypted value to stdout and nothing else, so it composes:
psql "$(xecret secrets get DATABASE_URL --plain)"Every --plain read is audited server-side as secret.revealed. An audit row
therefore always means a plaintext actually left the server — which is what
makes the log worth reading during an incident.
--version N reveals one earlier value instead of the current one, and requires
--plain, because the value is the only thing it adds over
secrets versions:
xecret secrets get STRIPE_SECRET_KEY --version 3 --plainA warning goes to stderr when the version is not the current one. It is worth reading: a rotated secret is usually still live at whoever issued it, so an old value is a working credential, not an archive entry.
xecret secrets set
xecret secrets set NAME [--type TYPE] [--note TEXT] [--from-file PATH] [--generate[=BYTES]] [--confirm-link]Reads the value from stdin, or prompts for it with the input hidden. Never
from a command-line argument, where it would land in your shell history and
in ps output for every user on the machine.
# interactive — the prompt hides your typing
xecret secrets set STRIPE_SECRET_KEY
# from a pipe, for scripts
openssl rand -hex 32 | xecret secrets set SESSION_SECRET
# from a file, byte for byte
xecret secrets set GOOGLE_SERVICE_ACCOUNT --from-file key.json
# have one generated — 32 bytes of entropy, never printed
xecret secrets set SESSION_SECRET --generate
# declare a type and a note
xecret secrets set PORT --type int --note "the container listens here"| Flag | Effect |
|---|---|
--type TYPE | Declares the value's shape: string boolean int decimal email url date datetime json yaml xml ulid uuidv4 uuidv7, or template for a value built from other secrets. Future writes that do not match are refused by the server. |
--note TEXT | A note shown beside the secret in the dashboard. |
--from-file PATH | Take the file's bytes as the value, verbatim — trailing newline included. A pipe's final newline is the shell's framing and is stripped; a file's is part of the file, and a PEM key ends in one. Limited to 64 KB, the server's ceiling on one secret. |
--generate[=BYTES] | Mint a random value with the OS entropy source and write it without ever printing it. 32 bytes by default, rendered as 43 url-safe characters. Use --generate=48 — with the = — to choose a length. |
--confirm-link | Confirm a production template that newly reads from an environment that is not production. Without it such a write is refused, naming who could then change what production reads — see references. |
Writing the same value again is a no-op rather than a new version.
A template (--type template, or a secret already declared one) refers to
other secrets — ${DB_HOST}, ${staging.DB_HOST}, ${billing.prod.API_KEY} —
and is resolved when it is read. The CLI parses the value before encrypting it,
refuses a malformed reference locally, and declares the references it found so
the server can check them for loops before storing anything. See
References and inheritance.
If your organisation has a secret policy, the value is checked against its entropy and forbidden-word rules before it is encrypted, and refused with the rule it broke. The value itself is never shown.
xecret secrets annotate
xecret secrets annotate NAME [--note TEXT] [--type TYPE] [--rename NEW]Changes what is said about a secret without touching what it holds, and
appends no version. Declaring PORT an integer is not a rotation, and
neither is a rename, so neither may bump the number that answers "when did this
credential last actually change?".
xecret secrets annotate PORT --type int
xecret secrets annotate DATABASE_URL --note "primary read-write connection"
xecret secrets annotate DATABASE_URL --note "" # clears the note
xecret secrets annotate API_KEY --rename STRIPE_API_KEYThe version history follows the secret through a rename. Everything that reads the old name stops finding it at once, so update your code and CI in the same change.
xecret secrets versions
xecret secrets versions NAME [--json]The history of one secret — metadata only.
VERSION WRITTEN BY
v4 current 2026-08-17 09:12 user
v3 2026-06-02 14:40 service token
v2 2026-04-11 08:03 userNo values, and that is the point rather than a limitation. A rotated secret
usually keeps working at the provider that issued it until somebody disables it
there, so a listing that returned values would hand out a page of live
credentials under an interface people reason about as an archive. Reading the
past is one version at a time, through secrets get --version N --plain or
secrets restore — and each of those is audited.
xecret secrets restore
xecret secrets restore NAME --version NRe-appends an earlier value as the new current version.
xecret secrets versions DATABASE_URL
xecret secrets restore DATABASE_URL --version 3✓ Restored DATABASE_URL from v3 as v5 in checkout-api/production.
History was not rewritten — v3 is still there.The old row stays exactly where it is: the history stays able to say "this value was current between Tuesday and Friday, then again from Monday". The value is decrypted and re-encrypted rather than copied, because the encryption is bound to the version number — bytes produced for v3 and stored as v5 would fail to decrypt for the rest of their life.
xecret secrets delete
xecret secrets delete NAME [--yes]Soft-deletes a secret. Without --yes you are asked to type the secret's name
to confirm — a y/n prompt is too easy to answer by reflex for something that
takes a value out of a running system.
Always refused for service tokens: CI rotates a value by writing a new one, and destroying history is a human's decision.
Moving secrets in bulk
xecret import
xecret import FILE [--format dotenv|json|yaml|shell] [--strategy skip|overwrite|rename] [--dry-run] [--json]Sends a file of secrets to one environment. The format is auto-detected;
--format overrides that when detection guesses wrong.
--strategy | On a name that already exists |
|---|---|
skip (default) | Leave the existing value alone |
overwrite | Append a new version with the imported value |
rename | Import under a suffixed name, keeping both |
xecret import .env --dry-run # see the plan, write nothing
xecret import .env --strategy overwrite--dry-run prints the exact plan the real import would execute, produced by
the same planning code the real import runs — so the preview cannot disagree
with the outcome. Always dry-run first against production.
Files are limited to 1 MB.
xecret pull
xecret pull [--format env|json|yaml|shell|docker] [-o FILE]Prints every current secret in the chosen format.
xecret pull --format json | jq 'keys'
xecret pull -o .env.production # creates the file 0600xecret export
xecret export [--format env|json|yaml|shell|docker] [-o FILE] [--force]The same data as pull, written to a file. Without -o the name is derived
from the format — .env, secrets.json, secrets.yaml, secrets.sh,
docker.env — and an existing file is never overwritten without --force.
The file is mode 0600 whether it was created or overwritten. A --force over
a .env that some earlier xecret pull > .env left at 0644 narrows it before
anything is written to it, rather than filling a world-readable file.
xecret export # → .env, mode 0600
xecret export --format json -o config/secrets.jsonIt is a separate command from pull rather than a flag on it because the two
are separate endpoints: the request path is what distinguishes "a build read
its configuration" from "somebody took a copy" in the audit log. Both are
recorded as secret.read with a count.
Neither pull nor export includes your personal overrides;
each says on stderr when you have some in the environment. Both refuse outright
when the environment's policy disables plaintext export,
naming the policy and who can change it. xecret run is never affected.
xecret vault open-archive
xecret vault open-archive FILE [--recovery-code] [--passphrase-stdin]
[--format env|json|yaml|shell|docker]
[--environment ORG/PROJECT/ENV] [--out-dir DIR] [--force] [--list]Opens the archive xecret emails when a vault is reset — the .zip, or the
archive.json inside it — with the old vault's master passphrase or one of
its recovery codes, and prints every secret, grouped by environment. It works
fully offline: no account, no login, no network, no configuration, no keychain.
The passphrase (or, with --recovery-code, the code) is asked for without
echo; --passphrase-stdin reads it from a pipe instead, never from a flag.
--out-dir writes one file per environment, mode 0600, instead of printing.
--list shows what the archive holds without asking for anything.
A value written under a key the account never held is reported, not printed; a damaged value is reported and makes the command exit non-zero. Everything that opened is still printed. See Resetting your vault.
The golden path
xecret run
xecret run [--project P] [--environment E] [--offline] [--no-cache] -- COMMAND [ARGS…]Fetches the environment, injects it into the child process, forwards signals, and exits with the child's exit code. Secrets never touch disk, argv or stdout.
xecret run -- npm run dev
xecret run --environment staging -- ./deploy.sh
xecret run -- go test ./...Everything after -- is your command. The -- is required: without it the CLI
cannot tell your flags from its own.
| Flag | Effect |
|---|---|
--offline | Use the encrypted cache without calling the API. Refused under XECRET_TOKEN, which never writes a cache. |
--no-cache | Neither read nor refresh the cache. |
The API is authoritative; the cache answers only when the API cannot — a network failure or a 5xx. A 401, 403 or 404 is a decision (a revocation, above all), and decisions are never softened by a local file. Full behaviour: the offline cache.
An environment that inherits from another, or holds templates, is resolved
before the process starts — every inherited value and every reference, each
opened under its own environment's key. Logged in as yourself (never under
XECRET_TOKEN), run then applies your personal overrides on top and names
them on stderr; offline, it says it did not.
xecret overrides
xecret overrides list [--json]
xecret overrides set NAME [-] [--from-file PATH]
xecret overrides unset NAMEYour own value for one secret in one environment, used by xecret run on your
machine in place of the shared one — a local database instead of staging's —
without anybody else's environment changing. Nobody else can read it, the
server included, and nobody else can make one in its place: it is encrypted
under a key only your own vault derives. The value is read from stdin or a
hidden prompt, never from an argument.
printf '%s' "postgres://localhost/app" | xecret overrides set DATABASE_URL -
xecret run -- npm run dev # applied 1 personal override(s): DATABASE_URL
xecret overrides unset DATABASE_URLRefused under XECRET_TOKEN: a CI run always gets the shared values. An
override set before a vault reset no longer opens, and run stops rather than
silently using the shared value, naming the unset that removes it. Setting one
needs the Pro plan. See Personal overrides.
Administration
xecret audit
xecret audit [--action ACTION] [--project P] [--environment E] [--outcome success|denied|error]
[--since 24h|7d|TIMESTAMP] [--until 1h|7d|TIMESTAMP] [--limit N] [--json]Reads the organisation's audit log — who did what, when, and whether it worked.
--since and --until take the same spellings: a duration counting back from
now (24h, 7d), or an RFC 3339 timestamp, normalised to UTC before it is
sent. A window that cannot contain anything — a negative duration, a --since
after --until, a --since in the future — is refused here rather than sent.
The server collapses a backwards range to a single instant, so each of those
would otherwise come back as "No matching events", which reads exactly like
"nothing happened".
xecret audit --action secret.revealed --since 7d
xecret audit --outcome denied --limit 100 --json | jq '.events[].actorLabel'WHEN ACTION OUTCOME ACTOR SUBJECT
2026-08-17 11:00 secret.revealed success ada@example.com checkout-api/production/API_KEY
2026-08-17 10:30 secret.read success ci-build checkout-api/productionOwners and admins hold audit.read; developers deliberately do not, because the
log spans projects a developer cannot see and records every denial anyone ever
received. A 403 here is the policy working.
The window actually scanned is printed after the table. The server clamps any
range to ninety days, and a query for "the last year" that quietly answered for
three months would be worse than useless during an incident. --limit is
honoured exactly, and the command says so when more events matched than it read.
--since takes a duration (24h, 7d) or an RFC 3339 timestamp.
xecret members
xecret members [--json]Who is in the organisation, their roles, and the seat count. Read-only: inviting, suspending and changing a role require a browser session server-side, so there is nothing here that could do them.
xecret tokens
xecret tokens list [--kind cli|service] [--json]
xecret tokens revoke ID --kind cli|service [--yes]The credentials that can reach your organisation.
clitokens are devices — whatxecret loginwrites. The listing shows your own devices only, revoked ones included, because a CLI token acts as its user and confers nothing of its own. Your own device: always revocable. Someone else's: needs the revoke permission.servicetokens are the CI credentials behindXECRET_TOKEN. They belong to the organisation rather than to a person, so revoking one takes the same authority that could have minted it.
xecret tokens list --kind service
xecret tokens revoke 7f3c… --kind service --yesRevocation is immediate: the next request carrying that token fails authentication. It is idempotent, so revoking a dead token succeeds without writing a second audit record.
Creating a service token is deliberately not a CLI command. The server requires a browser session for it — a token that could mint another token would turn one leaked credential into a permanent foothold, so the chain has to start with a person. Create them in the dashboard under Settings → Tokens.
Housekeeping
xecret cache clear
xecret cache clearRemoves every cached environment from this machine. xecret logout does this
for you.
xecret completion
xecret completion bash | zsh | fishPrints a completion script for commands, subcommands and the common flags.
# bash — this session, or append to ~/.bashrc
source <(xecret completion bash)
# zsh — needs a directory on $fpath
xecret completion zsh > "${fpath[1]}/_xecret"
# fish
xecret completion fish > ~/.config/fish/completions/xecret.fishSecret names are deliberately not completed: that would mean an API call — and a listing request against a server that rate-limits and audits — on every press of the Tab key.
xecret doctor
xecret doctor [--json]Checks this machine's setup and says what is wrong: which credential store is in
use, whether you are signed in, which deployment the commands resolve to and
why, whether the server is reachable, whether the credential is still
accepted, which .xecret.yaml applies, and what is in the offline cache.
xecret 1.2.0 (commit a1b2c3d, built 2026-08-17, darwin/arm64, go1.25.5)
✓ credential store: OS keychain
✓ signed in as ada@example.com, organisation acme
server: https://xecret.playxoft.com (stored with the credential at login)
✓ server reachable — xecret 0.1.0 (commit a1b2c3d)
✓ credential accepted by https://xecret.playxoft.com
✓ /work/checkout/.xecret.yaml → checkout-api/development
✓ offline cache: 2 environment(s) in ~/.xecret/cacheIt prints no credential. The keyring check writes and deletes a probe value of its own rather than reading yours. Paste the output into a bug report.
doctor exits non-zero when a check fails, so xecret doctor || exit 1 works
as a start-up guard in a container. Warnings — no .xecret.yaml in this
directory, an unreadable cache — are not failures. With --json every verdict
comes back in a checks array of {name, status, ok, detail}, alongside a
top-level ok:
xecret doctor --json | jq -r '.checks[] | select(.ok | not) | .detail'xecret upgrade
xecret upgrade [--json]Asks GitHub whether a newer release has been published, and prints the command to install it.
Nothing checks in the background. A version check is a request describing which
machine runs which build of a secret-management client, and making it a side
effect of xecret run would ship that from inside every CI job. It happens when
you ask.
It also does not replace the binary. Every published archive is checksummed and signed, and the installer verifies the checksum before unpacking; a secret manager that silently overwrites its own executable is exactly the supply-chain shape nobody should accept.
The upgrade notice
Commands that talk to your deployment may end with three lines on stderr:
↑ xecret 0.2.0 is available (you have 0.1.2)
Reads end-to-end encrypted environments - older builds return an empty
value for them.
Upgrade: xecret upgradeThis is not the background check the section above rules out, and it costs no request of any kind. Your deployment names the release it expects in a header on a response the CLI was already receiving; the CLI reads it and says so. Nobody is asked anything, and a server that sends no header produces no notice.
Running your own deployment? Then you decide what your developers are told, by deciding what you deploy — which is usually more useful than pointing everyone at the newest tag on GitHub.
The notice stays quiet when stderr is not a terminal, when XECRET_TOKEN is set
(a CI job cannot upgrade itself, and the line would land in every build log for
ever), when you have already been told about that release today, and when
XECRET_NO_UPGRADE_NOTICE is set. A newer release than the one you last saw is
raised straight away rather than waiting for tomorrow.
What you were last told is remembered in ~/.xecret/notices.json — a version
and a date, nothing else. It sits outside the cache directory on purpose, so
xecret cache clear and xecret logout are not read as "please start nagging
me again".
xecret version
xecret versionVersion, commit, build date, platform and Go version. Include this in any bug report.
xecret help
xecret help
xecret secrets helpQuick reference
# first time in a repository
xecret login
xecret init
# daily
xecret run -- npm run dev
xecret overrides set DATABASE_URL # your local value, only for you
# look at things
xecret projects
xecret environments
xecret secrets list
xecret secrets versions API_KEY
# change things
xecret secrets set API_KEY
xecret secrets get API_KEY
xecret secrets annotate API_KEY --note "issued by Stripe"
xecret import .env --dry-run
# when something went wrong
xecret secrets restore API_KEY --version 3
xecret audit --action secret.revealed --since 24h
xecret doctor
# after a vault reset: open the emailed archive, offline
xecret vault open-archive xecret-vault-archive-2026-10-12.zip
# in CI (no login, no config file)
XECRET_TOKEN=xst_… xecret run -- npm run buildNext
- Configuration and scope — where each setting comes from.
- The offline cache — behaviour when the network fails.
- Secrets in CI — service tokens end to end.