Skip to content

SOPs

Key Expirations — Track and Surface Every Expiring Credential

If a credential has an expiration date, secrets.json must carry expires_at for it.

Every helper that reads secrets (CLI fetches, daily briefing, journal email, daily reflection) reports the remaining days when a key is in play. Expiration-driven outages become an announced countdown, not a surprise.

An unexpired key is invisible. A key that expires next week is also invisible — right up until the script fails, the webhook stops firing, or a scheduled job breaks at 3 AM. The difference between “everything fine” and “3 AM outage” is one calendar date nobody tracked.

GitHub PATs, Google OAuth refresh tokens (in some modes), Stripe restricted keys, Facebook long-lived user tokens, Cloudflare-scoped API tokens, GHL private integration tokens — they all expire. Silently.

secrets.json entries gain two optional fields:

{
"name": "GH_TOKEN",
"value": "ghp_xxx",
"category": "GitHub",
"description": "Personal Access Token for gh CLI — scopes: repo, read:org, workflow",
"created": "2026-04-23",
"source": "https://github.com/settings/tokens",
"expires_at": "2026-07-22",
"no_expiry": false
}
  • expires_at — ISO date (YYYY-MM-DD), no time component. Set when the credential has a known expiration.
  • no_expiry: true — explicit marker for credentials that do not expire (Cloudflare Global API Key, GHL private integration tokens, non-expiring Facebook page tokens). Silences the “no expiration tracked” nag.
  • If neither field is present, the CLI treats the entry as unknown and nags on every read. This is intentional: unknown state should be resolved, not silently accepted.
  • Do not guess dates. If the expiration is not documented in the provider dashboard or returned by the API, leave expires_at unset and let the nag remind you.
Days LeftTierSurface
Negative (past)expiredDaily pester in every channel until rotated
0expired”Expires today” urgent callout
1–7urgentSpoken in daily briefing, flagged in email digest
8–30soonListed in daily briefing, flagged in email digest
31+okSilent — no alerts, still queryable

secrets_cli.py get NAME prints the expiry to stderr when expires_at is present. When it is missing and no_expiry is not set, the CLI prints a nag instead so unknown state surfaces the moment you use the key.

$ python3 ~/apps/cc/secrets/secrets_cli.py get GH_TOKEN
ghp_xxx
[GH_TOKEN expires 2026-07-22 — 90 days left]
$ python3 ~/apps/cc/secrets/secrets_cli.py get CF_API_TOKEN
<token value>
[CF_API_TOKEN has no expiration tracked — run 'secrets_cli.py probe CF_API_TOKEN' to populate, or mark "no_expiry": true]

Stdout is still just the value, so pipelines that capture it ($(secrets_cli.py get X)) do not break. The expiry line (or the nag) goes only to the operator.

For supported providers, the CLI can read the expiration straight from the provider’s API and write it back to secrets.json:

Terminal window
python3 ~/apps/cc/secrets/secrets_cli.py probe GH_TOKEN # one key
python3 ~/apps/cc/secrets/secrets_cli.py probe --all # every known provider

Supported providers out of the box:

ProviderMatcherEndpoint
GitHubvalue starts with ghp_, ghs_, or gho_GET /usergithub-authentication-token-expiration header
CloudflareCF_*_TOKEN, not wrangler (cfut_), not Global API KeyGET /user/tokens/verifyresult.expires_on
FacebookFB_*ACCESS_TOKEN*GET /debug_tokendata.expires_at

Adding a new provider is one tuple in PROVIDERS at the top of secrets_cli.py. Each probe function takes the token value and returns an ISO date string (or None if the provider reports no expiration).

A probe that returns “no expiration” is the signal to either (a) mark the entry no_expiry: true if that is intentional, or (b) rotate to a time-limited token so we stop carrying un-expiring credentials.

Central helper — ~/apps/cc/secrets/expirations.py

