vike
Set up

vike-cli secrets

Inspection of the one credential store — where it is, which key names it holds, which accounts those names resolve to, an empty grid to start one from — and the one narrow writer: set a single key, value from stdin or an environment variable, never from the command line.

There is one credential store, <project>/settings/secrets.env, and secrets is how you look at it without opening it in an editor. Three subcommands read; one writes, and only in one shape: set upserts a single named key into a store that already exists. There is deliberately no rotate, no delete, no --out on template and no way to create a store from here: it is the user's only copy of live venue credentials, so creating, moving or rewriting it is their decision, made with an editor. path prints the location so they can.

Subcommands and flags

invocationwhat it does
secrets listthe key names in the store — never a value — plus the accounts those names resolve to
secrets paththe store's path, whether it is there, and whether its mode exposes it
secrets templatean empty credential grid on stdout — per roster venue, the SIM/DEMO/LIVE × _API_KEY/_API_SECRET/_API_PASSPHRASE rows plus that venue's attribution keys. Not every name the workspace reads: the legacy MAINNET tier and the bespoke shapes are absent by design, and the emitted header says so
secrets set KEYupsert one credential into the project's existing store, the value read from stdin (one line) — see set
--from-env NAMEset only: take the value from the environment variable NAME instead of stdin, verbatim
--file PATHinspect this store instead of the project's (list, path). Refused on set: a write never takes an operator-supplied destination
--venue IDtemplate only: emit one roster venue's rows. Passing it to list or path is a named usage error rather than a silently ignored flag
-h, --helpthe usage text, on stdout, exit 0

Which file, exactly

path answers the question an operator must be able to settle without reading source: which file are my keys actually coming from? The settings directory is found by walking up from the working directory, and VIKE_SETTINGS_DIR names it outright, so the answer depends on where you are standing. Both inspecting subcommands take the same resolved directory the dispatcher hands every other subcommand, so this output cannot drift from what a daemon loads.

store:  /srv/vike-tradehub/settings/secrets.env (present)

Absent has three answers, not two

Path::exists maps every error to false, so a project root this process cannot search once reported the store as absent and then printed the fresh-install advice for what was really a permissions problem. crates/vike-cli/src/cmd/secrets.rs's Presence uses Path::try_exists instead: Ok(false) is NotFound and nothing else. Present, absent, and could not be determined each print different words — the third shares no word with the other two, because an operator greps this line. An undetermined answer is a finding, not a refusal: it prints the path, says what was not established, and still exits 0, since path is what you run when something is already broken.

When the store is genuinely absent, path prints the three commands that create one — mkdir -p, $EDITOR, and chmod 600 … # it is plaintext credentials — and, if a pre-one-store .env is sitting beside it, vike_secrets::legacy_store_warning says so rather than letting nothing here reads any other location stand unqualified.

Permissions are reported, never enforced

Both subcommands warn about an over-permissive mode. list always did, because it opens the store and vike_secrets::resolve hands the finding back with the credentials; path never did, because it opens nothing — so the command documented as the safe first one was the one that stayed silent about a world-writable credential file. Measured on a clean install at modes 600, 640, 644, 664 and 666, path printed zero warnings at every one while list warned from 640 up. path now asks the same question through vike_secrets::permission_warning, which lstats the file and follows a symlink only far enough to readlink and stat it — never its contents. A finding is never a refusal, so no exit code changes.

list — names, and the accounts behind them

The header names the file before the keys, because where did these come from is the question the list itself cannot answer. Then the key names, then the accounts, derived from those same names through vike_model::account_keys::accounts_in_store. That second block exists because two spellings look identical to a reader and are not:

HYPERLIQUID_LIVE_API_KEY__ALT     an account named ALT
HYPERLIQUID_LIVE_API_KEY_ALT      one underscore, and NOT an account

The grammar splits at a double underscore (vike_model::account_keys::ACCOUNT_SEPARATOR), so the single-underscore spelling leaves the whole string as one base name belonging to the default account — a credential that is silently inert. An account row is proof the labelled key was understood. The row renders as venue/TIER label, with the unlabelled account shown as (default) rather than AccountLabel's own DEFAULT, which is the one spelling AccountLabel::parse refuses.

This is a store disclosure and says nothing about arming

An account listed here is an account whose credentials exist. Whether it mounts is a separate, policy question, answered one level below the venue ceiling: vike_config::VenuePolicy::account resolves an account's mode as the lower of the venue's own line and that account's line in policy.toml's [accounts] table, and vike_mount::accounts_to_mount mounts a labelled account only when the answer lands above paper.

