Skip to main content

gws secret

gws secret stores values that don't belong in gws.json — database passwords, API keys, tokens — encrypted in the API and injected into pods on deploy.

The project is resolved from the git remotes of the current directory; if there is no remote registered with the API, the command exits with ENO_PROJECT_KEY and tells you to run gws config import first. All subcommands require authentication (gws login).

Usage

gws secret <subcommand> [name] [options]
SubcommandDescription
setCreate or update a secret (interactive prompt by default)
listList secret metadata for the current project (values are never returned)
deleteDelete a secret by name + scope

set

Create or update a secret. Upserts on the key (name, scope, service, worktree, profile) — running set twice with the same scope updates the existing value rather than creating a duplicate.

The default value source is an interactive TTY prompt so the secret never appears in shell history or ps. --stdin is the no-history path for scripts and CI; --value works but emits a stderr warning.

Usage

# Recommended — interactive prompt
gws secret set DATABASE_URL --service api

# CI / AI-friendly — read from stdin
echo "$DATABASE_URL" | gws secret set DATABASE_URL --service api --stdin

# One-off, value visible in history (warned)
gws secret set FEATURE_FLAG --value enabled

# Worktree-scoped override
gws secret set DATABASE_URL --service api --worktree feature-payments --stdin

# File-mounted secret
gws secret set TLS_CERT --type file --mount-path /etc/tls/cert.pem --stdin

# Image-pull credential for a private registry
echo "$REGISTRY_PASSWORD" | gws secret set ghcr-pull \
--type registry --registry-hostname ghcr.io --username myorg --stdin

Output:

✓ Created secret "DATABASE_URL"

(Or ✓ Updated secret "DATABASE_URL" when an existing secret with the same scope is overwritten.)

Arguments and options

Argument / optionDescription
<name>Secret name. Required.
--service <name>Scope the secret to one service. Validated against the API's service list — unknown service names are rejected. Omit for a project-wide (global) secret.
--worktree <name>Scope the secret to one worktree. Defaults to unscoped.
--profile <name>Scope the secret to a specific configuration profile. Defaults to unscoped.
--type <env|file|registry>Storage shape. env (default) injects the value as a container env var. file mounts it into the pod (requires --mount-path). registry is a Docker-registry credential pair (requires --username and --registry-hostname).
--stdinRead the value from stdin. Use this for scripts, CI, and AI flows.
--value <value>Pass the value inline. Discouraged — visible in shell history and ps. Emits a stderr warning.
--mount-path <path>Path inside the pod to mount the secret at. Required for --type file.
--username <name>Registry username. Required for --type registry.
--registry-hostname <host>Registry hostname (e.g. ghcr.io). Required for --type registry.

If you provide both --value and --stdin, --value wins. An empty value is rejected with ECONFIG_INVALID.


list

List secret metadata for the current project. Plaintext values are never returned — only name, scope, type, service, worktree, and profile.

Usage

gws secret list                            # all secrets
gws secret list --service api # only service-scoped secrets for `api`
gws secret list --worktree feature-payments
gws secret list --json # machine-readable output

Output:

NAME          TYPE  SCOPE    SERVICE  WORKTREE          PROFILE
------------ ---- ------- ------- ---------------- -------
DATABASE_URL env service api - -
DATABASE_URL env service api feature-payments -
TLS_CERT file global - - -

With --json, the same information is emitted as a JSON array of secret-metadata objects, suitable for piping into other tools.

Arguments and options

Argument / optionDescription
--service <name>Filter to secrets scoped to a specific service.
--worktree <name>Filter to secrets scoped to a specific worktree.
--profile <name>Filter to secrets scoped to a specific profile.
--jsonEmit a JSON array of metadata objects instead of the table.

When no secrets match, (no secrets) is printed (or [] with --json).


delete

Delete a secret. Resolves by name + scope and deletes by API ID; the scope flags must match the ones the secret was created with.

Usage

gws secret delete <name>
gws secret delete <name> --service api
gws secret delete <name> --service api --worktree feature-payments

Output:

✓ Deleted secret "DATABASE_URL"

If no secret matches the supplied name + scope, the command exits with ENO_PROJECT_KEY and the message secret "<name>" not found.

Arguments and options

Argument / optionDescription
<name>Secret name. Required.
--service <name>Service scope of the secret to delete. Omit for a global secret.
--worktree <name>Worktree scope of the secret to delete.
--profile <name>Profile scope of the secret to delete.

Why interactive-by-default?

Secret leakage via shell history was the most common foot-gun in the wizard era. The interactive prompt is the default; --value exists for parity but warns; --stdin is the no-history path that AI agents and CI should use.

If you need to migrate a .env file in bulk, the AI's /gws-setup skill loops over each line and calls gws secret set <KEY> --service <svc> --stdin; you don't need to script it yourself.


See also