Skip to content

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.

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 scan exits 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-code exits 9 when the two secret sets differ, the same non-zero-on-difference contract as git diff --exit-code. Without --exit-code, diff always 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.

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:

Terminal window
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.

  • Exact bytes out: skret get KEY --plain. The default get (no --plain) appends one trailing newline for terminal readability; --plain gives 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 (also yaml, export, or the dotenv default) — all four formats round-trip byte-exact; pick json when a script needs to parse the whole environment.
  • Multi-line value in: skret set KEY --from-stdin < file.pem or skret 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, and sync/import preserve every byte, including NUL, CR, and embedded newlines. skret run/skret run --watch sanitize 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.yaml directly, 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.

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:

Terminal window
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.

Terminal window
DB_URL=$(skret get DATABASE_URL --plain)
Terminal window
skret run -- ./server

Every 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).

Terminal window
skret env --format=json | jq -r '.DATABASE_URL'
Terminal window
skret sync --to=github,cloudflare

github 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.

Terminal window
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.

Terminal window
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.

  • Leading-dash values. skret set KEY -----BEGIN... fails — skret’s flag parser reads -----BEGIN... as a flag, not a value. Use skret set -- KEY value, or avoid the problem entirely with --from-stdin/--from-file.

  • --from-stdin at 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 < file or echo -n "$VALUE" | skret set KEY --from-stdin.

  • Trailing newlines are stripped; embedded ones are not. --from-stdin and --from-file remove only a trailing run of \n bytes (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 history and skret rollback are experimental and gated. Both require SKRET_EXPERIMENTAL=1 in the environment; without it they exit 8 (ExitValidationError) with an explanatory message instead of running:

    Terminal window
    SKRET_EXPERIMENTAL=1 skret history DATABASE_URL
    SKRET_EXPERIMENTAL=1 skret rollback DATABASE_URL 3 --confirm