A small CLI that decodes Kubernetes Secrets into readable key/value pairs.
Reads a raw manifest (YAML or JSON), a custom key: value blob, or fetches a
live secret via kubectl, base64-decodes every value, and prints the result
in a few different shapes.
kubectl is only required for live-fetching a resource by name. Decoding a
file (-f) or piped stdin doesn't need it installed at all.
Four options:
1. Homebrew (macOS and Linux):
brew install alialjaffer/tap/s2tThe formula is Formula/s2t.rb in this repo, and the
alialjaffer/homebrew-tap tap
tracks it. It builds from source, so it needs a Go toolchain of at least the
version in go.mod (a dependency, sealed-secrets, sets that floor).
2. go install (needs Go 1.26+):
go install github.com/alialjaffer/s2t@latest3. Prebuilt binary from Releases, for Windows, macOS, and Linux (amd64 and arm64). Verify with checksums.txt if you like:
# macOS (Apple Silicon) example
curl -fLO https://github.com/alialjaffer/s2t/releases/latest/download/s2t-darwin-arm64
chmod +x s2t-darwin-arm64 && mv s2t-darwin-arm64 ~/.local/bin/s2t4. From source:
git clone https://github.com/alialjaffer/s2t.git
cd s2t
make installmake install runs go vet and the tests, builds the binary, and copies it to ~/.local/bin/s2t. Make sure that directory is on your PATH.
Releases are cut automatically: pushing a v* tag (e.g. v0.1.0) triggers .github/workflows/release.yml, which builds and attaches the binaries to a GitHub Release, then rewrites Formula/s2t.rb's url and sha256 for that tag and pushes it to the tap. The tap push needs a HOMEBREW_TAP_TOKEN secret in this repo holding a token with contents: write on alialjaffer/homebrew-tap.
s2t -hExamples:
s2t -f secret.yaml decode a saved manifest file
cat secret.json | s2t decode piped stdin (format auto-detected)
s2t db-creds -n prod fetch and decode a live secret via kubectl
s2t -f secret.yaml --only username,password only print specific keys
s2t -f secret.yaml -o env print as KEY=value pairs
s2t db-creds -n prod -o yaml re-encode a live secret as a patch-ready manifest
s2t -f app.yaml -k configmap decode a ConfigMap manifest instead of a Secret
s2t cm/app-config -n prod fetch a ConfigMap live; kind/name in one argument
s2t cm app-config -n prod fetch a ConfigMap live; kind and name as separate arguments
s2t diff a.yaml b.yaml compare two secrets' decoded contents key by key
s2t diff db-creds db-creds -n stg -B prod compare a live secret across two namespaces
s2t set db-creds -n prod password=s3cret upsert plaintext keys into a live secret
s2t edit db-creds -n prod edit a live secret's values in $EDITORThe resource name is a plain positional argument, just like kubectl get secret NAME -n NAMESPACE — no --name needed (though --name still works, if you prefer being explicit).
$ s2t -n ar # Auto-completion using default kubeconfig (-n is --namespace's shorthand, matching kubectl)
argocd arms-prom
$ s2t argocd-initial-admin-secret -n argocd # given the namespace and secret, print plain
password: CLG31TzP3S31XX5j
$ s2t argocd-initial-admin-secret -n argocd -oenv # print as .env file
password=CLG31TzP3S31XX5j
$ s2t argocd-initial-admin-secret -n argocd -oyaml # print as YAML string data
stringData:
password: CLG31TzP3S31XX5j
$ s2t argocd-initial-admin-secret -n argocd -ojsonc # print as compact json (single line)
{"stringData":{"password":"CLG31TzP3S31XX5j"}}| Flag | Description | Default |
|---|---|---|
[name] or [kind] [name] |
Positional: name of the resource to fetch live via kubectl — a plain name (uses --kind), kind/name (e.g. secret/my-secret, cm/my-cm), or kind name as two separate arguments |
- |
-f, --file |
Path to a file containing secret data | stdin |
-t, --format |
Input format: yaml, json, kv, or sealed-secret (sealed-secret must be requested explicitly), if empty, use any which auto-detects |
any |
-k, --kind |
Resource kind: secret or configmap |
secret |
--name |
Same as the positional [name] argument; useful if you'd rather be explicit or the name collides with a subcommand (e.g. diff) |
- |
-n, --namespace |
Kubernetes namespace (used when fetching a resource by name) | defaults to the kubeconfig's current context if omitted |
--kubeconfig |
Path to the kubeconfig file to use | ~/.kube/config |
--only |
Comma-separated list of keys to print | - |
-o, --output |
Output format: empty (plain), env, json, jsonc, or yaml (json/jsonc/yaml produce a patch-ready manifest; jsonc is compact) |
empty |
--mask |
Replace every value with a fixed-length placeholder; cannot be combined with -o json/jsonc/yaml |
false |
-h, --help |
Print usage | false |
Patch a live secret from an edited local file — -o jsonc produces a compact, single-line stringData payload that's directly usable as a kubectl patch argument (the API server base64-encodes it server-side):
kubectl patch secret db-creds -n prod --type merge -p "$(s2t -f secret.yaml -o jsonc)"Export a live secret as a .env file for local development:
s2t db-creds -n staging -o env > .envDiff two secret files' decoded values, key by key, with s2t diff (see Diffing secrets below):
s2t diff staging.yaml prod.yamlDiff a live secret across two namespaces directly, no shell trick needed:
s2t diff db-creds db-creds -n staging -B prodGrab a single value for scripting, combining --only with -o env:
PASSWORD=$(s2t -f secret.yaml --only password -o env | cut -d= -f2-)s2t -f sealed.yaml -t sealed-secret decrypts a Sealed Secrets manifest client-side (spec.encryptedData), given the sealed-secrets controller's private key. Set the S2T_SEALED_SECRETS_KEY_FILE env var to a path to that key's PEM file:
export S2T_SEALED_SECRETS_KEY_FILE=~/.config/s2t/sealed-secrets-key.pem
s2t -f sealed.yaml -t sealed-secretThe key is read from a file path, not the env var's value directly, so the key material never touches argv or the environment itself. This isn't auto-detected by -t any — it must be requested explicitly, since it depends on external key material that no other format needs.
-k configmap decodes a ConfigMap instead of a Secret. ConfigMaps store data as plain text (not base64) plus an optional binaryData for actual binary content — the opposite of a Secret's always-base64 data — so s2t handles both fields correctly and merges them into one output, whether they're used separately or together in the same manifest:
s2t -f app-config.yaml -k configmap
s2t app-config -n prod -k configmap # live fetch
s2t -f app-config.yaml -k configmap -o jsonc # patch payload wraps as {"data": ...}, not stringData-k configmap only combines with --format json, yaml, or any — kv and sealed-secret have no defined ConfigMap shape and are rejected with a clear error.
The kind can also be given positionally instead of via -k, matching kubectl's TYPE/NAME and TYPE NAME forms:
s2t secret/db-creds -n prod
s2t cm/app-config -n prod # cm is a short alias for configmap
s2t cm app-config -n prod # kind and name as two separate argumentsIf a positional kind is given and -k is also passed with a different kind, s2t rejects the conflict rather than silently picking one.
--mask replaces every value with a fixed-length placeholder (********, same length regardless of the real value's length, so it doesn't leak how long the secret actually is) — for sharing output in a screen-share, chat message, or CI log without exposing the real value:
s2t -f secret.yaml --mask--mask cannot be combined with -o json, jsonc, or yaml: those produce a kubectl patch-ready payload, and a masked value would silently overwrite the real secret with the literal string "********" if applied. s2t refuses this combination outright rather than risk it.
s2t set <name> [key=value ...] updates or adds keys on a live Secret or
ConfigMap without ever base64-encoding a value by hand. It reads plaintext from
positional key=value arguments and/or -f/stdin (JSON, YAML, or env,
auto-detected), flattens nested JSON/YAML with __, and builds a merge patch.
Secrets use stringData, which the API server encodes for you:
s2t set db-creds -n prod password=s3cret
s2t set db-creds -n prod username=admin password=s3cretThat prints a pipeable payload by default, so nothing touches the cluster until you decide:
s2t set db-creds -n prod password=s3cret -o jsonc
{"stringData":{"password":"s3cret"}}Add --apply to patch directly, with no shell pipe needed. Combine --dry-run
to validate server-side first:
s2t set db-creds -n prod password=s3cret --apply
s2t set db-creds -n prod password=s3cret --apply --dry-rundotnet appsettings. Nested JSON (or YAML) is flattened with __, so an
appsettings file maps straight onto secret keys, and a couple of ad-hoc
overrides can be layered on top of it:
cat appsettings.json | s2t set db-creds -n prod --apply
cat appsettings.json | s2t set db-creds -n prod Feature__Enabled=true --apply{"ConnectionStrings": {"Default": "Server=db"}, "Feature": {"Enabled": false}}becomes the patch {"stringData":{"ConnectionStrings__Default":"Server=db","Feature__Enabled":"true"}}
(the second command overrides the file's Feature.Enabled=false).
Arrays are indexed (Servers__0, Servers__1), and a command-line pair always
wins over the same key from a file/stdin (with a warning). Keys must be valid
Kubernetes keys (alphanumerics, -, _, .); -k configmap patches a
ConfigMap's plaintext data instead.
A positional pair is always key=value, and multiple pairs can be comma-joined
in one argument (key1=val1,key2=val2) or passed separately. Because commas
separate pairs, use JSON/YAML input for a value that itself contains a comma.
| Flag | Description | Default |
|---|---|---|
-n, --namespace |
Namespace of the target resource | kubeconfig's current context |
-f, --file |
Plaintext key/value file (JSON, YAML, or env); stdin if omitted | stdin |
-t, --format |
Input format: any, env, json, or yaml (kv is treated as env) |
any |
-k, --kind |
secret or configmap |
secret |
-o, --output |
Patch payload format: jsonc, json, or yaml |
jsonc |
--apply |
Patch the cluster with kubectl patch instead of printing |
false |
--dry-run |
With --apply, validate with kubectl --dry-run=server without writing |
false |
--separator |
Separator used to flatten nested JSON/YAML keys | __ |
--kubeconfig |
Path to the kubeconfig file to use | same resolution as the root command |
s2t edit <name> -n <ns> fetches a live resource, shows only its data keys
(no metadata, labels, or annotations) in $EDITOR, and prints a merge patch for
whatever you changed: added/updated keys go through stringData, deleted keys
are removed with a null in data. It then asks whether to apply:
s2t edit db-creds -n prod
s2t edit cm/app-config -n prod -o yaml # use yaml/json for multi-line values{
"stringData": {
"password": "NEWPASS"
},
"data": {
"deprecated-key": null
}
}
Apply this patch? [y/N]:
Empty input (or a non-interactive stdin) defaults to No; nothing is written
until you answer y. Values are shown as flat keys, edited in env
(KEY=value), yaml, or json format; env refuses values containing
newlines, so use yaml/json for those.
s2t diff <fileA> <fileB> compares two manifests' decoded contents key by key — added, removed, and changed keys, not a raw line diff:
s2t diff staging.yaml prod.yaml- deprecated-key
+ new-feature-flag
~ password
Values are hidden by default (diff output is more likely to end up pasted into a ticket or chat than a single decode). Pass --show-values to see the actual old/new values for changed keys:
s2t diff --show-values staging.yaml prod.yaml- deprecated-key: old-value
+ new-feature-flag: enabled
~ password: staging-pass -> prod-pass
s2t diff shares --format/-t and --kind/-k with the root command (both sources are assumed to be the same format/kind).
Either (or both) s2t diff argument can be a live resource name instead of a file — s2t checks whether the argument names a real file on disk first, and falls back to a live kubectl fetch otherwise, so a plain filename keeps working exactly as before:
s2t diff db-creds db-creds -n staging -B prod # same secret, two namespaces
s2t diff cm/app-config cm/app-config -n stg -B prod # same, for a ConfigMap
s2t diff old-secret-name new-secret-name -n prod # two different names, one namespace
s2t diff staging.yaml db-creds -n prod # a file against a live secret| Flag | Description | Default |
|---|---|---|
-n, --namespace |
Namespace for either argument that's a live resource, unless overridden per-side by -A/-B |
kubeconfig's current context |
-A, --namespace-a |
Namespace for the first argument, if it's a live resource; overrides --namespace |
- |
-B, --namespace-b |
Namespace for the second argument, if it's a live resource; overrides --namespace |
- |
--kubeconfig |
Path to the kubeconfig file to use for live resources | same resolution as the root command |
A live argument accepts kind/name (e.g. cm/app-config) to override --kind for that side only, matching the root command's own positional syntax.
make build # go vet + go test + build to bin/
make test # go test ./...
make vet # go vet ./...
make clean # remove bin/See CONTRIBUTING.md for details on contributing. Release notes are in CHANGELOG.md.