Get a code from a provider that holds the seed for you. The machine running the agent never has the secret, the provider logs every request, and access is revoked by disabling a token rather than by chasing files. Fixes the failure that wastes the most time here: a vendor CLI silently falling back to an interactive unlock, so an unattended job hangs until it is killed.
Which skill, in one question
Do you hold the seed? If yes — a file, keychain entry, or otpauth:// URI on
this machine — that is totp-generate plus totp-secret-store. If the seed lives in
a vault you authenticate to and never see, it is this skill. op item get --otp
computes locally inside the op binary, but you never hold the seed, so it is here.
When NOT to use
- Generating from a seed you have →
totp-generate. - Choosing where a seed you hold should live →
totp-secret-store. - Serving codes to MCP clients →
totp-mcp-server. Driving the login →login-2fa-flow. - Verifying your end users' TOTP codes (you are the service, they are the user) — that is Twilio Verify / an application TOTP library, the opposite direction. Not this skill.
Provider reality check
| Provider | How a code is fetched | Auth |
|---|---|---|
| 1Password | op item get ITEM --otp; op read "op://v/i/one-time password?attribute=otp"; SDK secrets.resolve(...) |
OP_SERVICE_ACCOUNT_TOKEN |
| Bitwarden | bw get totp ITEM_ID |
BW_SESSION from bw unlock --raw |
| HashiCorp Vault | GET /v1/totp/code/:name — plain HTTP, no client needed |
X-Vault-Token |
| 2FAuth (self-hosted) | GET /api/v1/twofaccounts/{id}/otp |
Authorization: Bearer PAT |
| Google Authenticator | No API exists. | — |
Google Authenticator is a phone app with no server API and no CLI. Its only egress is
the otpauth-migration://offline?data=… export QR — an undocumented, reverse-engineered
protobuf. If someone asks to "use the Google Authenticator API", say it does not
exist and pick from the rows above. Decode the migration blob once into otpauth://
URIs and move on.
Best pure-HTTP API: Vault (one GET, a token header, every request audited). Best if the team already has it: 1Password (service accounts are built for this). Best fully self-hosted with a UI: 2FAuth.
Workflow
1. Check the credential before you call anything
FETCH="${CLAUDE_SKILL_DIR}/scripts/fetch_code.py"
python3 "$FETCH" check # which providers are installed and credentialed
op and bw prompt for an interactive unlock when their credential is missing —
in CI or an unattended agent that is a hang, not an error. The script checks first,
closes stdin, and enforces a 30 s timeout, so it always fails fast and loudly.
2. Fetch
python3 "$FETCH" 1password "GitHub" --vault Automation --min-validity 5
python3 "$FETCH" bitwarden 99ee88d2-0000-0000-0000-000000000000 --min-validity 5
python3 "$FETCH" vault github --addr https://vault.internal:8200
python3 "$FETCH" 2fauth 3 --url https://2fauth.internal
Use --min-validity 5 whenever the code is going into a form. A provider returns
bare digits with no hint of how long they last, and the network round trip is pure
loss on top. TOTP windows are absolute — floor(unix_time / period) is the same
window for you and for the issuer — so the script can wait for a fresh window
before fetching. Without it, a code fetched near a boundary is rejected in a way
indistinguishable from a wrong seed, and it costs one of your two login attempts.
Pass --period if the account does not use 30 seconds.
One code on stdout, exit 0. Non-zero and nothing on stdout otherwise: 3 not found
or ambiguous, 5 credential missing/expired, 6 timeout, 7 provider error. This
is the same code command contract the rest of these skills consume — anything
that prints one code and exits 0 is interchangeable. The script also refuses output
that is not exactly one code-shaped line, so a bw sync notice or an op
deprecation banner can never be submitted as a code.
Raw vendor equivalents, when the script is not available:
op item get "GitHub" --otp # not with --fields
op read "op://Automation/GitHub/one-time password?attribute=otp"
bw get totp "$ITEM_ID" --nointeraction
curl -sH "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/totp/code/github" | jq -r .data.code
curl -sH "Authorization: Bearer $TWOFAUTH_TOKEN" "$TWOFAUTH_URL/api/v1/twofaccounts/3/otp" | jq -r .password
3. Scope the credential
- 1Password: a service account with access to one dedicated vault. Service
accounts cannot reach Private vaults, and
--vaultis required once more than one is visible.OP_CONNECT_HOST/OP_CONNECT_TOKENtake precedence over the service account token — unset them if a Connect setup is interfering. - Vault: a policy granting
readon exactlytotp/code/<name>. Do not granttotp/keys/*, which exposes the seed URL. - 2FAuth: a personal access token, revocable in Settings. It is valid until revoked.
- Always prefer a dedicated machine account at the upstream service over a human's personal 2FA.
4. Keep it out of the logs
Never echo the token. A fetched code is short-lived but is still a credential — do not print it in CI output, and remember CI secret masking only covers registered secrets, not values a provider returns at run time.
Output spec
- One code on stdout; the token never appears in output, logs, or the transcript.
- The provider, the item identity used, and the credential source were stated.
- Non-zero exits surfaced verbatim — each message names the fix.
Gotchas
| Symptom | Cause and fix |
|---|---|
op/bw hangs forever in CI |
Falling back to an interactive unlock — set the token/session; a longer timeout is not a fix |
More than one result was found |
Ambiguous item name — use the item ID |
cannot use '--otp' and '--fields' together |
Fetch the password and the OTP as two calls |
op read returns otpauth://… instead of digits |
Missing ?attribute=otp — you got the seed, not the code |
| Service account cannot see the item | It is in a Private vault, or the vault was never shared with the account |
Vault 403 on totp/code/x |
Policy grants the key path but not the code path |
| 2FAuth returns HTML | Base URL points at the web UI, not /api/v1 |
| Codes fine locally, rejected from CI | Provider-side generation is immune to your clock, but the runner's clock still matters if anything local also generates |
References
references/providers.md— per-provider setup, exact endpoints and response shapes, rate limits, token scoping, the Google Authenticator export format, and where Twilio Verify TOTP actually fits.