Diff
1diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
2index 36f4fd006963eb9349d4fa8fe982209eb54e8c73..fbeeeceb26bb571b59947eb66aa65ee934852d2a 100644
3--- a/src/core/deterministic.ts
4+++ b/src/core/deterministic.ts
5@@ -312,7 +312,7 @@ function safeFind(args: string[]): boolean {
6
7 type ArgumentRoles = { paths: string[]; searchRoots?: string[]; review?: boolean };
8
9-function searchPaths(program: string, args: string[]): ArgumentRoles {
10+function searchPaths(program: string, args: string[], pipedInput: boolean): ArgumentRoles {
11 const paths: string[] = [];
12 const positional: string[] = [];
13 const expressionOptions = new Set(["-e", "--regexp", "-f", "--file"]);
14@@ -439,12 +439,15 @@ function searchPaths(program: string, args: string[]): ArgumentRoles {
15 }
16 const roots = expression || (program === "rg" && files) ? positional : positional.slice(1);
17 paths.push(...roots);
18- return { paths, searchRoots: recursive ? (roots.length ? roots : ["."]) : undefined };
19+ return {
20+ paths,
21+ searchRoots: recursive ? (roots.length ? roots : pipedInput && program === "rg" && !files ? [] : ["."]) : undefined,
22+ };
23 }
24
25-function argumentRoles(program: string, args: string[]): ArgumentRoles {
26+function argumentRoles(program: string, args: string[], pipedInput: boolean): ArgumentRoles {
27 if (["echo", "printf", "dirname", "tr"].includes(program)) return { paths: [] };
28- if (program === "grep" || program === "rg") return searchPaths(program, args);
29+ if (program === "grep" || program === "rg") return searchPaths(program, args, pipedInput);
30 if (program === "find") {
31 const roots = findRoots(args);
32 return { paths: roots, searchRoots: roots.length ? roots : ["."] };
33@@ -601,7 +604,7 @@ export type BashDeterministicResult = {
34 reviewCommands?: string[];
35 };
36
37-function checkCommand(command: BashCommand, requestCwd: string): BashDeterministicResult {
38+function checkCommand(command: BashCommand, requestCwd: string, pipedInput: boolean): BashDeterministicResult {
39 const unknown: BashDeterministicResult = { reviewCommands: [command.source] };
40 const literalWords = command.words.map((word) => word.text);
41 const literalDanger = dangerousCommand(literalWords[0], literalWords.slice(1));
42@@ -631,7 +634,7 @@ function checkCommand(command: BashCommand, requestCwd: string): BashDeterminist
43 if (!program) return unknown;
44 const dangerous = dangerousCommand(program, args);
45 if (dangerous) return { ...unknown, decision: dangerous };
46- const roles = argumentRoles(program, args);
47+ const roles = argumentRoles(program, args, pipedInput);
48 let cwd = requestCwd;
49 if (program === "git") {
50 const operationIndex = gitOperationIndex(args);
51@@ -727,12 +730,12 @@ function combine(left: Decision | undefined, right: Decision | undefined): Decis
52 return left && right ? left : undefined;
53 }
54
55-function evaluateNode(node: BashNode, cwd: string): BashDeterministicResult {
56+function evaluateNode(node: BashNode, cwd: string, pipedInput = false): BashDeterministicResult {
57 if (node.kind === "unsupported") return {};
58- if (node.kind === "command") return checkCommand(node.command, cwd);
59- if (node.kind === "scope") return evaluateNode(node.body, cwd);
60- const left = evaluateNode(node.left, cwd);
61- const right = evaluateNode(node.right, cwd);
62+ if (node.kind === "command") return checkCommand(node.command, cwd, pipedInput);
63+ if (node.kind === "scope") return evaluateNode(node.body, cwd, pipedInput);
64+ const left = evaluateNode(node.left, cwd, pipedInput);
65+ const right = evaluateNode(node.right, cwd, pipedInput || node.operator === "|");
66 return {
67 decision: combine(left.decision, right.decision),
68 reviewCommands: combinedReviewCommands(left, right),
69diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
70index 8ee15d8231cb2468b86f1c32dfbeaba3db21b415..96ce003773f4a2b8216919570186995696e9d062 100644
71--- a/test/core/deterministic.test.ts
72+++ b/test/core/deterministic.test.ts
73@@ -146,6 +146,10 @@ test.each([
74 "find root-link -name '*.md'",
75 "find home-link -name '*.md'",
76 "rg needle /",
77+ "echo ok | rg needle /",
78+ "echo ok | rg --files ~",
79+ "echo ok | grep -r needle /",
80+ "echo ok; rg needle /",
81 "rg --files ~",
82 "rg -e needle ~",
83 "rg needle home-link",
84@@ -175,6 +179,29 @@ test.each(["grep -e -r README.md", "grep -- -r README.md", "grep needle"])(
85 },
86 );
87
88+test.each([
89+ "echo ok | rg -n -A45 -B4 '^#.*bell-features|bell-features ='",
90+ "echo ok | rg needle | rg other",
91+ "echo ok | (rg needle)",
92+])("allows piped rg input at filesystem roots: %s", async (command) => {
93+ for (const directory of ["/", process.env.HOME!]) {
94+ expect(await checkDeterministic("bash", { command }, directory)).toMatchObject({ decision: "allow" });
95+ }
96+});
97+
98+test("reviews ghostty rather than flagging its output as a filesystem search", async () => {
99+ expect(
100+ await checkDeterministic(
101+ "bash",
102+ {
103+ command:
104+ "ghostty +show-config --default --docs 2>/dev/null | rg -n -A45 -B4 '^#.*bell-features|bell-features ='",
105+ },
106+ "/",
107+ ),
108+ ).toBeUndefined();
109+});
110+
111 const reviewed = [
112 "rtk ls",
113 'echo "$TARGET"',