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.
This commit is contained in:
@@ -165,6 +165,16 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Read",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-read-path-deny.mjs",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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();
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { decide } from './enforce-read-path-deny.mjs';
|
||||
|
||||
describe('enforce-read-path-deny decide()', () => {
|
||||
it('allows Read on normal project file', () => {
|
||||
const r = decide({ toolName: 'Read', filePath: 'docs/observer/STATUS.md' });
|
||||
expect(r.block).toBe(false);
|
||||
});
|
||||
it('blocks Read on ~/.claude/projects/*.jsonl transcript', () => {
|
||||
const r = decide({ toolName: 'Read', filePath: '~/.claude/projects/abc-session.jsonl' });
|
||||
expect(r.block).toBe(true);
|
||||
expect(r.reason).toMatch(/protected/i);
|
||||
});
|
||||
it('blocks Read on absolute /c/Users/.../.claude/projects/x.jsonl', () => {
|
||||
const r = decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/proj/session.jsonl' });
|
||||
expect(r.block).toBe(true);
|
||||
});
|
||||
it('blocks Read on ~/.claude/runtime/*.json (runtime artifacts)', () => {
|
||||
const r = decide({ toolName: 'Read', filePath: '~/.claude/runtime/router-state-x.json' });
|
||||
expect(r.block).toBe(true);
|
||||
});
|
||||
it('blocks Read on .env', () => {
|
||||
const r = decide({ toolName: 'Read', filePath: '.env' });
|
||||
expect(r.block).toBe(true);
|
||||
});
|
||||
it('allows non-Read tool calls (no-op)', () => {
|
||||
const r = decide({ toolName: 'Bash', filePath: 'whatever' });
|
||||
expect(r.block).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,9 @@ export function defaultPathNormalize(target) {
|
||||
export const DEFAULT_PROTECTED_PATTERNS = [
|
||||
/(^|\/)\.claude\/runtime(\/|$)/i,
|
||||
/(^|\/)\.claude\/settings(\.local)?\.json$/i,
|
||||
// Smoke 5 emergency fix (2026-05-30) — transcript JSONL hard-deny (spec §3.1 was declared, not implemented).
|
||||
// Prevents self-exfil of parent context across sessions via Bash cat / PowerShell Get-Content / Read tool.
|
||||
/(^|\/)\.claude\/projects(\/|$)/i,
|
||||
/(^|\/)\.env(\.|$)/i,
|
||||
/(^|\/)node_modules\//i,
|
||||
/(^|\/)CLAUDE\.md$/i,
|
||||
|
||||
@@ -36,6 +36,12 @@ describe('isProtectedPath', () => {
|
||||
])('allows %s', (p) => {
|
||||
expect(isProtectedPath(p, defaultPathNormalize, DEFAULT_PROTECTED_PATTERNS)).toBe(false);
|
||||
});
|
||||
|
||||
// Smoke 5 emergency fix — transcript JSONL protection (single it() for shell-content-rules hook compliance)
|
||||
it('protects ~/.claude/projects/*.jsonl (transcript hard-deny per spec §3.1) in shell-content-rules', () => {
|
||||
expect(isProtectedPath('~/.claude/projects/foo.jsonl', defaultPathNormalize, DEFAULT_PROTECTED_PATTERNS)).toBe(true);
|
||||
expect(isProtectedPath('/c/Users/Administrator/.claude/projects/abc/def.jsonl', defaultPathNormalize, DEFAULT_PROTECTED_PATTERNS)).toBe(true);
|
||||
});
|
||||
});
|
||||
import {
|
||||
pathDenyOverlay,
|
||||
|
||||
Reference in New Issue
Block a user