Section titled “Central helper — ~/apps/cc/secrets/expirations.py”
Terminal window
python3 ~/apps/cc/secrets/expirations.py # human table of all expiring keys
python3 ~/apps/cc/secrets/expirations.py --alerts # only keys ≤30 days or expired
python3 ~/apps/cc/secrets/expirations.py --voice # one-line voice summary
python3 ~/apps/cc/secrets/expirations.py --json # machine-readable for hooks

Programmatic use:

from expirations import list_expiring
alerts = list_expiring(alerts_only=True) # sorted by days_left ascending
  • Daily briefing (~/apps/skills/skills/daily-briefing.md, Step 5) — calls --voice and speaks alerts.
  • Journal email (~/apps/skills/skills/email-journal.md, Step 5) — calls --alerts and injects a red callout into the HTML.
  • Daily reflection (~/apps/cc/daily-reflection.py) — calls list_expiring(alerts_only=True) and injects a red callout at the top of the email.

When rotating or adding a credential, record its expiration. For supported providers, probe does this for you:

  1. Create the credential in the provider’s dashboard. Note the expiration displayed at creation time.
  2. Add the key via secrets_cli.py add NAME VALUE --category X --desc Y --source URL.
  3. Probe it via secrets_cli.py probe NAME — if the provider is supported, expires_at populates automatically.
  4. If probe cannot reach it (unsupported provider, or provider reports no expiration): open secrets.json in VS Code. Either add "expires_at": "YYYY-MM-DD" manually, or add "no_expiry": true if the credential genuinely does not expire.
  5. Sync with secrets_cli.py sync.
  6. Verify with python3 ~/apps/cc/secrets/expirations.py. The key should appear in the table (or be silently covered by no_expiry).

A scheduled probe --all run catches tokens rotated externally (e.g., you created a new GitHub PAT in the dashboard with the same name, or a provider bumped the expiration on renewal). The job lives on the Mac Studio via launchd (com.secrets.probe-weekly.plist), not the VPS — secrets.json is on Google Drive and the VPS does not have Drive mounted.

  • Runs every Sunday at 3:00 AM local time.
  • Hits every provider whose token is present; skips entries marked no_expiry or without a known provider.
  • Logs to ~/apps/cc/logs/secrets-probe.log.
  • A diff of any expires_at changes appears in the daily reflection email the next morning.
  • GitHub PAT: github-authentication-token-expiration response header on any authenticated API call. Also shown on https://github.com/settings/tokens next to the token.
  • Google OAuth refresh token: https://console.cloud.google.com/apis/credentials/consent — projects in “Testing” expire refresh tokens after 7 days. “In production” apps do not expire refresh tokens unless revoked, so omit expires_at.
  • Facebook long-lived user token: https://developers.facebook.com/tools/debug/accesstoken/ — paste the token, read “Expires” field (typically ~60 days).
  • Stripe restricted keys: Do not expire by default. Set expires_at only if you chose an expiration at creation.
  • Cloudflare API tokens: https://dash.cloudflare.com/profile/api-tokens — each token shows its “Expires on” date.
  • GHL private integration tokens: Non-expiring. Skip expires_at.
  • On any key rotation — always overwrite the old date with the new one.
  • On any key renewal in the provider dashboard — update expires_at even if the value is unchanged.
  • On any key revocation — delete the entry from secrets.json entirely, do not just clear expires_at.

The “Fix It Now” Flow When an Alert Fires

Section titled “The “Fix It Now” Flow When an Alert Fires”
  1. Daily briefing speaks: “Heads up — GH_TOKEN expires in 6 days.”
  2. Open secrets.json to find the source URL.
  3. Visit the provider dashboard, create a new token with the same scopes.
  4. Run python3 ~/apps/cc/secrets/secrets_cli.py add GH_TOKEN <new_value>.
  5. Open secrets.json, update expires_at to the new expiration.
  6. Run sync to regenerate shared-secrets.env.
  7. Rerun the briefing helper — the alert should be gone.
  • Handling Secrets — how real credentials enter the system without leaking through chat.
  • Env Files — the shared-secrets.env generation flow.
  • super CLAUDE.md — Shared Secrets section — path conventions for secrets.json and shared-secrets.env.