@claude-flow/cli 3.38.13 → 3.38.15

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.
@@ -1 +1 @@
1
- 3.38.13
1
+ 3.38.15
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.38.13",
3
+ "version": "3.38.15",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "85fe05c757421c52137c0bc8545a0896bab6b4714538c2a11d1d0835bfcc8c1c",
6
6
  "hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
@@ -8,6 +8,6 @@
8
8
  "statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
9
9
  }
10
10
  },
11
- "signature": "V8Wxea6WL3Y6pa8RgPLLR+P2AuCu0HPq9kJ3UCtyGi12Zgk1X4NICNYxhuwftErwnyS2+zpjZ/XqmF03hsR9Cg==",
11
+ "signature": "BPJ/dA1o5KjvB5Ic9PGq/9BHvQnXADaTmGXZW3UUCfDKDKmDFKMzQZowG3HF54zTek1SapxJb3irPiIWMPAFDA==",
12
12
  "algorithm": "ed25519"
13
13
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 5,
4
- "generatedAt": "2026-08-21T14:18:22.552Z",
5
- "gitSha": "0f3c4510",
4
+ "generatedAt": "2026-08-21T14:40:50.895Z",
5
+ "gitSha": "87bacd55",
6
6
  "catalog": {
7
7
  "agents": 165,
8
8
  "tools": 397,
@@ -8,10 +8,11 @@
8
8
  import { output } from '../output.js';
9
9
  import { select, confirm, input } from '../prompt.js';
10
10
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
11
- import { spawn as childSpawn, execSync } from 'child_process';
11
+ import { spawn as childSpawn } from 'child_process';
12
12
  import { mkdir, writeFile } from 'fs/promises';
13
13
  import { existsSync } from 'fs';
14
14
  import { join } from 'path';
15
+ import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
15
16
  // Hive topologies
16
17
  const TOPOLOGIES = [
17
18
  { value: 'hierarchical', label: 'Hierarchical', hint: 'Queen-led with worker agents' },
@@ -185,20 +186,18 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
185
186
  await writeFile(promptFile, hiveMindPrompt, 'utf8');
186
187
  output.writeln();
187
188
  output.printSuccess(`Hive Mind prompt saved to: ${promptFile}`);
188
- // Check if claude command exists
189
- let claudeAvailable = false;
190
- try {
191
- execSync('which claude', { stdio: 'ignore' });
192
- claudeAvailable = true;
193
- }
194
- catch {
189
+ // Resolve a directly spawnable command. On Windows, npm exposes shell
190
+ // shims that Node cannot launch with shell:false, so the resolver follows
191
+ // the shim to Claude Code's executable or JavaScript entry point.
192
+ const claudeLaunch = resolveClaudeLaunchCommand();
193
+ if (!claudeLaunch) {
195
194
  output.writeln();
196
195
  output.printWarning('Claude Code CLI not found in PATH');
197
196
  output.writeln(output.dim('Install it with: npm install -g @anthropic-ai/claude-code'));
198
197
  output.writeln(output.dim('Falling back to displaying instructions...'));
199
198
  }
200
199
  const dryRun = flags.dryRun || flags['dry-run'];
201
- if (claudeAvailable && !dryRun) {
200
+ if (claudeLaunch && !dryRun) {
202
201
  // Build arguments - flags first, then prompt
203
202
  const claudeArgs = [];
204
203
  // #1748 Issue 2 — pass --mcp-config so the spawned worker actually has
@@ -273,7 +272,10 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
273
272
  output.printInfo('Launching Claude Code...');
274
273
  output.writeln(output.dim('Press Ctrl+C to pause the session'));
275
274
  // Spawn claude with properly ordered arguments
276
- const claudeProcess = childSpawn('claude', claudeArgs, {
275
+ const claudeProcess = childSpawn(claudeLaunch.command, [
276
+ ...claudeLaunch.argsPrefix,
277
+ ...claudeArgs,
278
+ ], {
277
279
  stdio: 'inherit',
278
280
  shell: false,
279
281
  });
@@ -613,6 +613,18 @@ const initClaudeAction = async (ctx) => {
613
613
  }
614
614
  output.printBox(summary.join('\n'), 'Summary');
615
615
  output.writeln();
616
+ // Security: surface anything settings-risk-scanner.ts flagged in a
617
+ // pre-existing settings.json this init carried forward unexamined.
618
+ if (result.warnings && result.warnings.length > 0) {
619
+ output.printWarning('Settings review recommended:');
620
+ for (const warning of result.warnings.slice(0, 5)) {
621
+ output.printInfo(` • ${warning}`);
622
+ }
623
+ if (result.warnings.length > 5) {
624
+ output.printInfo(` ... and ${result.warnings.length - 5} more`);
625
+ }
626
+ output.writeln();
627
+ }
616
628
  // Show what was created
617
629
  if (options.components.claudeMd || options.components.settings || options.components.skills || options.components.commands || options.components.agents) {
618
630
  output.printBox([
@@ -1022,6 +1034,18 @@ const wizardCommand = {
1022
1034
  return { success: false, exitCode: 1 };
1023
1035
  }
1024
1036
  spinner.succeed('Setup complete!');
1037
+ // Security: surface anything settings-risk-scanner.ts flagged in a
1038
+ // pre-existing settings.json this wizard run carried forward unexamined.
1039
+ if (result.warnings && result.warnings.length > 0) {
1040
+ output.writeln();
1041
+ output.printWarning('Settings review recommended:');
1042
+ for (const warning of result.warnings.slice(0, 5)) {
1043
+ output.printInfo(` • ${warning}`);
1044
+ }
1045
+ if (result.warnings.length > 5) {
1046
+ output.printInfo(` ... and ${result.warnings.length - 5} more`);
1047
+ }
1048
+ }
1025
1049
  // Initialize embeddings if enabled
1026
1050
  let embeddingsInitialized = false;
1027
1051
  if (enableEmbeddings) {
@@ -1170,6 +1194,15 @@ const skillsCommand = {
1170
1194
  const result = await executeInit(options);
1171
1195
  if (result.success) {
1172
1196
  spinner.succeed(`Installed ${result.summary.skillsCount} skills`);
1197
+ if (result.warnings && result.warnings.length > 0) {
1198
+ output.printWarning('Settings review recommended:');
1199
+ for (const warning of result.warnings.slice(0, 5)) {
1200
+ output.printInfo(` • ${warning}`);
1201
+ }
1202
+ if (result.warnings.length > 5) {
1203
+ output.printInfo(` ... and ${result.warnings.length - 5} more`);
1204
+ }
1205
+ }
1173
1206
  }
1174
1207
  else {
1175
1208
  spinner.fail('Failed to install skills');
@@ -1232,6 +1265,15 @@ const hooksCommand = {
1232
1265
  const result = await executeInit(options);
1233
1266
  if (result.success) {
1234
1267
  spinner.succeed(`Created settings.json with ${result.summary.hooksEnabled} hooks enabled`);
1268
+ if (result.warnings && result.warnings.length > 0) {
1269
+ output.printWarning('Settings review recommended:');
1270
+ for (const warning of result.warnings.slice(0, 5)) {
1271
+ output.printInfo(` • ${warning}`);
1272
+ }
1273
+ if (result.warnings.length > 5) {
1274
+ output.printInfo(` ... and ${result.warnings.length - 5} more`);
1275
+ }
1276
+ }
1235
1277
  }
1236
1278
  else {
1237
1279
  spinner.fail('Failed to create hooks configuration');
@@ -1342,6 +1384,18 @@ const upgradeCommand = {
1342
1384
  output.printBox(result.settingsUpdated.map(s => `+ ${s}`).join('\n'), 'Settings Updated');
1343
1385
  output.writeln();
1344
1386
  }
1387
+ // Security: surface anything settings-risk-scanner.ts flagged in the
1388
+ // pre-existing settings.json this upgrade carried forward unexamined.
1389
+ if (result.warnings && result.warnings.length > 0) {
1390
+ output.printWarning('Settings review recommended:');
1391
+ for (const warning of result.warnings.slice(0, 5)) {
1392
+ output.printInfo(` • ${warning}`);
1393
+ }
1394
+ if (result.warnings.length > 5) {
1395
+ output.printInfo(` ... and ${result.warnings.length - 5} more`);
1396
+ }
1397
+ output.writeln();
1398
+ }
1345
1399
  output.printSuccess('Your statusline helper has been updated to the latest version');
1346
1400
  output.printInfo('Existing metrics and learning data were preserved');
1347
1401
  // Show settings summary
@@ -8,7 +8,7 @@
8
8
  import { output } from '../output.js';
9
9
  import { confirm } from '../prompt.js';
10
10
  import { installParentDeathWatchdog } from '../runtime/parent-death-watchdog.js';
11
- import { getServerManager, getMCPServerStatus, } from '../mcp-server.js';
11
+ import { getServerManager, getMCPServerStatus, filterAdvertisedMcpTools, parseMcpToolSelection, } from '../mcp-server.js';
12
12
  import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js';
13
13
  // MCP tools categories
14
14
  const TOOL_CATEGORIES = [
@@ -406,7 +406,8 @@ const toolsCommand = {
406
406
  // Use local tool registry
407
407
  let tools;
408
408
  // Get tools from local registry
409
- const registeredTools = listMCPTools(category);
409
+ const selection = parseMcpToolSelection(process.env.CLAUDE_FLOW_MCP_TOOLS);
410
+ const registeredTools = filterAdvertisedMcpTools(listMCPTools(category), selection);
410
411
  if (registeredTools.length > 0) {
411
412
  tools = registeredTools.map(tool => ({
412
413
  name: tool.name,
@@ -417,7 +418,7 @@ const toolsCommand = {
417
418
  }
418
419
  else {
419
420
  // Fallback to static tool list
420
- tools = [
421
+ tools = filterAdvertisedMcpTools([
421
422
  // Agent tools
422
423
  { name: 'agent_spawn', category: 'agent', description: 'Spawn a new agent', enabled: true },
423
424
  { name: 'agent_list', category: 'agent', description: 'List all agents', enabled: true },
@@ -449,7 +450,7 @@ const toolsCommand = {
449
450
  { name: 'system_info', category: 'system', description: 'System information', enabled: true },
450
451
  { name: 'system_health', category: 'system', description: 'Health status', enabled: true },
451
452
  { name: 'system_metrics', category: 'system', description: 'Server metrics', enabled: true },
452
- ].filter(t => !category || t.category === category);
453
+ ].filter(t => !category || t.category === category), selection);
453
454
  }
454
455
  if (ctx.flags.format === 'json') {
455
456
  output.printJson(tools);
@@ -22,13 +22,23 @@ export interface UpgradeResult {
22
22
  addedCommands?: string[];
23
23
  /** Added by --settings flag */
24
24
  settingsUpdated?: string[];
25
+ /**
26
+ * Advisory-only findings from settings-risk-scanner.ts: dangerous-looking
27
+ * hook commands or Bash allow-rules found in the *pre-existing*
28
+ * settings.json this upgrade carried forward unexamined. Never blocks
29
+ * the upgrade — surfaced so a user can review before trusting it.
30
+ */
31
+ warnings?: string[];
25
32
  }
26
33
  /**
27
34
  * Merge new settings into existing settings.json
28
35
  * Preserves user customizations while adding new features like Agent Teams
29
36
  * Uses platform-specific commands for Mac, Linux, and Windows
30
37
  */
31
- export declare function mergeSettingsForUpgrade(existing: Record<string, unknown>): Record<string, unknown>;
38
+ export declare function mergeSettingsForUpgrade(existing: Record<string, unknown>): {
39
+ merged: Record<string, unknown>;
40
+ warnings: string[];
41
+ };
32
42
  /**
33
43
  * Execute upgrade - updates helpers and creates missing metrics without losing data
34
44
  * This is safe for existing users who want the latest statusline fixes
@@ -18,6 +18,7 @@ import { generatePreCommitHook, generatePostCommitHook, generateSessionManager,
18
18
  import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js';
19
19
  import { generateClaudeMd } from './claudemd-generator.js';
20
20
  import { recordMemoryPackagePath } from './memory-package-resolver.js';
21
+ import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
21
22
  /**
22
23
  * Skills to copy based on configuration
23
24
  */
@@ -277,6 +278,10 @@ export async function executeInit(options) {
277
278
  * Uses platform-specific commands for Mac, Linux, and Windows
278
279
  */
279
280
  export function mergeSettingsForUpgrade(existing) {
281
+ // Scan the pre-existing hooks/permissions BEFORE they get spread into
282
+ // `merged` below — this is the untrusted, disk-sourced content a
283
+ // malicious settings.json would use to smuggle a hook payload through.
284
+ const warnings = formatRiskFindingsAsWarnings(scanSettingsForRisk({ hooks: existing.hooks, permissions: existing.permissions }));
280
285
  const merged = { ...existing };
281
286
  const platform = detectPlatform();
282
287
  const isWindows = platform.os === 'windows';
@@ -459,7 +464,7 @@ export function mergeSettingsForUpgrade(existing) {
459
464
  agentScopes: existingMemory.agentScopes ?? { enabled: true },
460
465
  },
461
466
  };
462
- return merged;
467
+ return { merged, warnings };
463
468
  }
464
469
  /**
465
470
  * Execute upgrade - updates helpers and creates missing metrics without losing data
@@ -643,7 +648,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
643
648
  if (fs.existsSync(settingsPath)) {
644
649
  try {
645
650
  const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
646
- const mergedSettings = mergeSettingsForUpgrade(existingSettings);
651
+ const { merged: mergedSettings, warnings: settingsRiskWarnings } = mergeSettingsForUpgrade(existingSettings);
647
652
  fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf-8');
648
653
  result.updated.push('.claude/settings.json');
649
654
  result.settingsUpdated = [
@@ -655,6 +660,9 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
655
660
  'claudeFlow.agentTeams',
656
661
  'claudeFlow.memory (learningBridge, memoryGraph, agentScopes)',
657
662
  ];
663
+ if (settingsRiskWarnings.length > 0) {
664
+ result.warnings = [...(result.warnings ?? []), ...settingsRiskWarnings];
665
+ }
658
666
  }
659
667
  catch (settingsError) {
660
668
  result.errors.push(`Settings merge failed: ${settingsError instanceof Error ? settingsError.message : String(settingsError)}`);
@@ -797,6 +805,13 @@ async function writeSettings(targetDir, options, result) {
797
805
  // Merge hooks/env/permissions into existing settings instead of skipping
798
806
  try {
799
807
  const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
808
+ // Scan the pre-existing hooks/permissions BEFORE any merge decision
809
+ // below — same untrusted, disk-sourced content mergeSettingsForUpgrade()
810
+ // scans; see settings-risk-scanner.ts for why.
811
+ const riskWarnings = formatRiskFindingsAsWarnings(scanSettingsForRisk({ hooks: existing.hooks, permissions: existing.permissions }));
812
+ if (riskWarnings.length > 0) {
813
+ result.warnings = [...(result.warnings ?? []), ...riskWarnings];
814
+ }
800
815
  let merged = false;
801
816
  // Merge hooks (the critical missing piece — #1484)
802
817
  if (generated.hooks && !existing.hooks) {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Static, non-blocking risk scanner for pre-existing .claude/settings.json
3
+ * content that `ruflo init`/`ruflo init --upgrade` carry forward unexamined.
4
+ *
5
+ * `mergeSettingsForUpgrade()` and `writeSettings()` both read a target
6
+ * project's *existing* settings.json off disk and spread its `hooks` /
7
+ * `permissions.allow` entries into the merged output verbatim (see
8
+ * executor.ts:352-353 and :901-912) — the same trust shape as the publicly
9
+ * disclosed CVE-2025-59536 class (a settings.json hook payload achieving
10
+ * command execution with no review step). Ruflo's own hook dispatch is a
11
+ * closed set of internal handlers (hook-handler.cjs), so this isn't
12
+ * remotely exploitable through Ruflo itself — but a malicious fork, PR, or
13
+ * compromised dependency that plants a bad settings.json before a user
14
+ * runs `ruflo init`/`--upgrade` in that directory would have its payload
15
+ * silently preserved and reported as a normal "merged"/"updated" result,
16
+ * with zero visibility.
17
+ *
18
+ * This scanner is advisory-only: it never mutates its input and never
19
+ * blocks a merge/write. Callers decide what to do with the findings
20
+ * (surfaced as a CLI warning today).
21
+ *
22
+ * KNOWN LIMITATION (found by an independent adversarial review the same
23
+ * night this was written — see docs/dream-cycle/dream-gist-2026-08-16.md
24
+ * §5): this is a best-effort, static, blocklist-style heuristic, NOT a
25
+ * security boundary. It catches copy-pasted proof-of-concept payload
26
+ * shapes; it does not and cannot catch every obfuscation a motivated
27
+ * adversary could construct (string-built commands, uncommon interpreters,
28
+ * novel exfil channels, etc). Treat findings as "worth a second look,"
29
+ * never as a guarantee of safety.
30
+ */
31
+ export interface SettingsRiskFinding {
32
+ location: string;
33
+ snippet: string;
34
+ reason: string;
35
+ }
36
+ /** Reasons a single hook `command` string looks risky (empty = clean). */
37
+ export declare function scanCommandStringForRisk(command: string): string[];
38
+ /** Reasons a single `permissions.allow` rule string looks risky (empty = clean). */
39
+ export declare function scanAllowRuleForRisk(rule: string): string[];
40
+ /**
41
+ * Scans a settings.json-shaped object's `hooks` and `permissions.allow`
42
+ * for content matching known-dangerous patterns.
43
+ */
44
+ export declare function scanSettingsForRisk(settings: Record<string, unknown>): SettingsRiskFinding[];
45
+ /** Formats findings as short, human-readable CLI warning lines. */
46
+ export declare function formatRiskFindingsAsWarnings(findings: SettingsRiskFinding[]): string[];
47
+ //# sourceMappingURL=settings-risk-scanner.d.ts.map
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Static, non-blocking risk scanner for pre-existing .claude/settings.json
3
+ * content that `ruflo init`/`ruflo init --upgrade` carry forward unexamined.
4
+ *
5
+ * `mergeSettingsForUpgrade()` and `writeSettings()` both read a target
6
+ * project's *existing* settings.json off disk and spread its `hooks` /
7
+ * `permissions.allow` entries into the merged output verbatim (see
8
+ * executor.ts:352-353 and :901-912) — the same trust shape as the publicly
9
+ * disclosed CVE-2025-59536 class (a settings.json hook payload achieving
10
+ * command execution with no review step). Ruflo's own hook dispatch is a
11
+ * closed set of internal handlers (hook-handler.cjs), so this isn't
12
+ * remotely exploitable through Ruflo itself — but a malicious fork, PR, or
13
+ * compromised dependency that plants a bad settings.json before a user
14
+ * runs `ruflo init`/`--upgrade` in that directory would have its payload
15
+ * silently preserved and reported as a normal "merged"/"updated" result,
16
+ * with zero visibility.
17
+ *
18
+ * This scanner is advisory-only: it never mutates its input and never
19
+ * blocks a merge/write. Callers decide what to do with the findings
20
+ * (surfaced as a CLI warning today).
21
+ *
22
+ * KNOWN LIMITATION (found by an independent adversarial review the same
23
+ * night this was written — see docs/dream-cycle/dream-gist-2026-08-16.md
24
+ * §5): this is a best-effort, static, blocklist-style heuristic, NOT a
25
+ * security boundary. It catches copy-pasted proof-of-concept payload
26
+ * shapes; it does not and cannot catch every obfuscation a motivated
27
+ * adversary could construct (string-built commands, uncommon interpreters,
28
+ * novel exfil channels, etc). Treat findings as "worth a second look,"
29
+ * never as a guarantee of safety.
30
+ */
31
+ const DANGEROUS_COMMAND_WORDS = [
32
+ 'rm', 'rmdir', 'del', 'format', 'mkfs', 'dd', 'chmod', 'chown',
33
+ 'kill', 'killall', 'pkill', 'reboot', 'shutdown', 'poweroff', 'halt',
34
+ 'sudo', 'eval',
35
+ ];
36
+ // Words that make a *hook command string* look risky when combined with a
37
+ // downloader (curl/wget) or a decode step (base64) anywhere in the same
38
+ // string — deliberately co-occurrence-based, not strict adjacency/piping,
39
+ // since `curl … -o f && sh f` and `curl … | tee x | bash` both reach the
40
+ // same outcome as the textbook `curl … | bash` and a strict pipe-adjacency
41
+ // regex misses both (found by adversarial review).
42
+ const SHELL_EXEC_WORDS = ['bash', 'sh', 'zsh', 'python', 'python3', 'perl', 'ruby', 'eval'];
43
+ const DOWNLOADER_WORDS = ['curl', 'wget'];
44
+ const STANDALONE_RISK_PATTERNS = [
45
+ /\bInvoke-Expression\b/i,
46
+ /\biex\s*\(/i,
47
+ /powershell(\.exe)?\s+.*-e(nc(odedcommand)?)?\s+\S/i,
48
+ /\/dev\/tcp\//i,
49
+ /\bnc\s+-e\b/i,
50
+ /base64\s+(-d|--decode)\b/i,
51
+ ];
52
+ // Words a Bash allow-rule should never blanket-preapprove without a
53
+ // specific argument scope, even though they aren't "dangerous" on their
54
+ // own the way `rm`/`chmod` are — pre-approving them removes the human
55
+ // review step for exactly the commands download-and-execute attacks use.
56
+ const RISKY_PREAPPROVE_WORDS = [...DANGEROUS_COMMAND_WORDS, ...SHELL_EXEC_WORDS, ...DOWNLOADER_WORDS];
57
+ function wordBoundaryMatches(text, word) {
58
+ return new RegExp(`\\b${word}\\b`, 'i').test(text);
59
+ }
60
+ /** Reasons a single hook `command` string looks risky (empty = clean). */
61
+ export function scanCommandStringForRisk(command) {
62
+ const reasons = [];
63
+ for (const pattern of STANDALONE_RISK_PATTERNS) {
64
+ if (pattern.test(command)) {
65
+ reasons.push('matches a known-dangerous execution pattern (encoded/remote command execution)');
66
+ break;
67
+ }
68
+ }
69
+ const hasDownloader = DOWNLOADER_WORDS.some((w) => wordBoundaryMatches(command, w));
70
+ const hasShellExec = SHELL_EXEC_WORDS.some((w) => wordBoundaryMatches(command, w));
71
+ if (hasDownloader && hasShellExec) {
72
+ reasons.push('combines a downloader (curl/wget) with a shell/interpreter invocation in the same command');
73
+ }
74
+ for (const word of DANGEROUS_COMMAND_WORDS) {
75
+ if (wordBoundaryMatches(command, word)) {
76
+ reasons.push(`invokes dangerous command "${word}"`);
77
+ }
78
+ }
79
+ return reasons;
80
+ }
81
+ /** Reasons a single `permissions.allow` rule string looks risky (empty = clean). */
82
+ export function scanAllowRuleForRisk(rule) {
83
+ const trimmed = rule.trim();
84
+ const reasons = [];
85
+ if (/^Bash\(\s*\*\s*(:\s*\*\s*)?\)$/i.test(trimmed)) {
86
+ reasons.push('grants unrestricted Bash execution (wildcard allow rule)');
87
+ }
88
+ for (const word of RISKY_PREAPPROVE_WORDS) {
89
+ if (new RegExp(`^Bash\\(\\s*${word}\\s*(:| |\\))`, 'i').test(trimmed)) {
90
+ reasons.push(`pre-allows "${word}" with no per-call confirmation`);
91
+ }
92
+ }
93
+ return reasons;
94
+ }
95
+ /**
96
+ * Strips ANSI/control characters from untrusted text before it's ever
97
+ * embedded in a CLI warning string. Without this, a malicious hook command
98
+ * could use escape sequences to spoof or corrupt the terminal output of
99
+ * the very warning meant to flag it (found during this candidate's own
100
+ * security review — not part of the CVE-2025-59536 class itself, but the
101
+ * same "untrusted settings.json content reaches the user's terminal"
102
+ * shape, so it gets the same treatment here rather than being left open).
103
+ */
104
+ function sanitizeForDisplay(text) {
105
+ // eslint-disable-next-line no-control-regex
106
+ return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
107
+ }
108
+ function buildSnippet(text) {
109
+ const clean = sanitizeForDisplay(text);
110
+ return clean.length > 120 ? `${clean.slice(0, 120)}…` : clean;
111
+ }
112
+ /**
113
+ * Scans a settings.json-shaped object's `hooks` and `permissions.allow`
114
+ * for content matching known-dangerous patterns.
115
+ */
116
+ export function scanSettingsForRisk(settings) {
117
+ const findings = [];
118
+ const hooks = settings.hooks;
119
+ if (hooks && typeof hooks === 'object') {
120
+ for (const [eventName, groups] of Object.entries(hooks)) {
121
+ if (!Array.isArray(groups))
122
+ continue;
123
+ groups.forEach((group, groupIdx) => {
124
+ const hookList = group?.hooks;
125
+ if (!Array.isArray(hookList))
126
+ return;
127
+ hookList.forEach((h, hookIdx) => {
128
+ if (typeof h?.command !== 'string')
129
+ return;
130
+ for (const reason of scanCommandStringForRisk(h.command)) {
131
+ findings.push({
132
+ location: `hooks.${eventName}[${groupIdx}].hooks[${hookIdx}]`,
133
+ snippet: buildSnippet(h.command),
134
+ reason,
135
+ });
136
+ }
137
+ });
138
+ });
139
+ }
140
+ }
141
+ const allow = settings.permissions?.allow;
142
+ if (Array.isArray(allow)) {
143
+ allow.forEach((rule, idx) => {
144
+ if (typeof rule !== 'string')
145
+ return;
146
+ for (const reason of scanAllowRuleForRisk(rule)) {
147
+ findings.push({
148
+ location: `permissions.allow[${idx}]`,
149
+ snippet: buildSnippet(rule),
150
+ reason,
151
+ });
152
+ }
153
+ });
154
+ }
155
+ return findings;
156
+ }
157
+ /** Formats findings as short, human-readable CLI warning lines. */
158
+ export function formatRiskFindingsAsWarnings(findings) {
159
+ return findings.map((f) => `${f.location}: ${f.reason} — "${f.snippet}"`);
160
+ }
161
+ //# sourceMappingURL=settings-risk-scanner.js.map
@@ -327,5 +327,12 @@ export interface InitResult {
327
327
  agentsCount: number;
328
328
  hooksEnabled: number;
329
329
  };
330
+ /**
331
+ * Advisory-only findings from settings-risk-scanner.ts: dangerous-looking
332
+ * hook commands or Bash allow-rules found in a *pre-existing*
333
+ * settings.json this init carried forward unexamined. Never blocks
334
+ * init — surfaced so a user can review before trusting it.
335
+ */
336
+ warnings?: string[];
330
337
  }
331
338
  //# sourceMappingURL=types.d.ts.map
@@ -392,7 +392,7 @@ const activeTrajectories = new Map();
392
392
  const MEMORY_DIR = '.claude-flow/memory';
393
393
  const MEMORY_FILE = 'store.json';
394
394
  function getMemoryPath() {
395
- return resolve(join(MEMORY_DIR, MEMORY_FILE));
395
+ return resolve(join(getProjectCwd(), MEMORY_DIR, MEMORY_FILE));
396
396
  }
397
397
  function loadMemoryStore() {
398
398
  try {
@@ -407,6 +407,80 @@ function loadMemoryStore() {
407
407
  }
408
408
  return { entries: {}, version: '3.0.0' };
409
409
  }
410
+ function isLearnedPatternEntry(entry) {
411
+ return entry.key.includes('pattern') ||
412
+ entry.metadata?.type === 'pattern' ||
413
+ entry.key.startsWith('learned-') ||
414
+ entry.namespace === 'patterns' ||
415
+ entry.metadata?.type === 'routing-decision';
416
+ }
417
+ function timestampInRange(value, start, end) {
418
+ if (typeof value !== 'string' && typeof value !== 'number')
419
+ return false;
420
+ const timestamp = typeof value === 'number' ? value : Date.parse(value);
421
+ return Number.isFinite(timestamp) && timestamp >= start && timestamp <= end;
422
+ }
423
+ function nonNegativeInteger(value) {
424
+ return typeof value === 'number' && Number.isFinite(value)
425
+ ? Math.max(0, Math.floor(value))
426
+ : 0;
427
+ }
428
+ function loadActiveSessionState() {
429
+ try {
430
+ const sessionPath = join(getProjectCwd(), '.claude-flow', 'sessions', 'current.json');
431
+ if (!existsSync(sessionPath))
432
+ return null;
433
+ const session = JSON.parse(readFileSync(sessionPath, 'utf-8'));
434
+ if (typeof session.id !== 'string' || session.id.trim().length === 0)
435
+ return null;
436
+ if (typeof session.startedAt !== 'string' || !Number.isFinite(Date.parse(session.startedAt)))
437
+ return null;
438
+ return session;
439
+ }
440
+ catch {
441
+ return null;
442
+ }
443
+ }
444
+ function loadSessionActivity(session, endedAt) {
445
+ const startedAt = Date.parse(session.startedAt);
446
+ let tasksCompleted = 0;
447
+ try {
448
+ const taskPath = join(getProjectCwd(), '.claude-flow', 'tasks', 'store.json');
449
+ if (existsSync(taskPath)) {
450
+ const store = JSON.parse(readFileSync(taskPath, 'utf-8'));
451
+ for (const task of Object.values(store.tasks ?? {})) {
452
+ if (task.status === 'completed' && timestampInRange(task.completedAt, startedAt, endedAt)) {
453
+ tasksCompleted++;
454
+ }
455
+ }
456
+ }
457
+ }
458
+ catch {
459
+ // Missing or malformed task state contributes no completed tasks.
460
+ }
461
+ const patternsLearned = Object.values(loadMemoryStore().entries)
462
+ .filter(entry => isLearnedPatternEntry(entry) && timestampInRange(entry.storedAt, startedAt, endedAt))
463
+ .length;
464
+ return {
465
+ tasksCompleted,
466
+ patternsLearned,
467
+ editsRecorded: nonNegativeInteger(session.metrics?.edits),
468
+ commandsRecorded: nonNegativeInteger(session.metrics?.commands),
469
+ errorsRecorded: nonNegativeInteger(session.metrics?.errors),
470
+ };
471
+ }
472
+ function buildSessionSummary(activity, duration) {
473
+ const durationMinutes = Math.max(0, Math.round(duration / 60000));
474
+ const count = (value, singular, plural = `${singular}s`) => `${value} ${value === 1 ? singular : plural}`;
475
+ return [
476
+ `${count(activity.tasksCompleted, 'task')} completed`,
477
+ `${count(activity.patternsLearned, 'pattern')} learned`,
478
+ `${count(activity.editsRecorded, 'edit')} recorded`,
479
+ `${count(activity.commandsRecorded, 'command')} recorded`,
480
+ `${count(activity.errorsRecorded, 'error')} recorded`,
481
+ `duration ${count(durationMinutes, 'minute')}`,
482
+ ].join('; ');
483
+ }
410
484
  /**
411
485
  * Get real intelligence statistics from memory store
412
486
  */
@@ -428,11 +502,7 @@ function getIntelligenceStatsFromMemory() {
428
502
  // patterns the metric is meant to count, so include them: any entry
429
503
  // whose namespace is `patterns`, plus the original shapes for
430
504
  // forward-compatibility with a future explicit `pattern` writer.
431
- const patternEntries = entries.filter(e => e.key.includes('pattern') ||
432
- e.metadata?.type === 'pattern' ||
433
- e.key.startsWith('learned-') ||
434
- e.namespace === 'patterns' ||
435
- e.metadata?.type === 'routing-decision');
505
+ const patternEntries = entries.filter(isLearnedPatternEntry);
436
506
  // Categorize patterns
437
507
  const categories = {};
438
508
  patternEntries.forEach(e => {
@@ -2144,7 +2214,15 @@ export const hooksSessionEnd = {
2144
2214
  handler: async (params) => {
2145
2215
  const saveState = params.saveState !== false;
2146
2216
  const shouldStopDaemon = params.stopDaemon !== false;
2147
- const sessionId = `session-${Date.now() - 3600000}`; // Default session (1 hour ago)
2217
+ const session = loadActiveSessionState();
2218
+ if (!session) {
2219
+ throw new Error('No active session state found at .claude-flow/sessions/current.json');
2220
+ }
2221
+ const sessionId = session.id;
2222
+ const endedAt = Date.now();
2223
+ const duration = Math.max(0, endedAt - Date.parse(session.startedAt));
2224
+ const activity = loadSessionActivity(session, endedAt);
2225
+ const summary = buildSessionSummary(activity, duration);
2148
2226
  // Stop daemon if enabled
2149
2227
  let daemonStopped = false;
2150
2228
  if (shouldStopDaemon) {
@@ -2157,12 +2235,10 @@ export const hooksSessionEnd = {
2157
2235
  // Daemon may not be running
2158
2236
  }
2159
2237
  }
2160
- // Read actual counts from stores
2238
+ // Read aggregate store data for the remaining compatibility metrics.
2161
2239
  const store = loadMemoryStore();
2162
2240
  const allEntries = Object.values(store.entries);
2163
- const taskCount = allEntries.filter(e => e.key.includes('task')).length;
2164
2241
  const agentCount = allEntries.filter(e => e.key.includes('agent')).length;
2165
- const patternCount = allEntries.filter(e => e.key.includes('pattern')).length;
2166
2242
  const trajectoryCount = activeTrajectories.size;
2167
2243
  // Check for pending-insights.jsonl
2168
2244
  let insightCount = 0;
@@ -2183,9 +2259,9 @@ export const hooksSessionEnd = {
2183
2259
  bridge = await import('../memory/memory-bridge.js');
2184
2260
  const result = await bridge.bridgeSessionEnd({
2185
2261
  sessionId,
2186
- summary: saveState ? 'Session ended with state saved' : 'Session ended',
2187
- tasksCompleted: taskCount,
2188
- patternsLearned: patternCount,
2262
+ summary,
2263
+ tasksCompleted: activity.tasksCompleted,
2264
+ patternsLearned: activity.patternsLearned,
2189
2265
  });
2190
2266
  if (result) {
2191
2267
  sessionPersistence = {
@@ -2210,19 +2286,19 @@ export const hooksSessionEnd = {
2210
2286
  }
2211
2287
  return {
2212
2288
  sessionId,
2213
- duration: 3600000, // 1 hour in ms
2289
+ duration,
2214
2290
  statePath: saveState ? `.claude/sessions/${sessionId}.json` : undefined,
2215
2291
  daemon: { stopped: daemonStopped },
2216
2292
  sessionPersistence: sessionPersistence || { controller: 'none', persisted: false },
2217
2293
  summary: {
2218
- tasksExecuted: taskCount,
2219
- filesModified: 0,
2294
+ tasksExecuted: activity.tasksCompleted,
2295
+ filesModified: activity.editsRecorded,
2220
2296
  agentsSpawned: agentCount,
2221
2297
  pendingInsights: insightCount,
2222
2298
  memoryEntries: allEntries.length,
2223
2299
  },
2224
2300
  learningUpdates: {
2225
- patternsLearned: patternCount,
2301
+ patternsLearned: activity.patternsLearned,
2226
2302
  trajectoriesRecorded: trajectoryCount,
2227
2303
  },
2228
2304
  };
@@ -936,11 +936,22 @@ export async function bridgeStoreEntry(options) {
936
936
  persistWarning = `sql.js fallback driver in use — this write may not be durably persisted to disk (${msg}). Install better-sqlite3's native binding to restore durable writes (see issue #2968: npm rebuild better-sqlite3, or reinstall with install scripts enabled).`;
937
937
  }
938
938
  }
939
- // Phase 2: Write-through to TieredCache
939
+ // Phase 2: Invalidate cache so the next read fetches the fresh row.
940
+ //
941
+ // #3051 — this used to be a write-through with a partial payload
942
+ // (`{ id, key, namespace, content, embedding }` — missing `tags`,
943
+ // access_count, timestamps, metadata, provenance_type). Any read that
944
+ // hit the cache then returned `tags: cached.tags || []` = `[]` for the
945
+ // very entry that had just been written with real tags, so
946
+ // `memory_store({ tags })` → `memory_retrieve()` looked like it silently
947
+ // dropped tags. Invalidating instead of partial-cache-set forces the
948
+ // next `bridgeGetEntry` to load the authoritative row (which correctly
949
+ // parses tags/metadata from the JSON columns) and repopulate the cache
950
+ // from that full shape.
940
951
  const safeNs = String(namespace).replace(/:/g, '_');
941
952
  const safeKey = String(key).replace(/:/g, '_');
942
953
  const cacheKey = `entry:${safeNs}:${safeKey}`;
943
- await cacheSet(registry, cacheKey, { id, key, namespace, content: value, embedding: embeddingJson });
954
+ await cacheInvalidate(registry, cacheKey);
944
955
  // Phase 4: AttestationLog write audit
945
956
  await logAttestation(registry, 'store', id, { key, namespace, hasEmbedding: !!embeddingJson });
946
957
  return {
@@ -0,0 +1,21 @@
1
+ export interface ClaudeLaunchCommand {
2
+ command: string;
3
+ argsPrefix: string[];
4
+ }
5
+ interface ClaudeCommandResolverOptions {
6
+ platform?: NodeJS.Platform;
7
+ nodeExecutable?: string;
8
+ lookup?: (command: string, args: string[]) => string;
9
+ fileExists?: (path: string) => boolean;
10
+ }
11
+ /**
12
+ * Resolve Claude Code to a target that Node can spawn without a shell.
13
+ *
14
+ * Windows npm shims (`claude.cmd` and `claude.ps1`) are shell scripts and
15
+ * cannot be passed directly to spawn with `shell: false`. Resolve the native
16
+ * executable used by current Claude Code releases, or the JavaScript entry
17
+ * used by older releases, while keeping user-provided prompts out of a shell.
18
+ */
19
+ export declare function resolveClaudeLaunchCommand(options?: ClaudeCommandResolverOptions): ClaudeLaunchCommand | null;
20
+ export {};
21
+ //# sourceMappingURL=claude-command.d.ts.map
@@ -0,0 +1,49 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { posix, win32 } from 'node:path';
4
+ /**
5
+ * Resolve Claude Code to a target that Node can spawn without a shell.
6
+ *
7
+ * Windows npm shims (`claude.cmd` and `claude.ps1`) are shell scripts and
8
+ * cannot be passed directly to spawn with `shell: false`. Resolve the native
9
+ * executable used by current Claude Code releases, or the JavaScript entry
10
+ * used by older releases, while keeping user-provided prompts out of a shell.
11
+ */
12
+ export function resolveClaudeLaunchCommand(options = {}) {
13
+ const platform = options.platform ?? process.platform;
14
+ const path = platform === 'win32' ? win32 : posix;
15
+ const nodeExecutable = options.nodeExecutable ?? process.execPath;
16
+ const lookup = options.lookup ?? ((command, args) => execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
17
+ const fileExists = options.fileExists ?? existsSync;
18
+ let matches;
19
+ try {
20
+ const output = platform === 'win32'
21
+ ? lookup('where.exe', ['claude'])
22
+ : lookup('which', ['claude']);
23
+ matches = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ if (platform !== 'win32') {
29
+ const command = matches.find(fileExists);
30
+ return command ? { command, argsPrefix: [] } : null;
31
+ }
32
+ const executable = matches.find((candidate) => path.extname(candidate).toLowerCase() === '.exe' && fileExists(candidate));
33
+ if (executable)
34
+ return { command: executable, argsPrefix: [] };
35
+ for (const shim of matches) {
36
+ const npmBin = path.dirname(shim);
37
+ const packageRoot = path.join(npmBin, 'node_modules', '@anthropic-ai', 'claude-code');
38
+ const nativeExecutable = path.join(packageRoot, 'bin', 'claude.exe');
39
+ if (fileExists(nativeExecutable)) {
40
+ return { command: nativeExecutable, argsPrefix: [] };
41
+ }
42
+ const javascriptEntry = path.join(packageRoot, 'cli.js');
43
+ if (fileExists(javascriptEntry)) {
44
+ return { command: nodeExecutable, argsPrefix: [javascriptEntry] };
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ //# sourceMappingURL=claude-command.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.38.13",
3
+ "version": "3.38.15",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",