#!/usr/bin/env node // Claude Code PreToolUse hook for the Bash tool (see scripts/mcp-tmp-guard): // refuses `claude mcp add ...` when the MCP server would run from /tmp, which // is wiped every time the Space restarts, and tells Claude where to put it. 'use strict'; const path = require('path'); const { tempPathIn } = require(path.join(__dirname, 'temp-paths.js')); const MCP_ADD = /\bmcp\s+add(?:-json)?\b/; const CD = /^\s*(?:builtin\s+)?(?:cd|pushd)\s+["']?([^\s"']+)/; const TEMP_DIR = /^(?:\/var)?\/tmp(?:\/|$)|^\/dev\/shm(?:\/|$)/; const PWD = /\$\(pwd\)|`pwd`|\$\{?PWD\b/; // Only the `mcp add` part of a command line counts (`mv /tmp/x ~/mcp-servers/x // && claude mcp add ...` is the fix, not the problem), plus a `cd` into a temp // folder that the add part then refers to through $(pwd) / $PWD. function tempPathAdded(command) { let cwd = ''; for (const part of command.split(/&&|\|\||[;|\n]/)) { const cd = CD.exec(part); if (cd) cwd = TEMP_DIR.test(cd[1]) ? cd[1] : ''; if (!MCP_ADD.test(part)) continue; const found = tempPathIn(part) || (cwd && PWD.test(part) ? cwd : ''); if (found) return found; } return ''; } // `claude mcp add ... -- node ~/mcp-servers/x/...` runs straight from the // Storage Bucket, whose node_modules / venv links are gone after a restart. const BUCKET_SERVER = /(?:~|\$HOME|\$\{HOME\}|\/data\/home|\/home\/node)\/mcp-servers\/([A-Za-z0-9._-]+)/; function bucketServerAdded(command) { for (const part of command.split(/&&|\|\||[;|\n]/)) { if (!MCP_ADD.test(part) || /agentweb-mcp-run/.test(part)) continue; const m = BUCKET_SERVER.exec(part); if (m) return m[1]; } return ''; } function decide(event, home) { if (!event || event.tool_name !== 'Bash') return null; const command = event.tool_input && typeof event.tool_input.command === 'string' ? event.tool_input.command : ''; if (!MCP_ADD.test(command)) return null; const bucket = bucketServerAdded(command); if (bucket) { return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: `Agent Web stopped this command: it registers an MCP server that runs straight from ~/mcp-servers/${bucket} on the Storage Bucket, ` + 'which loses symlinks and execute bits (node_modules/.bin, venv/bin/python) at every restart, so the server would break. ' + `Use: agentweb-install mcp --dir ~/mcp-servers/${bucket} [--run ""] [--env K=V] ` + '(it builds the runnable copy on local disk at every start and registers `agentweb-mcp-run `). See `agentweb-install help`.', }, }; } const found = tempPathAdded(command); if (!found) return null; const target = `${home ? String(home).replace(/\/+$/, '') : '~'}/mcp-servers/`; return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: `Agent Web stopped this command: it registers an MCP server that uses ${found}. ` + '/tmp is wiped every time this Space restarts, so the server would stop working. ' + `Install it with \`agentweb-install mcp --git \` (or --dir ) instead: the source goes to ${target} ` + 'and the runnable copy is rebuilt on local disk at every start. See `agentweb-install help`.', }, }; } if (require.main === module) { let raw = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { raw += chunk; }); process.stdin.on('end', () => { let event; try { event = JSON.parse(raw); } catch { return; } const answer = decide(event, process.env.HOME); if (answer) process.stdout.write(JSON.stringify(answer)); }); } module.exports = { decide, tempPathAdded };