From e4d90e70cc3636992571f2463de04b1f1f2c528f Mon Sep 17 00:00:00 2001 From: Bin Le Date: Sun, 23 Aug 2026 13:36:52 -0400 Subject: [PATCH] CLAUDE.md: retain the PyPI break-glass token; bar agents from deleting credentials The PyPI hygiene note told the reader to delete the project-scoped token once OIDC Trusted Publishing was confirmed on a release. That contradicted the failure playbook two paragraphs above it, which names the token-driven Flow B as the recovery path when publish-pypi fails on an OIDC / trusted-publisher error -- so obeying the hygiene rule removed the fallback exactly when a broken release would need it, and forced a pypi.org round-trip at the worst possible moment. It was also a silent failure. ~/.zshrc builds TWINE_PASSWORD with $(cat ... 2>/dev/null), so deleting the file yields an empty password and a confusing PyPI auth error rather than an obvious missing-file one. Three changes: - Reverse the hygiene note: retain the token, refresh it on expiry, revoke only on leak or replacement. Warn that ~/.zshrc names the file literally (not by glob), so a rename must edit the .zshrc path in the same commit. - Document that the break-glass credential comes from the system environment (TWINE_USERNAME / TWINE_PASSWORD, pre-exported), so twine needs no flags and nobody hunts for a token file. - Add a hard rule under Security & Configuration Tips against deleting, revoking, or rotating any credential, or proposing it as cleanup, plus secret-handling rules: never print a value, assert on presence rather than content, and treat filenames/env-var names as non-secret. Also dedupe the two near-identical "avoid persistent privileged processes" bullets in that section. Docs only -- no code, CLI, or test change. Lands under [Unreleased]. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 19 +++++++++++++++++++ CLAUDE.md | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f8c84..ecc5a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ This project follows a Keep a Changelog-style format and uses version tags for r ## [Unreleased] +### Changed + +- **`CLAUDE.md`: the PyPI break-glass token is retained and refreshed, not + deleted — and agents are now barred from deleting credentials at all.** The + hygiene note used to say to delete the project-scoped token once OIDC Trusted + Publishing was confirmed on a release. That contradicted the failure playbook + two paragraphs above it, which names the token-driven Flow B as the recovery + path when `publish-pypi` fails on an OIDC / trusted-publisher error — so + following the hygiene rule removed the fallback exactly when a broken release + would need it. It was also a silent failure: `~/.zshrc` builds + `TWINE_PASSWORD` with `$(cat … 2>/dev/null)`, so a deleted file yields an + empty password and a confusing PyPI auth error rather than a missing-file one. + Three changes: the hygiene note is reversed (retain, refresh on expiry, revoke + only on leak/replacement); the break-glass credential is documented as coming + from the **system environment**, not a file hunt; and *Security & + Configuration Tips* gains a hard rule against deleting, revoking, or rotating + any credential, plus secret-handling rules (never print a value; assert on + presence, not content). Docs only — no code or CLI change. + ## [1.9.11] - 2026-08-23 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index f3110bc..c9e96a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,11 +134,14 @@ OIDC Trusted Publishing is the default (no stored secret; the `pypi` environment ```bash python -m build # produces dist/*.tar.gz and dist/*.whl -python -m twine upload --skip-existing dist/* -# username: __token__ password: +python -m twine upload --skip-existing dist/* # credentials already in the env ``` -Prefer `TWINE_USERNAME=__token__` + `TWINE_PASSWORD` env vars (or `~/.pypirc`) so the token never hits shell history. **Hygiene:** once OIDC is confirmed on a release, delete the project-scoped token from pypi.org and `~/env-secrets/api_keys/` so no long-lived PyPI credential remains; regenerate only to bootstrap or break glass. +**The break-glass credential comes from the system environment — check there first, do not hunt for the token file.** `~/.zshrc` exports `TWINE_USERNAME=__token__` and `TWINE_PASSWORD=$(cat $HOME/env-secrets/api_keys/pypi-org-api-token-binlecode 2>/dev/null)` in every login shell, so `twine upload` authenticates with no flags, no prompt, and nothing pasted on a command line. Confirm it is present with `env | grep -c TWINE_PASSWORD` (expect `1`) — never echo the value. The file under `~/env-secrets/` is only the backing store for that export; the env var is the interface. + +**Hygiene — keep the token; do NOT delete it after OIDC works.** It stays (mode `600`) as the standing break-glass credential and is **refreshed when it expires**, not revoked. Flow B *is* the documented recovery path when `publish-pypi` fails on an OIDC / trusted-publisher error (see the failure playbook above), so deleting the token removes the fallback exactly when a broken release needs it. Worse, the export is written with `2>/dev/null`: delete the file and `TWINE_PASSWORD` silently becomes the empty string, turning a missing credential into a confusing PyPI auth failure instead of an obvious missing-file error. An earlier version of this section said to delete the token once OIDC was confirmed; that was wrong and is retired. + +Rules for the retained token: never commit it or paste it into a PR/issue (`.githooks/pre-commit` redaction is a backstop, not the control); never echo it — rely on the env export; keep it **project-scoped to `actop`**, never account-wide, so a leak cannot reach other projects. On refresh, **write the new token into the existing filename** — `~/.zshrc` names `pypi-org-api-token-binlecode` literally, not by glob, so renaming the file (e.g. to add an `-exp--` suffix like the other dated keys) breaks the export and, because of the `2>/dev/null`, does so silently. To adopt a dated name, edit the `.zshrc` path in the same commit as the rename. Revoke on pypi.org only if the token leaks or is replaced. ### Homebrew Core submission (optional) @@ -273,6 +276,28 @@ a mount point, not a fake); faking the data or the logic under test is not. - screenshot or terminal capture for UI-visible changes. ## Security & Configuration Tips + +**RULE — never delete, revoke, or rotate a credential, and never propose it as +cleanup.** Not the PyPI token, not `HOMEBREW_TAP_TOKEN`, nothing under +`~/env-secrets/`. Deleting a credential is irreversible from this side, and the +ones that look idle are usually the break-glass path for a pipeline that is +already broken — removing them costs nothing until the day it costs everything. +Credential lifecycle is the maintainer's call alone: if a credential genuinely +looks stale, say so in one line and stop. Do not run the deletion, do not stage +it, do not put it on a checklist. This rule exists because an agent read the old +"delete the token once OIDC is confirmed" hygiene note as an action item and +proposed deleting the live PyPI break-glass token immediately after a successful +release (2026-08-23); the note is retired, but the reflex it triggered is what +this rule blocks. + +**Reading secrets:** prefer the system environment (`TWINE_USERNAME` / +`TWINE_PASSWORD` are pre-exported by `~/.zshrc` — see *PyPI publishing — two +flows*) over reading files under `~/env-secrets/`. Never `cat`, `echo`, print, +or interpolate a secret value into a command line, a commit, a PR body, or an +issue: mask it (`sed -E 's/[A-Za-z0-9_-]{20,}//g'`) or, better, assert +on its presence (`env | grep -c TWINE_PASSWORD`) instead of its content. +Filenames and env-var *names* are not secrets; their values are. + - The IOReport backend runs unprivileged. -- Avoid introducing persistent privileged processes or unsafe temporary-file handling. -- Avoid introducing persistent privileged processes or shell commands that invoke `sudo`. +- Avoid introducing persistent privileged processes, unsafe temporary-file + handling, or shell commands that invoke `sudo`.