Ship safe-op --reveal stdout-leak guard (6th leak in 6 weeks; --fields X --reveal | head bypass shape)
completedAgent: sergey-engineer
Priority: 1
Context: 6th credential leak self-reported 2026-06-16T17:03Z. Pattern: `op item get <uuid> --fields password --reveal | head -c 40` echoed first ~16 chars of Neon DB texture-domains password into tool transcript. The current `~/bin/safe-op` wrapper ALLOWS `--fields X --reveal` because that pair is the documented safe shape (per AGENTS.md). But piping `--reveal` output to stdout in a tool-output context IS the leak — even partial output leaks ~50% entropy.
The 6/12 meta-rule says: any anti-pattern in the credential/secret family that has happened ≥3 times MUST file a structural-fix fleet-task in the same session as the self-report. This is leak #6. The 17:03Z self-report explicitly committed to filing this fleet-task. Per the 6/10 opportunistic-isn't-a-plan rule: filing now, not at reflection.
Leak shape gap analysis:
- `op item get <uuid> --format json` → BLOCKED (RULE 1 in safe-op)
- bare `op item get <uuid> --reveal` (no --fields) → BLOCKED (RULE 2)
- bare `op item get <uuid>` on URL-field-denylisted uuid → BLOCKED (RULE 3, shipped today via fleet-task ca379584)
- `op item get <uuid> --fields password --reveal | head` → ALLOWED (current gap — the leak shape from today)
The fundamental issue: `--reveal` to stdout in any tool-capture context is unsafe regardless of `--fields` scoping. The pair `--fields X --reveal` was added as the 'safe fallback' for field discovery + value extraction, but in interactive/tool sessions stdout IS captured by the gateway transcript.
Fix approach options (decide in step 1):
(A) Strict: hard-block `--reveal` entirely from interactive sessions; force all secret reads through `op read 'op://...'` which has the same plaintext-output property but is more intentional (named field, named vault, no partial-leak via head/cut).
(B) Wrap: when `--reveal` is in argv, intercept stdout and pipe through a SHA256 hash so the transcript captures only the digest, not the value. Tool consuming the secret (e.g. psql via env var) gets the real value through a different channel (env var set in same shell line, never echoed).
(C) Hybrid: allow `--reveal` ONLY when stdout is being piped into a recognized 'safe sink' (e.g., command substitution into a variable: SECRET=$(safe-op item get ... --fields X --reveal); the wrapper detects via tty test or PPID inspection that output is NOT going to a terminal/tool-captured pipe). Otherwise BLOCK.
Note: option (A) is simplest, option (B) is most secure, option (C) is most ergonomic. Step 1 decides.
Subtasks (atomic, each ≤30 min, heartbeat-pickable):
1. DECIDE approach (≤10 min): read safe-op source + AGENTS.md secret-handling section + LEARNINGS 6/11+6/12+6/16 entries. Pick A vs B vs C with explicit reasoning. Document in task result.
2. PATCH `~/bin/safe-op` (≤20 min): implement chosen approach. New RULE 4: --reveal stdout-leak guard. Update RULE numbering. Preserve all existing rules (RULE 1: --format json BLOCKED; RULE 2: bare --reveal BLOCKED; RULE 3: URL-field denylist BLOCKED).
3. AUDIT-LOG schema (≤5 min): new BLOCKED case logs reason=REVEAL_STDOUT_LEAK (or whatever the chosen approach calls it).
4. SELF-TESTS (≤15 min):
- `safe-op item get <safe-uuid> --fields password --reveal | head -c 40` → BLOCKED with REVEAL_STDOUT_LEAK reason
- `safe-op item get <safe-uuid> --fields password --reveal > /tmp/secret.txt` (file redirect) → BLOCKED (still going to a captured-by-shell sink)
- `SECRET=$(safe-op item get <safe-uuid> --fields password --reveal)` → if option C, ALLOWED; if A/B, BLOCKED
- `safe-op read 'op://Vault/Item/field'` → ALLOWED (existing safe shape preserved)
- `safe-op item get <safe-uuid> --fields hostname` (no --reveal, non-secret field) → ALLOWED (regression test)
- All 5 existing RULE 1/2/3 tests still pass
5. UPDATE AGENTS.md `🔐 Secret Handling` section (≤15 min): add 6th leak row to the table. Replace the 'PREFER op read' guidance with the new mechanical rule. Cross-link this fleet-task ID.
6. UPDATE LEARNINGS.md tonight's reflection: append 6th-leak entry + meta-rule's 3rd clean application (6/11 → 6/16 leak#5 → 6/16 leak#6 same-day) + structural-fix shipped. Apology-shaped per 6/4 convention.
7. CROSS-LINK: LEARNINGS ↔ AGENTS ↔ MEMORY ↔ `~/bin/safe-op` ↔ this fleet-task ID. Update MEMORY.md if any new safe-shape recipe lands.
8. AUDIT: scan `~/.local/state/safe-op.log` for any prior --reveal|head shapes that may have been called before today's 17:03Z leak (i.e., was today's leak the FIRST instance of this shape, or has it been silently happening?). Document findings in task result.
NOT in scope: rotating the leaked Neon DB texture-domains password — Sergey owns the rotation. Self-report sent 17:03Z. I owe structural fix.
SOFT DEADLINE: tonight's reflection (~12h from filing). Same-day shipping per the 6/12 meta-rule (this is the second same-day structural-fix ship today after ca379584 at 16:39Z — the rule is firing 2x same-day).
Event Timeline
created
status_change
queued → in_progress
failed
lease expired — re-queued for retry
in_progress → queued
status_change
queued → completed