gm-codex 2.0.177 → 2.0.179
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.
- package/hooks/pre-tool-use-hook.js +59 -22
- package/package.json +1 -1
- package/plugin.json +1 -1
|
@@ -11,16 +11,31 @@ const writeTools = ['Write', 'write_file'];
|
|
|
11
11
|
const searchTools = ['glob', 'search_file_content', 'Search', 'search'];
|
|
12
12
|
const forbiddenTools = ['find', 'Find', 'Glob', 'Grep'];
|
|
13
13
|
|
|
14
|
+
const allow = (additionalContext) => ({
|
|
15
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', ...(additionalContext && { additionalContext }) }
|
|
16
|
+
});
|
|
17
|
+
const deny = (reason) => isGemini
|
|
18
|
+
? { decision: 'deny', reason }
|
|
19
|
+
: { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } };
|
|
20
|
+
const allowWithNoop = (context) => ({
|
|
21
|
+
hookSpecificOutput: {
|
|
22
|
+
hookEventName: 'PreToolUse',
|
|
23
|
+
permissionDecision: 'allow',
|
|
24
|
+
additionalContext: context,
|
|
25
|
+
updatedInput: { command: 'echo ""' }
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
14
29
|
const run = () => {
|
|
15
30
|
try {
|
|
16
31
|
const input = fs.readFileSync(0, 'utf-8');
|
|
17
32
|
const data = JSON.parse(input);
|
|
18
33
|
const { tool_name, tool_input } = data;
|
|
19
34
|
|
|
20
|
-
if (!tool_name) return
|
|
35
|
+
if (!tool_name) return allow();
|
|
21
36
|
|
|
22
37
|
if (forbiddenTools.includes(tool_name)) {
|
|
23
|
-
return
|
|
38
|
+
return deny('Use the code-search skill for codebase exploration instead of Grep/Glob/find. Describe what you need in plain language — it understands intent, not just patterns.');
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
if (writeTools.includes(tool_name)) {
|
|
@@ -30,7 +45,7 @@ const run = () => {
|
|
|
30
45
|
const base = path.basename(file_path).toLowerCase();
|
|
31
46
|
if ((ext === '.md' || ext === '.txt' || base.startsWith('features_list')) &&
|
|
32
47
|
!base.startsWith('claude') && !base.startsWith('readme') && !inSkillsDir) {
|
|
33
|
-
return
|
|
48
|
+
return deny('Cannot create documentation files. Only CLAUDE.md and readme.md are maintained. For task-specific notes, use .prd. For permanent reference material, add to CLAUDE.md.');
|
|
34
49
|
}
|
|
35
50
|
if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/.test(base) ||
|
|
36
51
|
/^(jest|vitest|mocha|ava|jasmine|tap)\.(config|setup)/.test(base) ||
|
|
@@ -38,24 +53,24 @@ const run = () => {
|
|
|
38
53
|
file_path.includes('/tests/') || file_path.includes('/fixtures/') ||
|
|
39
54
|
file_path.includes('/test-data/') || file_path.includes('/__mocks__/') ||
|
|
40
55
|
/\.(snap|stub|mock|fixture)\.(js|ts|json)$/.test(base)) {
|
|
41
|
-
return
|
|
56
|
+
return deny('Test files forbidden on disk. Use Bash tool with real services for all testing.');
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
if (searchTools.includes(tool_name)) return
|
|
60
|
+
if (searchTools.includes(tool_name)) return allow();
|
|
46
61
|
|
|
47
62
|
if (tool_name === 'Task' && (tool_input?.subagent_type || '') === 'Explore') {
|
|
48
|
-
return
|
|
63
|
+
return deny('Use gm:thorns-overview for codebase insight, then use gm:code-search');
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
if (tool_name === 'EnterPlanMode') {
|
|
52
|
-
return
|
|
67
|
+
return deny('Plan mode is disabled. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) via gm:gm subagent instead.');
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
if (tool_name === 'Skill') {
|
|
56
71
|
const skill = (tool_input?.skill || '').toLowerCase().replace(/^gm:/, '');
|
|
57
72
|
if (skill === 'explore' || skill === 'search') {
|
|
58
|
-
return
|
|
73
|
+
return deny('Use the code-search skill for codebase exploration. Describe what you need in plain language — it understands intent, not just patterns.');
|
|
59
74
|
}
|
|
60
75
|
}
|
|
61
76
|
|
|
@@ -66,7 +81,7 @@ const run = () => {
|
|
|
66
81
|
const rawLang = (execMatch[1] || '').toLowerCase();
|
|
67
82
|
const code = execMatch[2];
|
|
68
83
|
if (/^\s*agent-browser\s/.test(code)) {
|
|
69
|
-
return
|
|
84
|
+
return deny(`Do not call agent-browser via exec:bash. Use exec:agent-browser instead:\n\nexec:agent-browser\n<plain JS here>\n\nThe code is piped directly to the browser eval. No base64, no flags, no shell wrapping.`);
|
|
70
85
|
}
|
|
71
86
|
const cwd = tool_input?.cwd;
|
|
72
87
|
const detectLang = (src) => {
|
|
@@ -75,7 +90,7 @@ const run = () => {
|
|
|
75
90
|
if (/^\s*(echo |ls |cd |mkdir |rm |cat |grep |find |export |source |#!)/.test(src)) return 'bash';
|
|
76
91
|
return 'nodejs';
|
|
77
92
|
};
|
|
78
|
-
const aliases = { js: 'nodejs', javascript: 'nodejs', ts: 'typescript', node: 'nodejs', py: 'python', sh: 'bash', shell: 'bash', zsh: 'bash', powershell: 'bash', ps1: 'bash', cmd: 'bash', browser: 'agent-browser', ab: 'agent-browser' };
|
|
93
|
+
const aliases = { js: 'nodejs', javascript: 'nodejs', ts: 'typescript', node: 'nodejs', py: 'python', sh: 'bash', shell: 'bash', zsh: 'bash', powershell: 'bash', ps1: 'bash', cmd: 'bash', browser: 'agent-browser', ab: 'agent-browser', codesearch: 'codesearch', search: 'search', status: 'status', sleep: 'sleep', close: 'close', runner: 'runner' };
|
|
79
94
|
const lang = aliases[rawLang] || rawLang || detectLang(code);
|
|
80
95
|
const IS_WIN = process.platform === 'win32';
|
|
81
96
|
const stripFooter = (s) => s.replace(/\n\[Running tools\][\s\S]*$/, '').trimEnd();
|
|
@@ -93,7 +108,7 @@ const run = () => {
|
|
|
93
108
|
const r = spawnSync('bun', ['x', 'gm-exec', 'exec', `--lang=${l}`, `--file=${tmp}`, ...(cwd ? [`--cwd=${cwd}`] : [])], { encoding: 'utf-8', timeout: 65000 });
|
|
94
109
|
try { fs.unlinkSync(tmp); } catch (e) {}
|
|
95
110
|
let out = stripFooter((r.stdout || '') + (r.stderr || ''));
|
|
96
|
-
const bg = out.match(/
|
|
111
|
+
const bg = out.match(/Task ID:\s*(task_\S+)/);
|
|
97
112
|
if (bg) {
|
|
98
113
|
spawnSync('bun', ['x', 'gm-exec', 'sleep', bg[1], '60'], { encoding: 'utf-8', timeout: 70000 });
|
|
99
114
|
const sr = spawnSync('bun', ['x', 'gm-exec', 'status', bg[1]], { encoding: 'utf-8', timeout: 15000 });
|
|
@@ -108,6 +123,32 @@ const run = () => {
|
|
|
108
123
|
try { const d = Buffer.from(t, 'base64').toString('utf-8'); return /[\x00-\x08\x0b\x0e-\x1f]/.test(d) ? s : d; } catch { return s; }
|
|
109
124
|
};
|
|
110
125
|
const safeCode = decodeB64(code);
|
|
126
|
+
if (['codesearch', 'search'].includes(lang)) {
|
|
127
|
+
const query = safeCode.trim();
|
|
128
|
+
const r = spawnSync('bun', ['x', 'codebasesearch', query], { encoding: 'utf-8', timeout: 30000, ...(cwd && { cwd }) });
|
|
129
|
+
return allowWithNoop(`exec:${lang} output:\n\n${stripFooter((r.stdout || '') + (r.stderr || '')) || '(no results)'}`);
|
|
130
|
+
}
|
|
131
|
+
if (lang === 'status') {
|
|
132
|
+
const taskId = safeCode.trim();
|
|
133
|
+
const r = spawnSync('bun', ['x', 'gm-exec', 'status', taskId], { encoding: 'utf-8', timeout: 15000 });
|
|
134
|
+
return allowWithNoop(`exec:status output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
|
|
135
|
+
}
|
|
136
|
+
if (lang === 'sleep') {
|
|
137
|
+
const parts = safeCode.trim().split(/\s+/);
|
|
138
|
+
const args = ['x', 'gm-exec', 'sleep', ...parts];
|
|
139
|
+
const r = spawnSync('bun', args, { encoding: 'utf-8', timeout: 70000 });
|
|
140
|
+
return allowWithNoop(`exec:sleep output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
|
|
141
|
+
}
|
|
142
|
+
if (lang === 'close') {
|
|
143
|
+
const taskId = safeCode.trim();
|
|
144
|
+
const r = spawnSync('bun', ['x', 'gm-exec', 'close', taskId], { encoding: 'utf-8', timeout: 15000 });
|
|
145
|
+
return allowWithNoop(`exec:close output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
|
|
146
|
+
}
|
|
147
|
+
if (lang === 'runner') {
|
|
148
|
+
const sub = safeCode.trim();
|
|
149
|
+
const r = spawnSync('bun', ['x', 'gm-exec', 'runner', sub], { encoding: 'utf-8', timeout: 15000 });
|
|
150
|
+
return allowWithNoop(`exec:runner output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
|
|
151
|
+
}
|
|
111
152
|
try {
|
|
112
153
|
let result;
|
|
113
154
|
if (lang === 'bash') {
|
|
@@ -134,35 +175,31 @@ const run = () => {
|
|
|
134
175
|
} else {
|
|
135
176
|
result = runWithFile(lang, safeCode);
|
|
136
177
|
}
|
|
137
|
-
return
|
|
178
|
+
return allowWithNoop(`exec:${lang} output:\n\n${result || '(no output)'}`);
|
|
138
179
|
} catch (e) {
|
|
139
|
-
return
|
|
180
|
+
return allowWithNoop(`exec:${lang} error:\n\n${(e.stdout || '') + (e.stderr || '') || e.message || '(exec failed)'}`);
|
|
140
181
|
}
|
|
141
182
|
}
|
|
142
183
|
|
|
143
184
|
if (!/^exec(\s|:)/.test(command) && !/^bun x gm-exec(@[^\s]*)?(\s|$)/.test(command) && !/^git /.test(command) && !/^bun x codebasesearch/.test(command) && !/(\bclaude\b)/.test(command) && !/^npm install .* \/config\/.gmweb/.test(command) && !/^bun install --cwd \/config\/.gmweb/.test(command)) {
|
|
144
185
|
let helpText = '';
|
|
145
186
|
try { helpText = '\n\n' + execSync('bun x gm-exec --help', { timeout: 10000 }).toString().trim(); } catch (e) {}
|
|
146
|
-
return
|
|
187
|
+
return deny(`Bash is restricted to exec:<lang> and git.\n\nexec:<lang> syntax (lang auto-detected if omitted):\n exec:nodejs / exec:python / exec:bash / exec:typescript\n exec:go / exec:rust / exec:java / exec:c / exec:cpp\n exec:agent-browser ← plain JS piped to browser eval (NO base64)\n exec ← auto-detects language\n\nTask management shortcuts (body = args):\n exec:status\n <task_id>\n\n exec:sleep\n <task_id> [seconds] [--next-output]\n\n exec:close\n <task_id>\n\n exec:runner\n start|stop|status\n\nCode search shortcut:\n exec:codesearch\n <natural language query>\n\nNEVER encode agent-browser code as base64 — pass plain JS directly.\n\nbun x gm-exec${helpText}\n\nAll other Bash commands are blocked.`);
|
|
147
188
|
}
|
|
148
189
|
}
|
|
149
190
|
|
|
150
191
|
const allowedTools = ['agent-browser', 'Skill', 'code-search', 'electron', 'TaskOutput', 'ReadMcpResourceTool', 'ListMcpResourcesTool'];
|
|
151
|
-
if (allowedTools.includes(tool_name)) return
|
|
192
|
+
if (allowedTools.includes(tool_name)) return allow();
|
|
152
193
|
|
|
153
|
-
return
|
|
194
|
+
return allow();
|
|
154
195
|
} catch (error) {
|
|
155
|
-
return
|
|
196
|
+
return allow();
|
|
156
197
|
}
|
|
157
198
|
};
|
|
158
199
|
|
|
159
200
|
try {
|
|
160
201
|
const result = run();
|
|
161
|
-
|
|
162
|
-
console.log(JSON.stringify({ decision: isGemini ? 'deny' : 'block', reason: result.reason }));
|
|
163
|
-
process.exit(0);
|
|
164
|
-
}
|
|
165
|
-
if (isGemini) console.log(JSON.stringify({ decision: 'allow' }));
|
|
202
|
+
console.log(JSON.stringify(result));
|
|
166
203
|
process.exit(0);
|
|
167
204
|
} catch (error) {
|
|
168
205
|
process.exit(0);
|
package/package.json
CHANGED