Last updated

Password Managers

The ws CLI reads its API token from the WS_TOKEN environment variable, and WS_TOKEN takes priority over any token stored in ws.toml or ~/.config/ws/config.toml. That means you can keep your config files tokenless and let a password manager, secret manager, or OS keychain supply the token for each command.

Note: WS_TOKEN overrides file configuration, so the examples below work with a ws.toml that contains only url, defaults.workspace_key, and status_aliases. See CLI Configuration for the full priority order.

The Pattern

Every example has the same shape: fetch the secret, pass it as WS_TOKEN, run the command.

WS_TOKEN=<fetch the secret> ws task mine

Prefer this one-shot form over exporting the variable. A one-shot token exists only for that single command. If you export WS_TOKEN=..., every program you start in that shell inherits it until the shell exits.

# Token available for one command only (recommended)
WS_TOKEN=$(op read "op://Dev/windshift/credential") ws task mine

# Token available for the rest of the shell session
export WS_TOKEN=$(op read "op://Dev/windshift/credential")
ws task mine

Why Not Put the Token in ws.toml

ws.toml is a project file. Projects get shared, committed, and cloned, and a token in that file overrides the global token for everyone who uses the checkout. Keep the token out of it:

ws.toml
WINDSHIFT.md

1Password

Create a .env file (gitignore it) containing an op:// reference:

# .env
WS_TOKEN=op://Dev/windshift/credential

Then run any ws command through op run:

op run --env-file=.env -- ws task mine

With op read

WS_TOKEN=$(op read "op://Dev/windshift/credential") ws task mine

Bitwarden

Unlock the vault once per shell, then read the item:

export BW_SESSION=$(bw unlock --raw)
WS_TOKEN=$(bw get password windshift) ws task mine

pass

WS_TOKEN=$(pass show windshift/token) ws task mine

HashiCorp Vault

WS_TOKEN=$(vault kv get -field=token secret/windshift) ws task mine

Doppler

Store the token as WS_TOKEN in your Doppler config, then run the CLI through Doppler:

doppler run -- ws task mine

Infisical

Store the token as WS_TOKEN in your Infisical project and environment, then:

infisical run -- ws task mine

If the directory is not linked to a project, pass the identifiers:

infisical run --projectId <project-id> --env prod -- ws task mine

Or fetch a single secret:

WS_TOKEN=$(infisical secrets get WS_TOKEN --plain --env=prod) ws task mine

macOS Keychain

Store the token once:

security add-generic-password -a "$USER" -s windshift-token -w "crw_..." -U

Then read it per command:

WS_TOKEN=$(security find-generic-password -a "$USER" -s windshift-token -w) ws task mine

Cloud Secret Managers

AWS Secrets Manager

WS_TOKEN=$(aws secretsmanager get-secret-value --secret-id windshift/token \
  --query SecretString --output text) ws task mine

Google Cloud Secret Manager

WS_TOKEN=$(gcloud secrets versions access latest --secret=windshift-token) ws task mine

Azure Key Vault

WS_TOKEN=$(az keyvault secret show --vault-name my-vault --name windshift-token \
  --query value -o tsv) ws task mine

Shell Sessions with direnv

If you always want the token available in a specific checkout, use direnv with a gitignored .envrc:

# .envrc
export WS_TOKEN="$(op read 'op://Dev/windshift/credential')"
direnv allow

After that, plain ws task mine works in that directory. Keep secret values out of a committed .envrc.

CI

Inject WS_TOKEN from the CI secret store. Do not write it into ws.toml. When WS_URL and WS_TOKEN are set, the CLI does not need a config file at all. If you do want a file, ws config init --non-interactive reads the same environment variables.

GitHub Actions

env:
  WS_URL: https://windshift.example.com
  WS_WORKSPACE: ENG
  WS_TOKEN: ${{ secrets.WS_TOKEN }}

GitLab CI

variables:
  WS_URL: https://windshift.example.com
  WS_WORKSPACE: ENG
  WS_TOKEN: $WS_TOKEN

Troubleshooting

  • "API token not configured" — WS_TOKEN is not set in the process that runs ws. Exporting it in a parent shell does not reach a command launched from elsewhere.
  • op, bw, vault, or infisical not found — the manager's CLI is not on PATH for that shell or CI job.
  • Authentication fails with a token that looks correct — the secret may contain a trailing newline or multiple lines. Command substitution strips trailing newlines, but a multi-line secret does not. Fetch a single field, or pipe through head -n1.