@link-assistant/hive-mind 1.53.1 → 1.54.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.54.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ee156ba: Disable Claude Code built-in tools and MCP servers that have no value in autonomous headless runs. A new `--useless-tools-disabled` flag (default: `true`, use `--no-useless-tools-disabled` to opt out) adds `AskUserQuestion`, `CronCreate/Delete/List`, `EnterPlanMode/ExitPlanMode`, `EnterWorktree/ExitWorktree`, `Monitor`, `NotebookEdit`, `PushNotification`, `RemoteTrigger`, `ScheduleWakeup` and the three `claude.ai` OAuth MCP connectors (Gmail, Google Drive, Google Calendar) to `--disallowedTools` / `--strict-mcp-config` on each `solve` run. The Docker images (`Dockerfile`, `coolify/Dockerfile`) also bake the same `disallowedTools` list into the baseline `~/.claude/settings.json` so interactive `claude` sessions inside the image don't surface them either (issue #1627).
8
+
3
9
  ## 1.53.1
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.53.1",
3
+ "version": "1.54.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -20,6 +20,7 @@ import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments
20
20
  import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
21
21
  import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
22
22
  import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
23
+ import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
23
24
  export { availableModels }; // Re-export for backward compatibility
24
25
  const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
25
26
  if (!sessionId || !tempDir) return;
@@ -774,14 +775,9 @@ export const executeClaudeCommand = async params => {
774
775
  await log(`🔄 Resuming from session: ${argv.resume}`);
775
776
  claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
776
777
  }
777
- let mcpConfigPath = null;
778
- if (argv.playwrightMcp === false) {
779
- mcpConfigPath = await buildMcpConfigWithoutPlaywright(log);
780
- if (mcpConfigPath) {
781
- claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
782
- await log('🎭 Playwright MCP physically disabled for this session via --strict-mcp-config', { verbose: true });
783
- }
784
- }
778
+ const { mcpConfigPath, disallowedToolsList } = await resolveClaudeSessionToolFlags({ argv, log, fallbackBuildMcpConfigWithoutPlaywright: buildMcpConfigWithoutPlaywright });
779
+ if (mcpConfigPath) claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
780
+ if (disallowedToolsList.length) claudeArgs += ` --disallowedTools ${disallowedToolsList.join(' ')}`;
785
781
  claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
786
782
  const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
787
783
  await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
@@ -806,11 +802,12 @@ export const executeClaudeCommand = async params => {
806
802
  }
807
803
  const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
808
804
  const mcpDisableArgs = mcpConfigPath ? ['--strict-mcp-config', '--mcp-config', mcpConfigPath] : [];
