mstro-app 0.3.1 → 0.3.5

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.
Files changed (36) hide show
  1. package/README.md +3 -19
  2. package/bin/mstro.js +15 -177
  3. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  4. package/dist/server/mcp/bouncer-integration.js +43 -135
  5. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  6. package/dist/server/services/platform.d.ts.map +1 -1
  7. package/dist/server/services/platform.js +2 -13
  8. package/dist/server/services/platform.js.map +1 -1
  9. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  10. package/dist/server/services/websocket/file-explorer-handlers.js +17 -1
  11. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  12. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  13. package/dist/server/services/websocket/file-utils.js +26 -20
  14. package/dist/server/services/websocket/file-utils.js.map +1 -1
  15. package/dist/server/services/websocket/types.d.ts +1 -1
  16. package/dist/server/services/websocket/types.d.ts.map +1 -1
  17. package/dist/server/utils/paths.d.ts +0 -12
  18. package/dist/server/utils/paths.d.ts.map +1 -1
  19. package/dist/server/utils/paths.js +0 -12
  20. package/dist/server/utils/paths.js.map +1 -1
  21. package/package.json +1 -2
  22. package/server/README.md +0 -1
  23. package/server/mcp/README.md +0 -5
  24. package/server/mcp/bouncer-integration.ts +55 -210
  25. package/server/services/platform.ts +2 -12
  26. package/server/services/websocket/file-explorer-handlers.ts +16 -1
  27. package/server/services/websocket/file-utils.ts +29 -24
  28. package/server/services/websocket/types.ts +1 -0
  29. package/server/utils/paths.ts +0 -14
  30. package/bin/configure-claude.js +0 -298
  31. package/dist/server/mcp/bouncer-cli.d.ts +0 -3
  32. package/dist/server/mcp/bouncer-cli.d.ts.map +0 -1
  33. package/dist/server/mcp/bouncer-cli.js +0 -138
  34. package/dist/server/mcp/bouncer-cli.js.map +0 -1
  35. package/hooks/bouncer.sh +0 -159
  36. package/server/mcp/bouncer-cli.ts +0 -180
