feat(skills): add secret-hygiene skill

Was a loose secret-hygiene.md at the root of ~/.claude/skills — not
dir/SKILL.md, so no harness ever loaded it. Promoted to a real skill so
it installs to Claude Code, Crush, Antigravity and Vibe alike.

Frontmatter description carries load-before triggers (kubectl exec/env,
whole-file SOPS decrypt, token rotation) so it fires ahead of the command
that would leak, not after.

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
This commit is contained in:
2026-08-23 07:50:42 +02:00
co-authored by Claude Opus 5
parent 85e125703e
commit bf7f1b579c
2 changed files with 77 additions and 0 deletions
+1
View File
@@ -25,6 +25,7 @@ This index lists all available engineering skills. Load the full SKILL.md on dem
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | | `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod |
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists | | `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists |
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry | | `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
| `secret-hygiene` | Stop credentials rendering into the session transcript; safe rotation chain when one leaks | Before any kubectl/SOPS/token command; after a confirmed leak |
| `regulatory-risk-assessment` | Structured risk register for regulated-industry features | Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data | | `regulatory-risk-assessment` | Structured risk register for regulated-industry features | Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data |
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | | `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review | | `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review |
+76
View File
@@ -0,0 +1,76 @@
---
name: secret-hygiene
description: 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):
```python
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).