Files
portal/tools/enforce-read-path-deny.mjs
T
Дмитрий 1072fae4a6 fix(router-gate-v4): EMERGENCY Smoke 5 closure — transcript JSONL hard-deny
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.
2026-05-30 08:01:49 +03:00

53 lines
1.8 KiB
JavaScript

/**
* PreToolUse(Read) wrapper — path-deny for Read tool.
* Router-gate v4 emergency fix (Smoke 5 2026-05-30).
*
* Spec §3.1 declared transcript JSONL hard-deny but Read tool had NO
* path-protection — controller could Read ~/.claude/projects/*.jsonl
* (parent context exfil from other sessions). Same for runtime artifacts,
* .env, normative files.
*
* Reuses DEFAULT_PROTECTED_PATTERNS from shell-content-rules.mjs.
* Fail-CLOSE on internal error (security default).
*/
import { fileURLToPath } from 'url';
import {
readStdin,
parseEventJson,
exitDecision,
} from './enforce-hook-helpers.mjs';
import { defaultPathNormalize, isProtectedPath, DEFAULT_PROTECTED_PATTERNS } from './shell-content-rules.mjs';
export function decide({ toolName, filePath }) {
if (toolName !== 'Read') return { block: false, reason: null };
const fp = String(filePath || '');
if (!fp) return { block: false, reason: null };
if (isProtectedPath(fp, defaultPathNormalize, DEFAULT_PROTECTED_PATTERNS)) {
return {
block: true,
reason: `path «${defaultPathNormalize(fp)}» protected against Read (§3.1 transcript/runtime/normative hard-deny)`,
};
}
return { block: false, reason: null };
}
async function main() {
try {
const raw = await readStdin();
const event = parseEventJson(raw);
const r = decide({
toolName: event.tool_name,
filePath: event.tool_input?.file_path || event.tool_input?.filePath,
});
if (r.block) {
return exitDecision({ block: true, message: `[read-path-deny] ${r.reason}` });
}
return exitDecision({ block: false });
} catch {
return exitDecision({ block: true, message: '[read-path-deny] внутренняя ошибка — fail-CLOSE' });
}
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();