Files
skills/skills/secret-hygiene/SKILL.md
T
mathiasandClaude Opus 5 56a947b2a0 refactor(plugin)!: ship as a Claude Code plugin, one dir link per harness
Skills move to skills/<name>/SKILL.md and the repo gains both manifests, so
it is now a marketplace hosting exactly one plugin whose root is the repo
root (source: "."). Claude Code installs it from git; no symlinks involved.

Why: install.sh wired the library into every consuming repo as an ABSOLUTE
symlink to $CHECKOUT_DIR. The checkout moved three times, most recently when
local-dev@1966d5a dropped the embedded .skills/ copy, and nothing migrated
the links — 243 dangling links across 12 repos, hidden from git by the
**/.claude/skills/ ignore rule. Worse, ~/dev spans two path roots
(/Users/mathias on the iCloud-shared Macs, /home/mathias on koala), so an
absolute link stored there is broken on one side by construction. Re-running
the installer cannot fix that; only removing the mechanism can.

install.sh keeps the harnesses with no plugin system and drops from 42 links
per harness to one directory link, via the convergent ~/.agents/skills
convention that Pi and OpenCode both read:

  ~/.agents/skills          -> $CHECKOUT_DIR/skills
  ~/.config/crush/skills    -> ~/.agents/skills
  ~/.config/opencode/skills -> ~/.agents/skills

wire_claude_repo is deleted outright rather than gated behind a flag — on a
mixed-root fleet it cannot be made correct. Antigravity and Vibe targets go
too; both unused. CHECKOUT_DIR keeps its host-local default deliberately:
pointing it into ~/dev would put a git working tree inside iCloud with two
concurrent writers.

Taskfile drops five near-identical wiring loops (they had already drifted)
and delegates to install.sh. `task check` gains frontmatter validation, a
stray-root-entry gate, and manifest/tag version parity — the loose
secret-hygiene.md sat unloadable for months because nothing checked layout.

Verified: claude plugin validate . passes clean; a probe skill loaded as
skills:zzprobe through --plugin-dir; task check green at 42 skills.

Refs: #23
Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-23 14:29:51 +02:00

4.2 KiB

name, description
name description
secret-hygiene Prevent agent sessions from rendering credentials into the transcript, and recover safely when one leaks. Load BEFORE any command that touches credentials - kubectl exec/env/describe, kubectl get secret, whole-file SOPS decryption, token generation or rotation, envFrom inspection, or edits to encrypted config. Also load when a credential has already been exposed and a rotation chain is needed. Triggers - "rotate this token", "decrypt the config", "check the env var", "leaked credential", "did that print a secret", "why is this secret wrong".

Secret hygiene for agent sessions

Goal: stop the recurring leak pattern where a diagnostic command renders a SOPS-encrypted value or a Kubernetes Secret value into the session transcript, forcing a rotation.

Confirmed past leaks (see mathias/infra#39 rotation umbrella):

  • POSTGRES_PASSWORD — kubectl exec ... env rendered envFrom value
  • INFRA_MCP_TOKEN — sops -d ingress.enc.yaml rendered the bearer in the configuration-snippet
  • brain_app postgres DSN — pre-fix ingestion pod log (hyperguild#20)

Three are pattern, not bug.

Rules — never do these

Forbidden Why Use instead
kubectl exec <pod> -- env (or printenv) Renders every envFrom + valueFrom.secretKeyRef plaintext kubectl describe pod <pod> — k8s redacts secretKeyRef in describe
sops -d <file> to inspect Whole-file decrypt dumps everything sops --extract '["key"]' <file> for pinpoint reads
kubectl get secret ... -o jsonpath='{.data}' (whole map) Returns base64 of every key; trivial to b64-decode the lot ... -o jsonpath='{.data.SPECIFIC_KEY}' for the one key needed
Echoing a generated token to verify length / content Renders the new token into transcript, defeating the rotation Verify functionally: curl ... -H "Authorization: Bearer $(cat /tmp/tokfile)" — never echo $NEW_TOK
Storing a token in a shell variable then running echo / set / env Same as above Pipe through file (mode 600) or stdin to the next consumer

Rules — do these instead

  • Verify by side-effect, not by value. If a token rotation should make the old token return 401 and the new one return 200, test those two HTTP responses. Never check $OLD == $LEAKED by printing it.
  • Pinpoint SOPS reads. When you need ONE key from a SOPS file, always sops --extract '["..."]'. The shape of the key path is safe to log; the value isn't.
  • Stash tokens in /dev/shm/<unique> mode 600 and pass via filename, not env. shred -u after use. Avoid /tmp if the system has /dev/shm.
  • If you must dump a secret to confirm wiring, immediately rotate it in the same commit train. Treat the dump as "this is now compromised".

Rotation chain pattern when a token is already exposed

If a token was leaked in a prior step of the same session, chain two rotations:

  1. T1: replace the leaked value with a new one. The verification step may also leak T1 (acceptable risk).
  2. T2: replace T1 with a fresh value. Run this without any decrypt step that emits to stdout. The new value lives only in the encrypted file + the consumer's secret store (~/.llmkeys, the k8s Secret).

Example T2 path (Python, never decrypts to stdout):

new_t2 = subprocess.check_output(["openssl","rand","-hex","32"]).decode().strip()
plaintext = subprocess.check_output(["sops","-d", path]).decode()   # in-memory only
patched = re.sub(r'Bearer [a-f0-9]+(?=")', f"Bearer {new_t2}", plaintext)
# write patched via /dev/shm tmp + sops -e → write back
# DO NOT print(patched), DO NOT print(new_t2)

After T2 lands via GitOps + Flux, update consumers from the file stash without echoing.

Detection ideas (not blocking — telemetry)

  • Add a bash pre-hook in Claude harness that refuses obvious patterns (kubectl exec * env*, sops -d *.enc.yaml). Filed as a follow-up.
  • Run a periodic grep over ~/.claude/projects/*/sessions/*.jsonl for high-entropy strings matching [a-f0-9]{32,} to catch new leak surface.

Reference

  • gitea.d-ma.be/mathias/infra#39 — umbrella credential rotation issue with confirmed-leak roster.
  • gitea.d-ma.be/mathias/infra#44 — POSTGRES_PASSWORD rotation procedure (template for future incidents).