Diff
1diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
2index 9ba9b64a244be231a8a531e31e9c7637c84edfee..11cf1ecff320c7ee680544ffe4bab64aab96a03b 100644
3--- a/src/core/deterministic.ts
4+++ b/src/core/deterministic.ts
5@@ -727,7 +727,14 @@ function checkCommand(command: BashCommand, context: ShellContext): CommandResul
6 return { ...unknown, decision: ask("System file changes require confirmation") };
7 writes ||= writing && paths[0] !== "/dev/null";
8 }
9- if (!context.certain || optionLikeGlob || roles.review || inlineCode(program, args)) return unknown;
10+ const readOnly = READ_ONLY.has(program) && !(program === "printf" && args[0] === "-v");
11+ if (
12+ optionLikeGlob ||
13+ roles.review ||
14+ inlineCode(program, args) ||
15+ (!context.certain && (!readOnly || writes || roles.paths.length))
16+ )
17+ return unknown;
18 if (
19 command.assignments.some(
20 (word) =>
21@@ -759,7 +766,6 @@ function checkCommand(command: BashCommand, context: ShellContext): CommandResul
22 return { decision, context: { ...context, certain: false }, changesDirectory: true };
23 }
24 }
25- const readOnly = READ_ONLY.has(program) && !(program === "printf" && args[0] === "-v");
26 return { decision, context: { ...context, certain: !writes && readOnly } };
27 }
28
29@@ -788,9 +794,8 @@ function evaluateNode(node: BashNode, context: ShellContext): CommandResult {
30 const left = evaluateNode(node.left, { ...context });
31 if (node.operator === "||") {
32 const right = evaluateNode(node.right, { ...context, certain: false });
33- const decision = combine(left.decision, right.decision);
34 return {
35- decision: decision?.decision === "allow" ? undefined : decision,
36+ decision: combine(left.decision, right.decision),
37 context: { ...context, certain: false },
38 reviewCommands: combinedReviewCommands(left, right),
39 };
40@@ -814,7 +819,8 @@ function evaluateNode(node: BashNode, context: ShellContext): CommandResult {
41 }
42
43 export function evaluateParsedBash(parsed: BashParseResult, cwd = process.cwd()): BashDeterministicResult {
44- if (parsed.parserUnavailable) return { decision: { decision: "deny", reason: "Bash parser is unavailable", category: "bash" } };
45+ if (parsed.parserUnavailable)
46+ return { decision: { decision: "deny", reason: "Bash parser is unavailable", category: "bash" } };
47 if (!parsed.parsed) return {};
48 if (!parsed.root) return { decision: { decision: "allow", reason: "No operations to evaluate", category: "empty" } };
49 try {
50diff --git a/src/core/normalize.ts b/src/core/normalize.ts
51index 2376ca8131727661d503e059b79865721953d7ef..674782059ca20b1627410f03275739d2b68c7bab 100644
52--- a/src/core/normalize.ts
53+++ b/src/core/normalize.ts
54@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
55 import type { PolicyRequest } from "./types";
56
57 // Bump to invalidate all cached decisions when rules change.
58-export const POLICY_VERSION = 39;
59+export const POLICY_VERSION = 40;
60
61 const SENSITIVE_KEY_RE = /(?:api[-_]?key|auth(?:entication|orization)?|credential|cookie|password|secret|token)/i;
62 const SENSITIVE_ARGUMENT_RE =
63diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
64index 29cd0b8006b23ba5b091c9ebede1bc1864e708a5..5fbd63e7c77fb3265976c61bff01070f1825122a 100644
65--- a/test/core/deterministic.test.ts
66+++ b/test/core/deterministic.test.ts
67@@ -52,6 +52,7 @@ const allowed = [
68 "echo text | wc -c",
69 'rocm-smi --showmemuse 2>&1; echo ---; ps aux | grep -E "comfyui|main.py" | grep -v grep',
70 "ls -la safe && ls safe/Sources 2>/dev/null | head -30",
71+ 'ls -la /mnt/media/documents/mama_prevod/sr/ && echo "---FRONT---" && cat /mnt/media/documents/mama_prevod/sr/01_front.txt 2>/dev/null || echo "no front file"',
72 ];
73
74 test.each(allowed)("deterministically allows %s", async (command) => {