@@ -1,298 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
3
- // Licensed under the MIT License. See LICENSE file for details.
4
-
5
- /**
6
- * Mstro Claude Configuration Tool
7
- *
8
- * Automatically configures ~/.claude/settings.json and installs
9
- * the bouncer hook for Claude Code integration.
10
- *
11
- * Usage:
12
- * npx mstro configure-hooks
13
- * node bin/configure-claude.js
14
- */
15
-
16
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
- import { homedir } from 'node:os';
18
- import { dirname, join, resolve } from 'node:path';
19
- import { createInterface } from 'node:readline';
20
- import { fileURLToPath } from 'node:url';
21
-
22
- const __filename = fileURLToPath(import.meta.url);
23
- const __dirname = dirname(__filename);
24
- const MSTRO_ROOT = resolve(__dirname, '..');
25
-
26
- const CLAUDE_DIR = join(homedir(), '.claude');
27
- const CLAUDE_HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
28
- const CLAUDE_SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
29
- const _BOUNCER_HOOK_SOURCE = join(MSTRO_ROOT, 'hooks', 'bouncer.sh');
30
- const BOUNCER_CLI_PATH = join(MSTRO_ROOT, 'server', 'mcp', 'bouncer-cli.ts');
31
- const TSX_PATH = join(MSTRO_ROOT, 'node_modules', '.bin', 'tsx');
32
-
33
- // ANSI colors
34
- const colors = {
35
- reset: '\x1b[0m',
36
- bold: '\x1b[1m',
37
- green: '\x1b[32m',
38
- yellow: '\x1b[33m',
39
- blue: '\x1b[34m',
40
- red: '\x1b[31m',
41
- dim: '\x1b[2m',
42
- };
43
-
44
- function log(msg, color = '') {
45
- console.log(`${color}${msg}${colors.reset}`);
46
- }
47
-
48
- function prompt(question) {
49
- const rl = createInterface({ input: process.stdin, output: process.stdout });
50
- return new Promise((resolve) => {
51
- rl.question(question, (answer) => {
52
- rl.close();
53
- resolve(answer.trim().toLowerCase());
54
- });
55
- });
56
- }
57
-
58
- /**
59
- * Generate the bouncer hook script with embedded paths
60
- * This ensures the hook knows where to find the bouncer CLI regardless of
61
- * how/where mstro was installed (npm global, npx, etc.)
62
- */
63
- function generateBouncerHook(tsxPath, bouncerCliPath) {
64
- return `#!/usr/bin/env bash
65
- #
66
- # Mstro Bouncer Gate - Claude Code Hook
67
- #
68
- # This hook intercepts Claude Code tool calls and routes them through
69
- # the Mstro bouncer for security analysis before execution.
70
- #
71
- # Generated by: npx mstro configure-hooks
72
- # Dependencies: Node.js (no jq or bun required)
73
- #
74
-
75
- set -euo pipefail
76
-
77
- # Paths configured at install time
78
- TSX_PATH="${tsxPath}"
79
- BOUNCER_CLI="${bouncerCliPath}"
80
-
81
- # User-configurable settings
82
- BOUNCER_TIMEOUT="\${BOUNCER_TIMEOUT:-10}"
83
- BOUNCER_LOG="\${BOUNCER_LOG:-$HOME/.claude/logs/bouncer.log}"
84
-
85
- # Ensure log directory exists
86
- mkdir -p "$(dirname "$BOUNCER_LOG")"
87
-
88
- # Read hook input from stdin
89
- INPUT=$(cat)
90
-
91
- # Run the bouncer via tsx (handles TypeScript execution)
92
- if [ -x "$TSX_PATH" ] && [ -f "$BOUNCER_CLI" ]; then
93
- RESULT=$(echo "$INPUT" | timeout "$BOUNCER_TIMEOUT" "$TSX_PATH" "$BOUNCER_CLI" 2>> "$BOUNCER_LOG" || echo '{"decision": "allow", "reason": "Bouncer timeout or error"}')
94
- echo "$RESULT"
95
- else
96
- # Fallback: use inline Node.js for basic pattern matching
97
- node --input-type=module -e "
98
- const input = JSON.parse(process.argv[1]);
99
- const toolName = input.tool_name || input.toolName || 'unknown';
100
- const toolInput = input.input || input.toolInput || {};
101
-
102
- // Quick allow for read-only operations
103
- const readOnly = ['Read', 'Glob', 'Grep', 'Search', 'List', 'WebFetch', 'WebSearch'];
104
- if (readOnly.includes(toolName)) {
105
- console.log(JSON.stringify({ decision: 'allow', reason: 'Read-only operation' }));
106
- process.exit(0);
107
- }
108
-
109
- // Build operation string
110
- let op = toolName + ': ';
111
- if (toolName === 'Bash') op += toolInput.command || '';
112
- else if (['Write', 'Edit'].includes(toolName)) op += toolInput.file_path || toolInput.filePath || '';
113
- else op += JSON.stringify(toolInput);
114
-
115
- // Critical threat patterns
116
- const threats = [
117
- [/rm\\s+-rf\\s+(\\/|~)(\\$|\\s)/, 'recursive delete of root/home'],
118
- [/:\\(\\)\\{.*\\}/, 'fork bomb'],
119
- [/dd\\s+if=\\/dev\\/zero\\s+of=\\/dev\\/sd/, 'disk overwrite'],
120
- [/mkfs\\s+\\/dev\\/sd/, 'filesystem format'],
121
- ];
122
-
123
- for (const [pattern, reason] of threats) {
124
- if (pattern.test(op)) {
125
- console.log(JSON.stringify({ decision: 'deny', reason: 'Critical threat: ' + reason }));
126
- process.exit(0);
127
- }
128
- }
129
-
130
- console.log(JSON.stringify({ decision: 'allow', reason: 'Fallback: basic check passed' }));
131
- " "$INPUT"
132
- fi
133
- `;
134
- }
135
-
136
- function ensureDirectories() {
137
- log('Step 1: Checking ~/.claude directory structure...', colors.bold);
138
-
139
- for (const dir of [CLAUDE_DIR, CLAUDE_HOOKS_DIR, join(CLAUDE_DIR, 'logs')]) {
140
- if (!existsSync(dir)) {
141
- log(` Creating ${dir}`, colors.dim);
142
- mkdirSync(dir, { recursive: true });
143
- } else {
144
- log(` ${dir} exists`, colors.green);
145
- }
146
- }
147
-
148
- log(' Done!\n', colors.green);
149
- }
150
-
151
- async function installBouncerHook(forceYes, isInteractive) {
152
- log('Step 2: Installing bouncer hook...', colors.bold);
153
-
154
- const hookDest = join(CLAUDE_HOOKS_DIR, 'bouncer.sh');
155
- const hookContent = generateBouncerHook(TSX_PATH, BOUNCER_CLI_PATH);
156
- const hookExists = existsSync(hookDest);
157
-
158
- if (hookExists) {
159
- log(` Hook already exists at ${hookDest}`, colors.yellow);
160
- let overwrite = forceYes;
161
-
162
- if (!forceYes && isInteractive) {
163
- const answer = await prompt(' Overwrite existing hook? [y/N]: ');
164
- overwrite = answer === 'y' || answer === 'yes';
165
- }
166
-
167
- if (!overwrite) {
168
- log(' Skipping hook installation', colors.dim);
169
- log(' Done!\n', colors.green);
170
- return hookDest;
171
- }
172
- }
173
-
174
- writeFileSync(hookDest, hookContent);
175
- chmodSync(hookDest, 0o755);
176
- log(` ${hookExists ? 'Overwrote' : 'Installed'} ${hookDest}`, colors.green);
177
- log(' Done!\n', colors.green);
178
- return hookDest;
179
- }
180
-
181
- function loadSettings() {
182
- if (!existsSync(CLAUDE_SETTINGS_FILE)) {
183
- return {};
184
- }
185
- try {
186
- const content = readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
187
- const settings = JSON.parse(content);
188
- log(` Loaded existing settings from ${CLAUDE_SETTINGS_FILE}`, colors.dim);
189
- return settings;
190
- } catch (err) {
191
- log(` Warning: Could not parse existing settings.json: ${err.message}`, colors.yellow);
192
- log(' Will create new settings file', colors.dim);
193
- return {};
194
- }
195
- }
196
-
197
- async function configureSettings(hookDest, forceYes, isInteractive) {
198
- log('Step 3: Configuring settings.json...', colors.bold);
199
-
200
- const settings = loadSettings();
201
- if (!settings.hooks) {
202
- settings.hooks = {};
203
- }
204
-
205
- const bouncerHook = { type: 'command', command: hookDest, timeout: 10000 };
206
- const bouncerHookConfig = [
207
- { matcher: 'Bash', hooks: [bouncerHook] },
208
- { matcher: 'Write', hooks: [bouncerHook] },
209
- { matcher: 'Edit', hooks: [bouncerHook] }
210
- ];
211
-
212
- const existingPreToolUse = settings.hooks.PreToolUse;
213
-
214
- if (existingPreToolUse) {
215
- log(' Existing PreToolUse hook configuration found:', colors.yellow);
216
- log(` ${JSON.stringify(existingPreToolUse, null, 2).split('\n').join('\n ')}`, colors.dim);
217
-
218
- let update = forceYes;
219
- if (!forceYes && isInteractive) {
220
- const answer = await prompt(' Update PreToolUse hook to use Mstro bouncer? [y/N]: ');
221
- update = answer === 'y' || answer === 'yes';
222
- }
223
-
224
- if (!update) {
225
- log(' Keeping existing PreToolUse configuration', colors.dim);
226
- } else {
227
- settings.hooks.PreToolUse = bouncerHookConfig;
228
- log(' Updated PreToolUse hook configuration', colors.green);
229
- }
230
- } else {
231
- settings.hooks.PreToolUse = bouncerHookConfig;
232
- log(' Added PreToolUse hook configuration', colors.green);
233
- }
234
-
235
- const settingsContent = JSON.stringify(settings, null, 2);
236
- const settingsExists = existsSync(CLAUDE_SETTINGS_FILE);
237
-
238
- if (settingsExists) {
239
- const currentContent = readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
240
- if (currentContent === settingsContent) {
241
- log(' No changes needed to settings.json', colors.dim);
242
- } else {
243
- const backupPath = `${CLAUDE_SETTINGS_FILE}.backup.${Date.now()}`;
244
- writeFileSync(backupPath, currentContent);
245
- log(` Backed up existing settings to ${backupPath}`, colors.dim);
246
- writeFileSync(CLAUDE_SETTINGS_FILE, settingsContent);
247
- log(` Updated ${CLAUDE_SETTINGS_FILE}`, colors.green);
248
- }
249
- } else {
250
- writeFileSync(CLAUDE_SETTINGS_FILE, settingsContent);
251
- log(` Created ${CLAUDE_SETTINGS_FILE}`, colors.green);
252
- }
253
-
254
- log(' Done!\n', colors.green);
255
- return settingsContent;
256
- }
257
-
258
- function printSummary(hookDest, settingsContent) {
259
- log('=== Configuration Complete ===\n', colors.bold + colors.green);
260
- log('The following files have been configured:', colors.bold);
261
- log(` ${CLAUDE_SETTINGS_FILE}`, colors.dim);
262
- log(` ${hookDest}`, colors.dim);
263
- log('');
264
- log('Your settings.json now contains:', colors.bold);
265
- log(`${settingsContent.split('\n').map(l => ` ${l}`).join('\n')}`, colors.dim);
266
- log('');
267
- log('How the bouncer works:', colors.bold);
268
- log('');
269
- log(' Mstro sessions (headless): Full 2-layer security', colors.green);
270
- log(' Layer 1: Pattern matching (<5ms) - instant allow/deny for known operations', colors.dim);
271
- log(' Layer 2: AI analysis (~200-500ms) - context-aware review of ambiguous operations', colors.dim);
272
- log('');
273
- log(' Claude Code terminal REPL (claude): 1-layer security', colors.yellow);
274
- log(' Layer 1: Pattern matching only - blocks critical threats (fork bombs,', colors.dim);
275
- log(' destructive commands), allows everything else', colors.dim);
276
- log(' The AI analysis layer requires a running mstro server.', colors.dim);
277
- log('');
278
- log('To disable the bouncer hook, remove the PreToolUse entry from', colors.dim);
279
- log(`${CLAUDE_SETTINGS_FILE}`, colors.dim);
280
- log('');
281
- }
282
-
283
- async function main() {
284
- log('\n=== Mstro Claude Configuration ===\n', colors.bold + colors.blue);
285
-
286
- const isInteractive = process.stdin.isTTY;
287
- const forceYes = process.argv.includes('--yes') || process.argv.includes('-y');
288
-
289
- ensureDirectories();
290
- const hookDest = await installBouncerHook(forceYes, isInteractive);
291
- const settingsContent = await configureSettings(hookDest, forceYes, isInteractive);
292
- printSummary(hookDest, settingsContent);
293
- }
294
-
295
- main().catch((err) => {
296
- log(`\nError: ${err.message}`, colors.red);
297
- process.exit(1);
298
- });
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=bouncer-cli.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bouncer-cli.d.ts","sourceRoot":"","sources":["../../../server/mcp/bouncer-cli.ts"],"names":[],"mappings":""}
@@ -1,138 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
3
- // Licensed under the MIT License. See LICENSE file for details.
4
- /**
5
- * Bouncer CLI - Shell-callable wrapper for Mstro security bouncer
6
- *
7
- * This CLI reads Claude Code hook input from stdin and returns a security decision.
8
- * It's designed to be called from bouncer.sh.
9
- *
10
- * Input (stdin): Claude Code PreToolUse hook JSON payload
11
- * Output (stdout): JSON decision { decision: "allow"|"deny", reason: string }
12
- *
13
- * The hook payload includes conversation context that we pass to the bouncer
14
- * so it can make context-aware decisions.
15
- */
16
- import { reviewOperation } from './bouncer-integration.js';
17
- /**
18
- * Read all data from stdin (Node.js compatible)
19
- */
20
- async function readStdin() {
21
- return new Promise((resolve, reject) => {
22
- const chunks = [];
23
- process.stdin.on('data', (chunk) => chunks.push(chunk));
24
- process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').trim()));
25
- process.stdin.on('error', reject);
26
- });
27
- }
28
- function buildOperationString(toolName, toolInput) {
29
- if (toolName === 'Bash' && toolInput.command) {
30
- return `${toolName}: ${toolInput.command}`;
31
- }
32
- if (['Write', 'Edit', 'Read'].includes(toolName)) {
33
- const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
34
- return filePath ? `${toolName}: ${filePath}` : `${toolName}: ${JSON.stringify(toolInput)}`;
35
- }
36
- return `${toolName}: ${JSON.stringify(toolInput)}`;
37
- }
38
- /**
39
- * Detect whether the caller is Claude Code (vs mstro).
40
- * Claude Code includes hook_event_name in its payload.
41
- */
42
- function isClaudeCodeHook(hookInput) {
43
- return hookInput.hook_event_name === 'PreToolUse';
44
- }
45
- /**
46
- * Format a bouncer decision for the calling system.
47
- * Claude Code expects: { hookSpecificOutput: { permissionDecision, ... } }
48
- * Mstro expects: { decision, reason, confidence, threatLevel, alternative }
49
- */
50
- function formatDecisionOutput(decision, claudeCode) {
51
- const mappedDecision = decision.decision === 'deny' ? 'deny' : 'allow';
52
- if (claudeCode) {
53
- return JSON.stringify({
54
- hookSpecificOutput: {
55
- hookEventName: 'PreToolUse',
56
- permissionDecision: mappedDecision,
57
- permissionDecisionReason: decision.reasoning,
58
- },
59
- });
60
- }
61
- return JSON.stringify({
62
- decision: mappedDecision,
63
- reason: decision.reasoning,
64
- confidence: decision.confidence,
65
- threatLevel: decision.threatLevel,
66
- alternative: decision.alternative,
67
- });
68
- }
69
- function formatSimpleOutput(d, reason, claudeCode) {
70
- if (claudeCode) {
71
- return JSON.stringify({
72
- hookSpecificOutput: {
73
- hookEventName: 'PreToolUse',
74
- permissionDecision: d,
75
- permissionDecisionReason: reason,
76
- },
77
- });
78
- }
79
- return JSON.stringify({ decision: d, reason });
80
- }
81
- function extractConversationContext(hookInput) {
82
- const lastUserMessage = hookInput.conversation?.last_user_message;
83
- if (lastUserMessage)
84
- return `User's request: "${lastUserMessage}"`;
85
- const recentMessages = hookInput.conversation?.messages?.slice(-5);
86
- if (recentMessages?.length) {
87
- return `Recent conversation:\n${recentMessages.map(m => `${m.role}: ${m.content}`).join('\n')}`;
88
- }
89
- return undefined;
90
- }
91
- async function main() {
92
- const inputStr = await readStdin();
93
- if (!inputStr) {
94
- // Can't detect caller without input — output both-compatible allow
95
- console.log(JSON.stringify({ decision: 'allow', reason: 'Empty input, allowing' }));
96
- process.exit(0);
97
- }
98
- let hookInput;
99
- try {
100
- hookInput = JSON.parse(inputStr);
101
- }
102
- catch (e) {
103
- console.error('[bouncer-cli] Failed to parse input JSON:', e);
104
- console.log(JSON.stringify({ decision: 'allow', reason: 'Invalid JSON input, allowing' }));
105
- process.exit(0);
106
- }
107
- const claudeCode = isClaudeCodeHook(hookInput);
108
- const toolName = hookInput.tool_name || hookInput.toolName || 'unknown';
109
- // Claude Code: tool_input, mstro: input/toolInput
110
- const toolInput = hookInput.tool_input || hookInput.input || hookInput.toolInput || {};
111
- const userRequestContext = extractConversationContext(hookInput);
112
- const lastUserMessage = hookInput.conversation?.last_user_message;
113
- const recentMessages = hookInput.conversation?.messages?.slice(-5);
114
- const bouncerRequest = {
115
- operation: buildOperationString(toolName, toolInput),
116
- context: {
117
- purpose: userRequestContext || 'Tool use request from Claude',
118
- // Claude Code: cwd, mstro: working_directory
119
- workingDirectory: hookInput.cwd || hookInput.working_directory || process.cwd(),
120
- toolName,
121
- toolInput,
122
- userRequest: lastUserMessage,
123
- conversationHistory: recentMessages?.map(m => `${m.role}: ${m.content}`),
124
- sessionId: hookInput.session_id,
125
- },
126
- };
127
- try {
128
- const decision = await reviewOperation(bouncerRequest);
129
- console.log(formatDecisionOutput(decision, claudeCode));
130
- }
131
- catch (error) {
132
- const message = error instanceof Error ? error.message : String(error);
133
- console.error('[bouncer-cli] Error:', message);
134
- console.log(formatSimpleOutput('allow', `Bouncer error: ${message}. Allowing to avoid blocking.`, claudeCode));
135
- }
136
- }
137
- main();
138
- //# sourceMappingURL=bouncer-cli.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bouncer-cli.js","sourceRoot":"","sources":["../../../server/mcp/bouncer-cli.ts"],"names":[],"mappings":"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"}
package/hooks/bouncer.sh DELETED
@@ -1,159 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Copyright (c) 2025-present Mstro, Inc. All rights reserved.
3
- # Licensed under the MIT License. See LICENSE file for details.
4
-
5
- #
6
- # Mstro Bouncer Gate - Claude Code Hook
7
- #
8
- # This hook intercepts Claude Code tool calls and routes them through
9
- # the Mstro bouncer for security analysis before execution.
10
- #
11
- # Installation:
12
- # Run: npx mstro --configure-hooks
13
- #
14
- # Dependencies: Node.js (no jq or bun required)
15
- #
16
-
17
- set -euo pipefail
18
-
19
- # Configuration - MSTRO_BOUNCER_CLI is set by configure-claude.js
20
- BOUNCER_CLI="${MSTRO_BOUNCER_CLI:-}"
21
- BOUNCER_TIMEOUT="${BOUNCER_TIMEOUT:-10}"
22
- BOUNCER_LOG="${BOUNCER_LOG:-$HOME/.claude/logs/bouncer.log}"
23
-
24
- # Ensure log directory exists
25
- mkdir -p "$(dirname "$BOUNCER_LOG")"
26
-
27
- # Read hook input from stdin (JSON format from Claude Code)
28
- INPUT=$(cat)
29
-
30
- # Use Node.js inline to parse JSON and handle logic (eliminates jq dependency)
31
- RESULT=$(node --input-type=module -e "
32
- import { spawn } from 'child_process';
33
- import { appendFileSync } from 'fs';
34
-
35
- const input = JSON.parse(process.argv[1]);
36
- const bouncerCli = process.argv[2];
37
- const timeout = parseInt(process.argv[3], 10) * 1000;
38
- const logFile = process.argv[4];
39
-
40
- const toolName = input.tool_name || input.toolName || 'unknown';
41
- // Claude Code: tool_input, mstro: input/toolInput
42
- const toolInput = input.tool_input || input.input || input.toolInput || {};
43
- const isClaudeCode = input.hook_event_name === 'PreToolUse';
44
-
45
- function log(msg) {
46
- const timestamp = new Date().toISOString();
47
- appendFileSync(logFile, \`[\${timestamp}] \${msg}\n\`);
48
- }
49
-
50
- function output(decision, reason) {
51
- if (isClaudeCode) {
52
- console.log(JSON.stringify({
53
- hookSpecificOutput: {
54
- hookEventName: 'PreToolUse',
55
- permissionDecision: decision,
56
- permissionDecisionReason: reason,
57
- },
58
- }));
59
- } else {
60
- console.log(JSON.stringify({ decision, reason }));
61
- }
62
- }
63
-
64
- // Quick path for read-only and side-effect-free operations
65
- const safeOps = ['Read', 'Glob', 'Grep', 'Search', 'List', 'WebFetch', 'WebSearch',
66
- 'ExitPlanMode', 'EnterPlanMode', 'TodoWrite', 'AskUserQuestion'];
67
- if (safeOps.includes(toolName)) {
68
- output('allow', 'Safe operation (no dangerous side effects)');
69
- process.exit(0);
70
- }
71
-
72
- // Quick path for malformed tool calls with empty params (no-ops)
73
- if (Object.keys(toolInput).length === 0 && ['Edit', 'Write'].includes(toolName)) {
74
- output('allow', 'Empty parameters - no-op');
75
- process.exit(0);
76
- }
77
-
78
- // Build operation string for logging
79
- let operation = toolName + ': ';
80
- if (toolName === 'Bash' && toolInput.command) {
81
- operation += toolInput.command;
82
- } else if (['Write', 'Edit'].includes(toolName)) {
83
- operation += toolInput.file_path || toolInput.filePath || toolInput.path || JSON.stringify(toolInput);
84
- } else {
85
- operation += JSON.stringify(toolInput);
86
- }
87
-
88
- log('Analyzing: ' + toolName);
89
-
90
- // Check if bouncer CLI is available
91
- if (bouncerCli) {
92
- try {
93
- const child = spawn('node', [bouncerCli], {
94
- stdio: ['pipe', 'pipe', 'pipe'],
95
- timeout: timeout
96
- });
97
-
98
- let stdout = '';
99
- let stderr = '';
100
-
101
- child.stdout.on('data', (data) => { stdout += data; });
102
- child.stderr.on('data', (data) => { stderr += data; });
103
-
104
- child.stdin.write(JSON.stringify(input));
105
- child.stdin.end();
106
-
107
- child.on('close', (code) => {
108
- if (stderr) log('Bouncer stderr: ' + stderr);
109
-
110
- if (code === 0 && stdout.trim()) {
111
- try {
112
- const result = JSON.parse(stdout.trim());
113
- const d = result.decision || result.hookSpecificOutput?.permissionDecision || 'unknown';
114
- const r = result.reason || result.hookSpecificOutput?.permissionDecisionReason || '';
115
- log(d + ': ' + operation + ' - ' + r);
116
- console.log(stdout.trim());
117
- } catch {
118
- log('allow (parse error): ' + operation);
119
- output('allow', 'Bouncer response parse error, allowing');
120
- }
121
- } else {
122
- log('allow (bouncer error): ' + operation);
123
- output('allow', 'Bouncer error, allowing');
124
- }
125
- });
126
-
127
- child.on('error', (err) => {
128
- log('allow (spawn error): ' + operation + ' - ' + err.message);
129
- output('allow', 'Bouncer spawn error, allowing');
130
- });
131
-
132
- } catch (err) {
133
- log('allow (exception): ' + operation + ' - ' + err.message);
134
- output('allow', 'Bouncer exception, allowing');
135
- }
136
- } else {
137
- // Fallback: critical threat pattern matching only
138
- const criticalPatterns = [
139
- { pattern: /rm\s+-rf\s+(\/|~)(\$|\s)/, reason: 'Critical threat: recursive delete of root or home' },
140
- { pattern: /:\(\)\{.*\}|:\(\)\{.*:\|:/, reason: 'Critical threat: fork bomb detected' },
141
- { pattern: /dd\s+if=\/dev\/zero\s+of=\/dev\/sd/, reason: 'Critical threat: disk overwrite' },
142
- { pattern: /mkfs\s+\/dev\/sd/, reason: 'Critical threat: filesystem format' },
143
- { pattern: />\s*\/dev\/sd/, reason: 'Critical threat: direct disk write' },
144
- ];
145
-
146
- for (const { pattern, reason } of criticalPatterns) {
147
- if (pattern.test(operation)) {
148
- log('DENIED: ' + operation + ' - ' + reason);
149
- output('deny', reason);
150
- process.exit(0);
151
- }
152
- }
153
-
154
- log('allow (fallback): ' + operation);
155
- output('allow', 'Basic pattern check passed');
156
- }
157
- " "$INPUT" "$BOUNCER_CLI" "$BOUNCER_TIMEOUT" "$BOUNCER_LOG" 2>> "$BOUNCER_LOG")
158
-
159
- echo "$RESULT"