A labelled account with no line of its own therefore resolves paper: the venue line was written when the venue had one account, and reading it as consent for a second is the silent escalation the ceiling exists to prevent. Names the grammar recognises as no credential contribute no row, and that is a classification rather than a gap: attribution codes, the POLY_* L2 trio, Dukascopy's numbered sub-account, Aster's TESTNET tier.

template — a shape, and a shape is safe to paste

Every name is derived from the venue roster (vike_model::venues::VENUES) crossed with the current tiers and suffixes in vike_model::credential_keys, so a venue added to the roster appears the same day; an unknown --venue is an error naming the roster rather than an empty emission. The header states the rule people miss: arming is not a credential question alone — policy.venues.<venue> is a ceiling consulted before the credential is read, and it defaults to paper for every venue.

The header's other ⚠ block states what the grid cannot contain, and that gap is structural rather than an oversight: a venue whose credentials have a bespoke shape is derivable from no central table, so its keys live only in that bridge's own crates/bridges/<venue>/src/config.rs and are written by hand — the FX brokers' login/password pair, the on-chain venues' _PRIVATE_KEY, the POLY_* trio. The pre-rename MAINNET tier is omitted for a different reason: the loader still reads it, so it stays in the registry's grid, but teaching a spelling we are retiring to somebody writing their first store would be its own kind of wrong. vike_model::credential_keys::starter_keys draws both lines; lookup_keys — which this command never calls — is what answers every name a computed lookup in this workspace can ask for.

Output goes to stdout and there is no --out flag, on purpose: the moment this command owns a path it can truncate a live store. Redirecting it yourself is your own keystroke against your own path.

set — the one writer, and the shape it is held to

printf %s "$SECRET" | vike-cli secrets set BINANCE_DEMO_API_KEY      # the value on stdin, one line
vike-cli secrets set BINANCE_LIVE_API_KEY --from-env BINANCE_KEY      # the value from $BINANCE_KEY

The CLI stayed read-only for a long time on four arguments — the store is the user's only copy, a writer invites scripts to regenerate the file wholesale, a value in a shell history is a leak, and nothing on the agent surface may ever write a credential — and the writer that finally exists is the one shape those arguments allow. crates/vike-cli/src/cmd/secrets.rs's run_set is the code, and the order it does things in is the argument:

  1. The key is validated first, before anything is read or asked of stdin, against the key grid vike_model::credential_keys::key_owner can read — so a refused key never consumed the secret you piped on its way to the error. An unknown name is refused by name, with the nearest real names beside it: the store is a flat file, so a hand-typed BINANCE_LIVE_API_KEY_ would otherwise be written as happily as the real one and the venue would stay on paper with no error anywhere. The one exception is a labelled second-account key such as HYPERLIQUID_LIVE_API_KEY__ALT — read, listed, and not settable from here, because the grid is a fixed enumeration and a label is not. Its refusal offers nothing to copy: the nearest name is the default account's live key, and writing this value there would overwrite the credential that account signs with.
  2. The store must already exist. set upserts into the project's store — the same file path prints, under the same VIKE_SETTINGS_DIR — and creates none: with no store the command refuses and prints the two lines that make one (template redirected, then chmod 600). A store that is there and cannot be read is an error, never "not configured", the same distinction list draws. A permissive mode is reported on stderr and the write proceeds.
  3. The value is taken last, from stdin or from the named variable, and never from the command line: a second positional — and, deliberately, any unrecognised token, a mistyped flag included — is refused with a message that quotes nothing you typed, because the token is the secret and an error echoing it would put it into the scrollback this refusal exists to keep it out of. Stdin is trimmed of the newline a shell or editor adds; a --from-env value is taken verbatim, so a credential with real leading or trailing whitespace survives, quoted. Both refuse empty (an empty value arms nothing, and reporting success for it is the failure class this project deletes settings keys over) and both refuse more than one line: a multi-line value written verbatim became a truncated credential plus a second KEY=VALUE the store never validated — an injection, not an untidiness — so --from-env names the variable and stops.
  4. The write is a second call site of the one upsert, vike_secrets::save_credentials: the named key is replaced in place or appended, every other line, comment and blank stays byte-identical, the store's mode and a symlinked store's link are preserved, and the result lands atomically. There is no second writer to drift from the GUI's.
  5. The write is recorded in the change journal, as the CLI actor, by key name and store — never by value — and a journal failure does not fail the call: the credential is on disk, and an error path for a write that succeeded is worse than a missing ledger line.

What it prints on success is one line, KEY replaced in PATH or KEY appended in PATH, and nothing else. vike-cli secrets list afterwards shows the name; no invocation of this command ever shows a value.

The agent surface has no counterpart. vike-cli mcp advertises no tool that writes a credential, and a test holds the tool roster to that — an agent that needs a key armed is told to ask the operator, who runs the line above.

See also

On this page