Skip to content

Migrating to secure credential storage

GTB v1.12 completes Phases 1โ€“3 of the credential-storage-hardening spec. This guide helps existing users and automated pipelines move off plaintext config.yaml storage onto one of the two secure modes GTB supports:

Mode Config records Secret lives
Env-var reference (default) <prefix>.env: <VAR_NAME> Your shell profile / CI platform secret
OS keychain <prefix>.keychain: <service>/<account> macOS Keychain / Linux Secret Service / Windows Credential Manager

The gtb config migrate-credentials command automates the move. This guide covers both interactive (workstation) and non-interactive (CI/CD) workflows.

What will be migrated

Running migrate-credentials scans the loaded config for every known literal credential key:

  • AI providers: anthropic.api.key, openai.api.key, gemini.api.key
  • VCS tokens: github.auth.value, gitlab.auth.value, gitea.auth.value, codeberg.auth.value, direct.auth.value
  • Bitbucket dual-credential pair: bitbucket.username + bitbucket.app_password (migrated together as one unit)

Credentials stored in other modes โ€” <prefix>.env or <prefix>.keychain โ€” are left untouched.

Interactive migration on a workstation

The default flow prompts for each credential: env var name, copy-to-shell instructions, and a verification check that the variable is exported before rewriting the config.

# Preview without mutating anything
mytool config migrate-credentials --dry-run

# Run interactively
mytool config migrate-credentials

The interactive flow looks like:

Env var name for anthropic.api.key
Credential currently stored as literal. Choose the env var that will
hold it. Default is the upstream ecosystem standard.
> ANTHROPIC_API_KEY

Set ANTHROPIC_API_KEY in your shell profile:

    export ANTHROPIC_API_KEY=<paste the current anthropic.api.key value>

The config file's literal entry will be cleared; the env var will be the
single source of truth going forward.

Have you set the env var? Yes, continue

After confirmation, the wizard verifies $ANTHROPIC_API_KEY is set in the current process. If not, the migration aborts for that credential โ€” your literal remains in place until you export the variable and re-run.

Non-interactive migration (CI/CD and scripts)

Pass --yes to skip every prompt. Env-var names fall back to the upstream-standard defaults:

# Silent migrate to env-var references using defaults
mytool config migrate-credentials --yes

# Pin a specific env var name for a particular credential
mytool config migrate-credentials --yes \
  --env-var anthropic.api.key=MYAPP_ANTHROPIC_KEY \
  --env-var openai.api.key=MYAPP_OPENAI_KEY

# Skip the "did you export it?" verification โ€” useful when the env
# vars are injected by systemd, CI runners, or /etc/profile rather
# than an interactive shell
mytool config migrate-credentials --yes --skip-verify

Migrating to the OS keychain

Keychain target requires the tool to have pkg/credentials/keychain imported in its main. The command refuses with an actionable error otherwise:

# Migrate all literals to the registered keychain backend
mytool config migrate-credentials --target=keychain --yes

# Override the keychain service (default: tool name)
mytool config migrate-credentials --target=keychain --yes \
  --keychain-service=myteam-mytool

Entries land under <service>/<account>. For Bitbucket, the dual credentials are serialised into a single JSON blob under one account (matching the format the resolver expects). Other credentials use one entry per secret.

Cascading target from config

Tools that don't want to require a --target flag on every invocation can set the default in their config:

credentials:
  migrate:
    default_target: keychain   # or env (default if unset)

When present, this acts as the fallback for --target when omitted. Explicit --target on the command line still wins.

Idempotence

The command is safe to re-run. Credentials that already have the target configuration (e.g. a prior migration already created anthropic.api.env) are reported as skipped:

Migration complete:

  SKIP anthropic.api.key (already migrated)
  github.auth.value โ†’ github.auth.env = GITHUB_TOKEN (target: env)

Before / after

Before (typical post-interactive-init literal config):

anthropic:
  api:
    key: sk-ant-api03-<redacted>
github:
  auth:
    value: ghp_<redacted>
bitbucket:
  username: alice
  app_password: ATBB-<redacted>

After migrate-credentials --yes (env-var target):

anthropic:
  api:
    env: ANTHROPIC_API_KEY
github:
  auth:
    env: GITHUB_TOKEN
bitbucket:
  username:
    env: BITBUCKET_USERNAME
  app_password:
    env: BITBUCKET_APP_PASSWORD

After migrate-credentials --target=keychain --yes:

anthropic:
  api:
    keychain: mytool/anthropic.api
github:
  auth:
    keychain: mytool/github.auth
bitbucket:
  keychain: mytool/bitbucket.auth   # single JSON-blob entry for both fields

The rewritten config file is written with 0600 permissions (matching the initial setup) via an atomic temp-file + rename, so an interrupted migration does not corrupt the existing config.

Flag reference

Flag Default Effect
--dry-run false Print the plan; don't write config or touch keychain
--target=env\|keychain env (or credentials.migrate.default_target from config) Destination storage mode
-y, --yes false Skip all prompts; use defaults for env var names
--env-var <key>=<name> (none) Override the env var name for a specific credential. Repeatable.
--skip-verify false Don't wait for the user to export the variable before rewriting config
--keychain-service <name> props.Tool.Name Service portion of the <service>/<account> keychain ref