805
+ const disallowedToolsArgs = disallowedToolsList.length ? ['--disallowedTools', ...disallowedToolsList] : [];
809
806
  if (argv.resume) {
810
807
  const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
811
- execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
808
+ execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
812
809
  } else {
813
- execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} --append-system-prompt "${simpleEscapedSystem}"`;
810
+ execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
814
811
  }
815
812
  await log(`${formatAligned('📋', 'Command details:', '')}`);
816
813
  await log(formatAligned('📂', 'Working directory:', tempDir, 2));
@@ -394,6 +394,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
394
394
  description: 'Automatically remove .playwright-mcp/ folder before checking for uncommitted changes. This prevents browser automation artifacts from triggering auto-restart. Use --no-playwright-mcp-auto-cleanup to keep the folder for debugging.',
395
395
  default: true,
396
396
  },
397
+ 'useless-tools-disabled': {
398
+ type: 'boolean',
399
+ description: 'Disable Claude Code built-in tools and MCP servers that have no value (and may be harmful) in autonomous headless runs: AskUserQuestion, CronCreate/Delete/List, EnterPlanMode/ExitPlanMode, EnterWorktree/ExitWorktree, Monitor, NotebookEdit, PushNotification, RemoteTrigger, ScheduleWakeup, and the claude.ai Gmail/Drive/Calendar OAuth connectors. Default: true. Use --no-useless-tools-disabled to keep them enabled. Supported for --tool claude (issue #1627).',
400
+ default: true,
401
+ },
397
402
  'auto-gh-configuration-repair': {
398
403
  type: 'boolean',
399
404
  description: 'Automatically repair git configuration using gh-setup-git-identity --repair when git identity is not configured. Requires gh-setup-git-identity to be installed.',
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // Useless Claude Code tools and MCP servers for autonomous headless workflows.
3
+ //
4
+ // Hive-mind runs `claude` inside Docker with `--print --dangerously-skip-permissions`
5
+ // and no human operator. Several built-in Claude Code tools and three `claude.ai`
6
+ // OAuth MCP connectors are active by default but either:
7
+ // - wait for a human reaction that will never come (`AskUserQuestion`,
8
+ // `EnterPlanMode`);
9
+ // - have side effects that outlive the session (`CronCreate`,
10
+ // `EnterWorktree`);
11
+ // - can never complete authentication without an interactive browser
12
+ // (`claude.ai Gmail`, `claude.ai Google Drive`, `claude.ai Google Calendar`).
13
+ //
14
+ // This module centralises the block-list and provides helpers that both the
15
+ // Docker image baseline and the `solve` runtime use to disable them.
16
+ //
17
+ // Related issue: https://github.com/link-assistant/hive-mind/issues/1627
18
+
19
+ if (typeof globalThis.use === 'undefined') {
20
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
21
+ }
22
+ const fs = (await use('fs')).promises;
23
+ const os = await use('os');
24
+ const path = (await use('path')).default;
25
+
26
+ /**
27
+ * Built-in Claude Code tools that have no value (and may be harmful) in
28
+ * autonomous headless hive-mind runs. Every entry is a tool name as it
29
+ * appears in the stream-json `tools` array emitted by `claude --verbose`.
30
+ */
31
+ export const USELESS_CLAUDE_BUILTIN_TOOLS = Object.freeze(['AskUserQuestion', 'CronCreate', 'CronDelete', 'CronList', 'EnterPlanMode', 'EnterWorktree', 'ExitPlanMode', 'ExitWorktree', 'Monitor', 'NotebookEdit', 'PushNotification', 'RemoteTrigger', 'ScheduleWakeup']);
32
+
33
+ /**
34
+ * Name prefixes of MCP servers that are always unusable in headless Docker
35
+ * runs because they require interactive OAuth that cannot complete without
36
+ * a browser. Match is case-insensitive on the full MCP server name.
37
+ */
38
+ export const USELESS_MCP_SERVER_NAME_PREFIXES = Object.freeze(['claude.ai gmail', 'claude.ai google drive', 'claude.ai google calendar']);
39
+
40
+ /**
41
+ * MCP tool-name prefixes derived from {@link USELESS_MCP_SERVER_NAME_PREFIXES}.
42
+ * Claude Code exposes MCP tools as `mcp__<server-name-slug>__<tool-name>`,
43
+ * replacing non-alphanumerics in the server name with `_`. Passing these
44
+ * entries to `--disallowedTools` is a belt-and-braces measure that complements
45
+ * filtering the MCP server itself.
46
+ */
47
+ export const USELESS_MCP_TOOL_NAME_PREFIXES = Object.freeze(['mcp__claude_ai_Gmail', 'mcp__claude_ai_Google_Drive', 'mcp__claude_ai_Google_Calendar']);
48
+
49
+ /**
50
+ * Tool identifiers accepted by `claude --disallowedTools ...`. This is a
51
+ * flat list combining the built-in tools with the wildcard forms of the
52
+ * useless MCP tool-name prefixes.
53
+ */
54
+ export const buildDisallowedToolsList = () => [...USELESS_CLAUDE_BUILTIN_TOOLS, ...USELESS_MCP_TOOL_NAME_PREFIXES.map(prefix => `${prefix}__*`)];
55
+
56
+ /**
57
+ * Returns true if `name` matches one of the useless MCP server prefixes.
58
+ */
59
+ export const isUselessMcpServerName = name => {
60
+ if (!name || typeof name !== 'string') return false;
61
+ const lower = name.toLowerCase();
62
+ return USELESS_MCP_SERVER_NAME_PREFIXES.some(prefix => lower.startsWith(prefix));
63
+ };
64
+
65
+ /**
66
+ * Returns the set of MCP server entries from an object (typically
67
+ * `~/.claude.json` `mcpServers` block) with useless entries removed.
68
+ */
69
+ export const filterMcpServersObject = (mcpServers = {}) => {
70
+ const filtered = {};
71
+ for (const [name, config] of Object.entries(mcpServers || {})) {
72
+ if (isUselessMcpServerName(name)) continue;
73
+ filtered[name] = config;
74
+ }
75
+ return filtered;
76
+ };
77
+
78
+ /**
79
+ * Build a temporary MCP config JSON file that filters out both the three
80
+ * `claude.ai` OAuth connectors and (optionally) Playwright.
81
+ *
82
+ * Designed to be used with `--strict-mcp-config --mcp-config <file>` so the
83
+ * excluded servers are not even advertised to the model for this run.
84
+ *
85
+ * @param {Object} [options]
86
+ * @param {boolean} [options.excludePlaywright] - Also exclude Playwright.
87
+ * @param {Function} [options.log] - Async logger with (msg, opts) signature.
88
+ * @returns {Promise<string|null>} absolute path to the temp config, or null
89
+ * if the home `.claude.json` file cannot be read (fatal errors are
90
+ * caught — callers should treat `null` as "skip --strict-mcp-config").
91
+ */
92
+ export const buildFilteredMcpConfig = async ({ excludePlaywright = false, log } = {}) => {
93
+ try {
94
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
95
+ const claudeJson = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
96
+ const mcpServers = claudeJson.mcpServers || {};
97
+ const filtered = {};
98
+ for (const [name, config] of Object.entries(mcpServers)) {
99
+ if (isUselessMcpServerName(name)) continue;
100
+ if (excludePlaywright && name.toLowerCase().includes('playwright')) continue;
101
+ filtered[name] = config;
102
+ }
103
+ const suffix = excludePlaywright ? 'no-playwright-no-useless' : 'no-useless';
104
+ const tempConfigPath = path.join(os.tmpdir(), `claude-mcp-${suffix}-${Date.now()}-${process.pid}.json`);
105
+ await fs.writeFile(tempConfigPath, JSON.stringify({ mcpServers: filtered }, null, 2));
106
+ if (log) {
107
+ const excluded = [...USELESS_MCP_SERVER_NAME_PREFIXES.map(p => `'${p}*'`), ...(excludePlaywright ? ["'playwright*'"] : [])].join(', ');
108
+ await log(`🧰 Created filtered MCP config (excluding ${excluded}): ${tempConfigPath}`, { verbose: true });
109
+ }
110
+ return tempConfigPath;
111
+ } catch (err) {
112
+ if (log) await log(`⚠️ Could not build filtered useless-MCP config: ${err.message}`, { verbose: true });
113
+ return null;
114
+ }
115
+ };
116
+
117
+ /**
118
+ * Resolve the per-session Claude CLI args for the useless-tools flag and the
119
+ * playwright-mcp flag in a single step. Returns an object with:
120
+ * - mcpConfigPath (string|null): temp file path for `--strict-mcp-config --mcp-config`
121
+ * - disallowedToolsList (string[]): values for `--disallowedTools`
122
+ *
123
+ * Callers are expected to append the returned values to the Claude command.
124
+ * Extracted from claude.lib.mjs to keep that file under the 1500-line cap.
125
+ */
126
+ export const resolveClaudeSessionToolFlags = async ({ argv, log, fallbackBuildMcpConfigWithoutPlaywright } = {}) => {
127
+ const uselessToolsDisabled = argv?.uselessToolsDisabled !== false;
128
+ const excludePlaywright = argv?.playwrightMcp === false;
129
+ let mcpConfigPath = null;
130
+ if (uselessToolsDisabled || excludePlaywright) {
131
+ mcpConfigPath = await buildFilteredMcpConfig({ excludePlaywright, log });
132
+ if (!mcpConfigPath && excludePlaywright && fallbackBuildMcpConfigWithoutPlaywright) {
133
+ mcpConfigPath = await fallbackBuildMcpConfigWithoutPlaywright(log);
134
+ }
135
+ if (mcpConfigPath && log) {
136
+ if (excludePlaywright) await log('🎭 Playwright MCP physically disabled for this session via --strict-mcp-config', { verbose: true });
137
+ if (uselessToolsDisabled) await log('🧰 Useless MCP servers (claude.ai Gmail/Drive/Calendar) disabled for this session via --strict-mcp-config (issue #1627)', { verbose: true });
138
+ }
139
+ }
140
+ const disallowedToolsList = uselessToolsDisabled ? buildDisallowedToolsList() : [];
141
+ if (uselessToolsDisabled && log) await log(`🧰 Disallowed ${disallowedToolsList.length} useless Claude Code tool(s) for this session (issue #1627)`, { verbose: true });
142
+ return { mcpConfigPath, disallowedToolsList };
143
+ };
144
+
145
+ /**
146
+ * Persist `disallowedTools` in `~/.claude/settings.json` so even interactive
147
+ * `claude` sessions launched outside of `solve` don't surface the useless
148
+ * tools. Existing entries in the settings file are preserved (shallow
149
+ * merge) and any existing `disallowedTools` list has the useless tools
150
+ * added to it without duplicates. Returns the set of tools that were
151
+ * newly added.
152
+ */
153
+ export const ensureDisallowedToolsInSettings = async ({ settingsPath, log } = {}) => {
154
+ const resolvedPath = settingsPath || path.join(os.homedir(), '.claude', 'settings.json');
155
+ const toBlock = buildDisallowedToolsList();
156
+ let settings = {};
157
+ try {
158
+ const content = await fs.readFile(resolvedPath, 'utf-8');
159
+ settings = JSON.parse(content);
160
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) settings = {};
161
+ } catch (err) {
162
+ if (err.code !== 'ENOENT' && log) {
163
+ await log(`⚠️ Could not read ${resolvedPath}: ${err.message}`, { verbose: true });
164
+ }
165
+ settings = {};
166
+ }
167
+ const existing = Array.isArray(settings.disallowedTools) ? settings.disallowedTools : [];
168
+ const merged = [...existing];
169
+ const added = [];
170
+ for (const tool of toBlock) {
171
+ if (!merged.includes(tool)) {
172
+ merged.push(tool);
173
+ added.push(tool);
174
+ }
175
+ }
176
+ settings.disallowedTools = merged;
177
+ try {
178
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
179
+ await fs.writeFile(resolvedPath, JSON.stringify(settings, null, 2));
180
+ if (log && added.length) {
181
+ await log(`🧰 Added ${added.length} useless tool(s) to ${resolvedPath} disallowedTools`, { verbose: true });
182
+ }
183
+ } catch (err) {
184
+ if (log) await log(`⚠️ Could not write ${resolvedPath}: ${err.message}`, { verbose: true });
185
+ }
186
+ return { added, total: merged.length, path: resolvedPath };
187
+ };