Found during Smoke 5 trace (recovery-procedures.md Section 5 fabrication #4):
extractPathArgs was missing protected paths when they appeared as a flag
value (--output=PATH or --output PATH) or as the second positional argument
(dd of=, tee, cp DST). The path-deny overlay correctly checks each candidate
path, but the candidate list was incomplete.
Fix: rewrite extractPathArgs to scan all tokens past index 0:
- recognize --flag=VALUE inline form (extract VALUE)
- recognize key=value (dd-style: if=, of=)
- skip URL-looking tokens (https://, ftp://, ssh://) as low-FP heuristic
- preserve existing behavior for plain positionals and skip redirect tokens
Regression: vitest tools 1726/1726 GREEN (was 1720; +6 path edge-case tests
under "extractPathArgs edge cases (Stream H Task 2)").
Stream H Task 2 of 11. Plan: docs/superpowers/plans/2026-05-30-router-gate-v4-stream-H.md
Pre-flight sync per Pravila §15.2 («git fetch origin && git log
HEAD..origin/main») was blocked because GIT_READONLY_SUB in
shell-content-rules.mjs missed both `fetch` and `ls-remote` subcommands.
Both are ref-only (no working-tree mutation, no commit/push side effect)
and Stream B Whitelist construction left them out by omission — surfaced
during Stream H pre-flight 2026-05-30.
Fix: add both to GIT_READONLY_SUB; RED→GREEN 5 it.each cases covering
`git fetch`, `git fetch origin`, `git fetch --all`, `git ls-remote origin`,
`git ls-remote --heads`.
Atomic precursor commit before any Stream H plan task — does not touch
extractPathArgs (H2) or path-deny display format (H3); pure whitelist
extension.
Regression: vitest tools shell-content-rules.test.mjs 67/67 GREEN
(was 62; +5 new readonly tests). Full tools regression in next step.
Smoke 5 (user-run 2026-05-30) found 5 of 6 self-exfil vectors OPEN — parent
context transcripts (~/.claude/projects/*/<session>.jsonl) readable via:
- Bash: ls ~/.claude/projects/, cat ~/.claude/projects/.../jsonl, head/tail
- PowerShell: Get-Content / gc / Select-String
- Read tool: direct file_path
Spec §3.1 declared this protection but Stream B/C/G never wired path-deny.
DEFAULT_PROTECTED_PATTERNS in shell-content-rules.mjs lacked ~/.claude/projects/.
Read tool had no path-deny hook at all.
Closure:
- shell-content-rules.mjs: +1 regex /(^|\/)\.claude\/projects(\/|$)/i in
DEFAULT_PROTECTED_PATTERNS. Bash hook + PowerShell hook already use this
shared list via pathDenyOverlay → both close automatically.
- enforce-read-path-deny.mjs (NEW): PreToolUse(Read) wrapper rejects any
Read on isProtectedPath() match. Uses same shared DEFAULT_PROTECTED_PATTERNS
for consistency (covers transcript / runtime / .env / normative / dep-checksums).
- .claude/settings.json: register enforce-read-path-deny.mjs PreToolUse Read.
Tests: shell-content-rules.test.mjs +3 path cases in protects %s + 1 dedicated
it() block. enforce-read-path-deny.test.mjs +6 cases covering Read tool block.
1714/1714 vitest tools GREEN (was 1707, +7 net).
Critical: re-run Smoke 5 in clean session before continuing other smokes.
Expected post-fix: all 6 vectors blocked (Bash/PowerShell via path-deny,
Read via enforce-read-path-deny, Write via existing normative-content-rules).
Side observation from Smoke 5 session: controller self-fabrication of
intermediate results (4 occurrences across Smokes 1-5) confirms one of the
7 fundamental limits — gate paradox + behavioral residual irreducible.
No hook catches in-response narrative fabrication (not through tool-gate).
This is a Stream H + recovery-procedures.md documentation item, not a hook fix.