Using skret from a script or agent
Exit codes, non-interactive flags, and copy-paste recipes for running skret from CI, cron, or an AI agent.
Every skret command is non-interactive by default — the exceptions are skret delete and skret rollback without --confirm/--force (both prompt y/N) and --from-stdin at a real terminal (blocks until you send EOF, see below). Output streams are deliberate: stdout carries data (secret values, JSON, rendered templates), stderr carries status (“Set KEY”, warnings, progress). Every failure exits with a distinct, documented code you can branch on instead of parsing stderr text.
Exit codes
Section titled “Exit codes”skret returns a distinct exit code per failure class, defined in pkg/skret/errors.go:
| Code | Constant | Meaning |
|---|---|---|
| 0 | ExitSuccess |
Operation completed successfully |
| 1 | ExitGenericError |
Unclassified error |
| 2 | ExitConfigError |
.skret.yaml missing or invalid |
| 3 | ExitProviderError |
Backend provider call failed (e.g. AWS SSM) |
| 4 | ExitAuthError |
Authentication failed |
| 5 | ExitNotFoundError |
Secret does not exist |
| 6 | ExitConflictError |
Key already exists (import --on-conflict=fail) |
| 7 | ExitNetworkError |
Network/connectivity failure |
| 8 | ExitValidationError |
Invalid input — bad flag combination, missing required value, experimental command not enabled |
| 9 | ExitDrift |
skret diff --exit-code found a difference between the two sides |
| 10 | ExitLeakFound |
skret scan found a managed secret value in a tracked (or --staged) file |
| 125 | ExitExecError |
skret run -- could not exec the command (not found on $PATH, or exec failure) |
Two of these are the ones you’ll branch on most in automation:
skret scanexits 10 when a managed secret value shows up in a file — wire it into a pre-commit hook or a CI leak-guard step. It exits 0 when nothing is found.skret diff A B --exit-codeexits 9 when the two secret sets differ, the same non-zero-on-difference contract asgit diff --exit-code. Without--exit-code,diffalways exits 0 — it’s a report, not a gate, unless you ask it to be one.
See the Error Codes reference for the full table plus provider-specific error mappings and remediation per code.
JSON error envelope
Section titled “JSON error envelope”Every command inherits a --format flag (table by default). When a command fails with --format json, stderr carries a parseable object instead of the plain-text message — the exit code and the JSON body’s code field always agree, so a caller can json.Unmarshal stderr instead of pattern-matching prose:
skret get MISSING_KEY --format json{ "error": "Secret not found. Use 'skret set MISSING_KEY <value>' to create it.: local: get \"MISSING_KEY\": secret not found", "code": 5}remediation appears only when the error carries a copy-pasteable fix hint (for example, an auth failure suggesting the exact skret auth login command to run) — omitted entirely otherwise, so don’t assume the key is always present. A command that already defines its own local --format flag (list, env, diff, scan, and the write-path commands below) uses that flag’s value for its own error rendering too, so skret delete MISSING --format json gets the envelope from that command’s --format, not the root one. get is the one exception — it has its own --json boolean instead of --format — but the root --format flag still works for it (skret get MISSING_KEY --format json above), since get has no local flag of that name to shadow it. The root flag exists so every command without an output-format flag of its own can still opt into the envelope.
Non-interactive checklist
Section titled “Non-interactive checklist”- Exact bytes out:
skret get KEY --plain. The defaultget(no--plain) appends one trailing newline for terminal readability;--plaingives you the value’s exact stored bytes with nothing added — use it whenever a script or agent needs the byte-exact value (skret get TOKEN --plain > token.bin). - Parseable dump:
skret env --format=json(alsoyaml,export, or thedotenvdefault) — all four formats round-trip byte-exact; pickjsonwhen a script needs to parse the whole environment. - Multi-line value in:
skret set KEY --from-stdin < file.pemorskret set KEY --from-file path. Both read the entire stream/file (not just the first line), so a PEM key or multi-line JSON blob survives with every embedded newline intact. - A value that starts with
-: pass--before the key, or it’s parsed as a flag:skret set -- KEY '-----BEGIN PRIVATE KEY-----...'. - Secrets are byte-exact everywhere except
run:get,env,template, andsync/importpreserve every byte, including NUL, CR, and embedded newlines.skret run/skret run --watchsanitize control bytes on the way into the child process’s environment, because an OS process environment can’t carry a NUL or embedded newline. Full detail: Value fidelity. - Multiple config namespaces from one process: the global
--config <path>flag loads a specific.skret.yamldirectly, bypassing directory discovery entirely — useful for a single cron container serving several projects, each declared in its own config file. If the file does not exist the command fails with a config error; it never silently falls back to discovery. See Configuration.
JSON output on the write path
Section titled “JSON output on the write path”get, list, env, diff, and scan have had --json/--format json since the read path was built. set, delete, history, and sync now carry the same --format json (default table), so a script driving a full rotation (set → verify → sync) never has to parse an English status line to know whether a write landed:
skret set API_KEY --from-stdin --format json < new-key.txt{ "key": "API_KEY", "path": "/myapp/prod", "version": 3, "created": false}created is true only when the key did not exist before this write; the secret value is never included in the payload, in any command’s JSON output. --format json replaces the human status line (Set KEY, Deleted KEY, Synced N secrets to TARGET) rather than printing both — the payload goes to stdout, so skret set ... --format json | jq .created works without stderr noise mixed in.
| Command | --format json payload |
|---|---|
set |
{"key", "path", "version", "created"} |
delete |
{"key", "path", "deleted"} |
history (SKRET_EXPERIMENTAL=1) |
[{"version", "value", "updated_at", "author"}, ...] — value is masked unless --verbose is also passed, the same policy the table already applies |
sync |
[{"source", "target", "synced"}, ...], one entry per target actually written; see Sync |
Every one of these composes with the JSON error envelope above: skret delete MISSING --format json fails with {"error": ..., "code": 5} on stderr instead of the success payload on stdout.
Recipes
Section titled “Recipes”Read one value
Section titled “Read one value”DB_URL=$(skret get DATABASE_URL --plain)Inject secrets into a command
Section titled “Inject secrets into a command”skret run -- ./serverEvery secret in the resolved environment is injected as a real process environment variable; skret forwards the child’s exit code (or exits 125 if the command itself can’t be found/exec’d).
Dump everything for a script to parse
Section titled “Dump everything for a script to parse”skret env --format=json | jq -r '.DATABASE_URL'Sync to CI/CD targets
Section titled “Sync to CI/CD targets”skret sync --to=github,cloudflaregithub needs GITHUB_TOKEN in the environment. cloudflare has no flags-only path — it must be declared under sync.targets in .skret.yaml (worker or pages) and needs CLOUDFLARE_API_TOKEN. Preview first with no writes: skret sync --to=github --dry-run prints the exact secret names it would push and exits without writing anything, saving sync state, or touching a dotenv file. Add --no-overwrite to only fill keys absent at the target (existing values are never overwritten) — the safer default for an agent driving sync unattended. See Sync.
Leak-guard in a pre-commit hook
Section titled “Leak-guard in a pre-commit hook”skret scan --staged || { echo "a managed secret would be committed" >&2; exit 1; }Exits 10 on a match, 0 when clean. --staged scans only staged content (git diff --cached), which is what a commit hook wants. See Scan.
Migrate an existing .env in
Section titled “Migrate an existing .env in”skret import --from=dotenv --file=.env --on-conflict=skip--on-conflict defaults to skip (silently skip keys that already exist); pass --on-conflict=fail to exit 6 (ExitConflictError) instead the first time a key collides, or --on-conflict=overwrite to replace it.
Gotchas
Section titled “Gotchas”-
Leading-dash values.
skret set KEY -----BEGIN...fails — skret’s flag parser reads-----BEGIN...as a flag, not a value. Useskret set -- KEY value, or avoid the problem entirely with--from-stdin/--from-file. -
--from-stdinat an interactive terminal blocks until EOF. It reads the whole stream, not one line, so with no pipe or redirect it will hang waiting for input — type the value, then send EOF yourself: Ctrl-D on macOS/Linux, Ctrl-Z then Enter on Windows. In a script, always pipe or redirect:... --from-stdin < fileorecho -n "$VALUE" | skret set KEY --from-stdin. -
Trailing newlines are stripped; embedded ones are not.
--from-stdinand--from-fileremove only a trailing run of\nbytes (so a value saved by a text editor round-trips without gaining an extra newline). A trailing\r, or any newline in the middle of the value, is left untouched. See Value fidelity for the exact byte-level rules. -
skret historyandskret rollbackare experimental and gated. Both requireSKRET_EXPERIMENTAL=1in the environment; without it they exit 8 (ExitValidationError) with an explanatory message instead of running:Terminal window SKRET_EXPERIMENTAL=1 skret history DATABASE_URLSKRET_EXPERIMENTAL=1 skret rollback DATABASE_URL 3 --confirm