gm-oc 2.0.177 → 2.0.178
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 +31 -20
- package/package.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) => {
|
|
@@ -134,35 +149,31 @@ const run = () => {
|
|
|
134
149
|
} else {
|
|
135
150
|
result = runWithFile(lang, safeCode);
|
|
136
151
|
}
|
|
137
|
-
return
|
|
152
|
+
return allowWithNoop(`exec:${lang} output:\n\n${result || '(no output)'}`);
|
|
138
153
|
} catch (e) {
|
|
139
|
-
return
|
|
154
|
+
return allowWithNoop(`exec:${lang} error:\n\n${(e.stdout || '') + (e.stderr || '') || e.message || '(exec failed)'}`);
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
|
|
143
158
|
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
159
|
let helpText = '';
|
|
145
160
|
try { helpText = '\n\n' + execSync('bun x gm-exec --help', { timeout: 10000 }).toString().trim(); } catch (e) {}
|
|
146
|
-
return
|
|
161
|
+
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\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
162
|
}
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
const allowedTools = ['agent-browser', 'Skill', 'code-search', 'electron', 'TaskOutput', 'ReadMcpResourceTool', 'ListMcpResourcesTool'];
|
|
151
|
-
if (allowedTools.includes(tool_name)) return
|
|
166
|
+
if (allowedTools.includes(tool_name)) return allow();
|
|
152
167
|
|
|
153
|
-
return
|
|
168
|
+
return allow();
|
|
154
169
|
} catch (error) {
|
|
155
|
-
return
|
|
170
|
+
return allow();
|
|
156
171
|
}
|
|
157
172
|
};
|
|
158
173
|
|
|
159
174
|
try {
|
|
160
175
|
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' }));
|
|
176
|
+
console.log(JSON.stringify(result));
|
|
166
177
|
process.exit(0);
|
|
167
178
|
} catch (error) {
|
|
168
179
|
process.exit(0);
|