agent-relay-runner 0.123.1 → 0.123.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${HOOK_DIR}/rg-r-guard.cjs\"",
10
+ "timeout": 5,
11
+ "statusMessage": "Checking ripgrep replace guard",
12
+ "suppressOutput": true
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ let input = "";
5
+ process.stdin.setEncoding("utf8");
6
+ process.stdin.on("data", (chunk) => {
7
+ input += chunk;
8
+ });
9
+ process.stdin.on("end", () => {
10
+ let payload;
11
+ try {
12
+ payload = JSON.parse(input || "{}");
13
+ } catch {
14
+ return;
15
+ }
16
+
17
+ const toolName = String(payload.tool_name || payload.toolName || "").toLowerCase();
18
+ const command = String(payload.tool_input?.command || payload.toolInput?.command || "");
19
+ if (toolName !== "bash" || !command || !containsRipgrepReplaceFootgun(command)) return;
20
+
21
+ const reason = "Blocked: `rg -r` is ripgrep replace, not recursive search. Use `rg PATTERN PATH` for search, or `rg --replace REPLACEMENT PATTERN` only when replacement output is intentional.";
22
+ process.stdout.write(JSON.stringify({
23
+ hookSpecificOutput: {
24
+ hookEventName: "PreToolUse",
25
+ permissionDecision: "deny",
26
+ permissionDecisionReason: reason,
27
+ },
28
+ }));
29
+ process.stdout.write("\n");
30
+ });
31
+
32
+ function containsRipgrepReplaceFootgun(command) {
33
+ for (const segment of splitShellSegments(command)) {
34
+ const words = shellWords(segment);
35
+ for (let i = 0; i < words.length; i += 1) {
36
+ const word = basename(words[i] || "");
37
+ if (word !== "rg" && word !== "ripgrep") continue;
38
+ if (ripgrepArgsUseReplace(words.slice(i + 1))) return true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function ripgrepArgsUseReplace(args) {
45
+ for (let i = 0; i < args.length; i += 1) {
46
+ const arg = args[i] || "";
47
+ if (arg === "--") return false;
48
+ if (arg === "--replace" || arg.startsWith("--replace=")) return true;
49
+ if (arg === "-r") return true;
50
+ if (/^-([A-Za-z]*r[A-Za-z]*|[A-Za-z]*r=.+)$/.test(arg)) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function splitShellSegments(text) {
56
+ const segments = [];
57
+ let current = "";
58
+ let quote = "";
59
+ let escaped = false;
60
+ for (let i = 0; i < text.length; i += 1) {
61
+ const ch = text[i];
62
+ if (escaped) {
63
+ current += ch;
64
+ escaped = false;
65
+ continue;
66
+ }
67
+ if (ch === "\\") {
68
+ current += ch;
69
+ escaped = true;
70
+ continue;
71
+ }
72
+ if (quote) {
73
+ current += ch;
74
+ if (ch === quote) quote = "";
75
+ continue;
76
+ }
77
+ if (ch === "'" || ch === "\"") {
78
+ quote = ch;
79
+ current += ch;
80
+ continue;
81
+ }
82
+ // Command-substitution + grouping boundaries (`$(rg -r …)`, backtick `` `rg -r …` ``,
83
+ // subshells) split into their own segments so the inner command is scanned on its own
84
+ // (basename of `$(rg` ≠ rg otherwise) and its args never bleed into the outer command's
85
+ // scan. Only fires OUTSIDE quotes — a single/double-quoted literal is left intact, matching
86
+ // the tokenizer's existing quote handling. Known gap: substitution inside double quotes
87
+ // ("$(rg -r x)") is still treated as an opaque quoted word.
88
+ if (ch === "(" || ch === ")" || ch === "`") {
89
+ if (current.trim()) segments.push(current);
90
+ current = "";
91
+ continue;
92
+ }
93
+ if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
94
+ if (current.trim()) segments.push(current);
95
+ current = "";
96
+ continue;
97
+ }
98
+ current += ch;
99
+ }
100
+ if (current.trim()) segments.push(current);
101
+ return segments;
102
+ }
103
+
104
+ function shellWords(text) {
105
+ const words = [];
106
+ let current = "";
107
+ let quote = "";
108
+ let escaped = false;
109
+ for (let i = 0; i < text.length; i += 1) {
110
+ const ch = text[i];
111
+ if (escaped) {
112
+ current += ch;
113
+ escaped = false;
114
+ continue;
115
+ }
116
+ if (ch === "\\") {
117
+ escaped = true;
118
+ continue;
119
+ }
120
+ if (quote) {
121
+ if (ch === quote) quote = "";
122
+ else current += ch;
123
+ continue;
124
+ }
125
+ if (ch === "'" || ch === "\"") {
126
+ quote = ch;
127
+ continue;
128
+ }
129
+ if (/\s/.test(ch)) {
130
+ if (current) {
131
+ words.push(current);
132
+ current = "";
133
+ }
134
+ continue;
135
+ }
136
+ current += ch;
137
+ }
138
+ if (current) words.push(current);
139
+ return words;
140
+ }
141
+
142
+ function basename(value) {
143
+ return value.split("/").pop() || value;
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.123.1",
3
+ "version": "0.123.2",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,9 @@
10
10
  "files": [
11
11
  "src/**/*.ts",
12
12
  "plugins/**",
13
- "!src/**/*.test.ts"
13
+ "hooks/**",
14
+ "!src/**/*.test.ts",
15
+ "!hooks/**/*.test.ts"
14
16
  ],
15
17
  "license": "AGPL-3.0-or-later",
16
18
  "author": "Edin Mujkanovic <edin@exelerus.com>",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.123.1",
4
+ "version": "0.123.2",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }