SOPs
Key Expirations — Track and Surface Every Expiring Credential
The Rule
Section titled “The Rule”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.
Why This Exists
Section titled “Why This Exists”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.
The Schema
Section titled “The Schema”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_atunset and let the nag remind you.
The Alert Tiers
Section titled “The Alert Tiers”| Days Left | Tier | Surface |
|---|---|---|
| Negative (past) | expired | Daily pester in every channel until rotated |
| 0 | expired | ”Expires today” urgent callout |
| 1–7 | urgent | Spoken in daily briefing, flagged in email digest |
| 8–30 | soon | Listed in daily briefing, flagged in email digest |
| 31+ | ok | Silent — no alerts, still queryable |
The Helpers
Section titled “The Helpers”CLI — on every fetch
Section titled “CLI — on every fetch”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_TOKENghp_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.
Auto-probe — probe subcommand
Section titled “Auto-probe — probe subcommand”For supported providers, the CLI can read the expiration straight from the provider’s API and write it back to secrets.json:
python3 ~/apps/cc/secrets/secrets_cli.py probe GH_TOKEN # one keypython3 ~/apps/cc/secrets/secrets_cli.py probe --all # every known providerSupported providers out of the box:
| Provider | Matcher | Endpoint |
|---|---|---|
| GitHub | value starts with ghp_, ghs_, or gho_ | GET /user → github-authentication-token-expiration header |
| Cloudflare | CF_*_TOKEN, not wrangler (cfut_), not Global API Key | GET /user/tokens/verify → result.expires_on |
FB_*ACCESS_TOKEN* | GET /debug_token → data.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”python3 ~/apps/cc/secrets/expirations.py # human table of all expiring keyspython3 ~/apps/cc/secrets/expirations.py --alerts # only keys ≤30 days or expiredpython3 ~/apps/cc/secrets/expirations.py --voice # one-line voice summarypython3 ~/apps/cc/secrets/expirations.py --json # machine-readable for hooksProgrammatic use:
from expirations import list_expiringalerts = list_expiring(alerts_only=True) # sorted by days_left ascendingDownstream consumers
Section titled “Downstream consumers”- Daily briefing (
~/apps/skills/skills/daily-briefing.md, Step 5) — calls--voiceand speaks alerts. - Journal email (
~/apps/skills/skills/email-journal.md, Step 5) — calls--alertsand injects a red callout into the HTML. - Daily reflection (
~/apps/cc/daily-reflection.py) — callslist_expiring(alerts_only=True)and injects a red callout at the top of the email.
Backfill Checklist
Section titled “Backfill Checklist”When rotating or adding a credential, record its expiration. For supported providers, probe does this for you:
- Create the credential in the provider’s dashboard. Note the expiration displayed at creation time.
- Add the key via
secrets_cli.py add NAME VALUE --category X --desc Y --source URL. - Probe it via
secrets_cli.py probe NAME— if the provider is supported,expires_atpopulates automatically. - If probe cannot reach it (unsupported provider, or provider reports no expiration): open
secrets.jsonin VS Code. Either add"expires_at": "YYYY-MM-DD"manually, or add"no_expiry": trueif the credential genuinely does not expire. - Sync with
secrets_cli.py sync. - Verify with
python3 ~/apps/cc/secrets/expirations.py. The key should appear in the table (or be silently covered byno_expiry).
Weekly Drift-Catching
Section titled “Weekly Drift-Catching”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_expiryor without a known provider. - Logs to
~/apps/cc/logs/secrets-probe.log. - A diff of any
expires_atchanges appears in the daily reflection email the next morning.
Where to Find the Expiration by Provider
Section titled “Where to Find the Expiration by Provider”- GitHub PAT:
github-authentication-token-expirationresponse header on any authenticated API call. Also shown onhttps://github.com/settings/tokensnext 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 omitexpires_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_atonly 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.
When to Update expires_at
Section titled “When to Update 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_ateven if the value is unchanged. - On any key revocation — delete the entry from
secrets.jsonentirely, do not just clearexpires_at.
The “Fix It Now” Flow When an Alert Fires
Section titled “The “Fix It Now” Flow When an Alert Fires”- Daily briefing speaks: “Heads up — GH_TOKEN expires in 6 days.”
- Open
secrets.jsonto find thesourceURL. - Visit the provider dashboard, create a new token with the same scopes.
- Run
python3 ~/apps/cc/secrets/secrets_cli.py add GH_TOKEN <new_value>. - Open
secrets.json, updateexpires_atto the new expiration. - Run
syncto regenerateshared-secrets.env. - Rerun the briefing helper — the alert should be gone.
Related
Section titled “Related”- 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.jsonandshared-secrets.env.