Diff
1diff --git a/src/deterministic.ts b/src/deterministic.ts
2index 4651187fd9a567b7d15ad4edb1ccf1ba83d308d2..6ccb8ebee80637d9f0ec4267c84842567e5ac782 100644
3--- a/src/deterministic.ts
4+++ b/src/deterministic.ts
5@@ -6,6 +6,7 @@ import {
6 SHELL_CONTROL_RE,
7 STDERR_REDIRECT_RE,
8 } from "./rules"
9+import { stripRtkPrefix } from "./normalizer"
10
11 function hasShellControl(cmd: string): boolean {
12 return SHELL_CONTROL_RE.test(cmd.replace(/"[^"]*"|'[^']*'/g, '""'))
13@@ -57,7 +58,9 @@ function checkConfigAllow(command: string): Decision | undefined {
14 function checkBash(command: string): Decision | undefined {
15 // Trailing `2>&1` / `2>/dev/null` are security-neutral; strip before matching
16 // so SHELL_CONTROL_RE's `>` check doesn't reject them.
17- const cleaned = command.replace(STDERR_REDIRECT_RE, "")
18+ // Strip optional `rtk ` prefix (transparent output-filter proxy) so policy
19+ // decisions apply to the proxied command.
20+ const cleaned = stripRtkPrefix(command.replace(STDERR_REDIRECT_RE, ""))
21 return checkHardAllow(cleaned) ?? checkConfigAllow(cleaned) ?? checkAskPatterns(cleaned)
22 }
23
24diff --git a/src/normalizer.ts b/src/normalizer.ts
25index b36259bec9a41f2a5fb567c6e238a4a5e6d0aeed..5a3eff3627f098e906a5a7f6b83e80b70d38eced 100644
26--- a/src/normalizer.ts
27+++ b/src/normalizer.ts
28@@ -1,11 +1,21 @@
29 import { createHash } from "crypto"
30 import { POLICY_VERSION } from "./rules"
31
32+// rtk (https://github.com/rtk-ai/rtk) is a transparent proxy that runs the
33+// underlying command and reformats output for fewer tokens. Strip the prefix
34+// so policy decisions match the proxied command.
35+const RTK_PREFIX_RE = /^rtk\s+/
36+
37+export function stripRtkPrefix(command: string): string {
38+ return command.replace(RTK_PREFIX_RE, "")
39+}
40+
41 function normalizeBashCommand(command: string): string {
42 let n = command.split(/\s+/).join(" ").trim()
43 const home = process.env.HOME ?? "~"
44 n = n.replaceAll("~", home)
45 n = n.replace(/;+\s*$/, "").trim()
46+ n = stripRtkPrefix(n)
47 return n
48 }
49
50diff --git a/src/rules.ts b/src/rules.ts
51index 4d714b044fbded167fd2d69e91ad355db432333e..5e4cd8471e7db7ddf8abc28fc639de7680e9cac1 100644
52--- a/src/rules.ts
53+++ b/src/rules.ts
54@@ -1,5 +1,5 @@
55 // Bump to invalidate all cached decisions when rules change.
56-export const POLICY_VERSION = 3;
57+export const POLICY_VERSION = 4;
58
59 // Trailing stderr redirections that are safe to strip before pattern matching.
60 // `2>&1` and `2>/dev/null` have no security implication but would otherwise
61diff --git a/test/deterministic.test.ts b/test/deterministic.test.ts
62index 74a2577cd4b603ecf4c9de37ec8c7c9321f2ce60..5061a8753845b4d3bcf6c75f1bb6dcef54a563fc 100644
63--- a/test/deterministic.test.ts
64+++ b/test/deterministic.test.ts
65@@ -197,3 +197,37 @@ test("webfetch always allowed", () => {
66 test("unknown tool type returns undefined", () => {
67 expect(checkDeterministic("mcp__linear__list", { foo: 1 })).toBeUndefined()
68 })
69+
70+describe("rtk prefix is transparent", () => {
71+ const allowed = [
72+ "rtk ls -la",
73+ "rtk ls src",
74+ "rtk cat package.json",
75+ "rtk grep -rn 'TODO' src",
76+ "rtk git status",
77+ "rtk find src -type f -name '*.ts'",
78+ "rtk ls -la 2>&1",
79+ ]
80+ for (const cmd of allowed) {
81+ test(`allow: ${cmd}`, () => {
82+ const d = checkDeterministic("bash", { command: cmd })
83+ expect(d).toBeDefined()
84+ expect(d!.decision).toBe("allow")
85+ })
86+ }
87+
88+ test("rtk-wrapped dangerous command still asks", () => {
89+ const d = checkDeterministic("bash", { command: "rtk sudo apt install foo" })
90+ expect(d).toBeDefined()
91+ expect(d!.decision).toBe("ask")
92+ })
93+
94+ test("rtk alone (no subcommand) falls through to LLM", () => {
95+ expect(checkDeterministic("bash", { command: "rtk" })).toBeUndefined()
96+ })
97+
98+ test("rtk with non-proxy subcommand falls through to LLM", () => {
99+ // `rtk smart` -> `smart`, not a known pattern
100+ expect(checkDeterministic("bash", { command: "rtk smart src/index.ts" })).toBeUndefined()
101+ })
102+})
103diff --git a/test/normalizer.test.ts b/test/normalizer.test.ts
104index 8d2246516c30f707073dc20f7e1b14696c324954..b3cf7fe867ffe2021ae4ee60eb42b326d957c01c 100644
105--- a/test/normalizer.test.ts
106+++ b/test/normalizer.test.ts
107@@ -28,6 +28,12 @@ describe("normalizeRequest", () => {
108 const n = normalizeRequest("edit", { path: "/tmp/x" })
109 expect(n).toContain("edit:")
110 })
111+
112+ test("bash — strips rtk prefix so cache key matches proxied command", () => {
113+ const a = normalizeRequest("bash", { command: "rtk ls -la" })
114+ const b = normalizeRequest("bash", { command: "ls -la" })
115+ expect(a).toBe(b)
116+ })
117 })
118
119 describe("cacheKey", () => {