@claude-flow/cli 3.56.3 → 3.56.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 (73) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +13 -8
  2. package/.claude/helpers/helpers.manifest.json +5 -5
  3. package/.claude/helpers/hook-handler.cjs +19 -14
  4. package/.claude/helpers/learning-service.mjs +5 -2
  5. package/.claude/helpers/session.cjs +48 -12
  6. package/.claude/helpers/statusline.cjs +14 -4
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/commands/doctor.js +7 -13
  9. package/dist/src/commands/hooks.js +3 -6
  10. package/dist/src/commands/init.js +3 -10
  11. package/dist/src/commands/metaharness.js +9 -14
  12. package/dist/src/commands/plugins.js +81 -8
  13. package/dist/src/commands/security-code-patterns.d.ts +40 -0
  14. package/dist/src/commands/security-code-patterns.js +192 -0
  15. package/dist/src/commands/security.js +8 -11
  16. package/dist/src/commands/status.js +8 -10
  17. package/dist/src/init/executor.d.ts +1 -1
  18. package/dist/src/init/executor.js +25 -16
  19. package/dist/src/init/helper-commands.d.ts +18 -0
  20. package/dist/src/init/helper-commands.js +142 -0
  21. package/dist/src/init/helpers-generator.js +128 -29
  22. package/dist/src/init/settings-generator.js +36 -103
  23. package/dist/src/mcp-tools/metaharness-tools.js +7 -10
  24. package/dist/src/mcp-tools/task-tools.js +24 -3
  25. package/dist/src/mcp-tools/testgen-tools.js +8 -12
  26. package/dist/src/mcp-tools/transfer-tools.js +9 -8
  27. package/dist/src/mcp-tools/workflow-tools.js +10 -4
  28. package/dist/src/memory/graph-edge-writer.js +33 -11
  29. package/dist/src/memory/memory-initializer.js +2 -19
  30. package/dist/src/memory/shared-sqlite.d.ts +6 -0
  31. package/dist/src/memory/shared-sqlite.js +35 -7
  32. package/dist/src/missions/schemas.d.ts +125 -125
  33. package/dist/src/mods/command-registry/schema.d.ts +199 -199
  34. package/dist/src/plugins/manager.d.ts +3 -1
  35. package/dist/src/plugins/manager.js +31 -10
  36. package/dist/src/plugins/own-install.d.ts +15 -0
  37. package/dist/src/plugins/own-install.js +51 -0
  38. package/dist/src/plugins/store/discovery.d.ts +26 -2
  39. package/dist/src/plugins/store/discovery.js +108 -81
  40. package/dist/src/plugins/store/index.d.ts +1 -1
  41. package/dist/src/plugins/store/index.js +1 -1
  42. package/dist/src/plugins/store/types.d.ts +5 -0
  43. package/dist/src/plugins/trust-policy.d.ts +13 -0
  44. package/dist/src/plugins/trust-policy.js +29 -0
  45. package/dist/src/ruvector/graph-analyzer.js +18 -6
  46. package/dist/src/transfer/ipfs/client.js +3 -1
  47. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  48. package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts +1 -0
  49. package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts.map +1 -1
  50. package/node_modules/@claude-flow/mcp/dist/resource-registry.js +10 -4
  51. package/node_modules/@claude-flow/mcp/dist/resource-registry.js.map +1 -1
  52. package/node_modules/@claude-flow/mcp/dist/sampling.d.ts.map +1 -1
  53. package/node_modules/@claude-flow/mcp/dist/sampling.js +13 -6
  54. package/node_modules/@claude-flow/mcp/dist/sampling.js.map +1 -1
  55. package/node_modules/@claude-flow/mcp/dist/task-manager.d.ts.map +1 -1
  56. package/node_modules/@claude-flow/mcp/dist/task-manager.js +10 -1
  57. package/node_modules/@claude-flow/mcp/dist/task-manager.js.map +1 -1
  58. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts +1 -0
  59. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  60. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +17 -11
  61. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  62. package/node_modules/@claude-flow/mcp/package.json +1 -1
  63. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  64. package/node_modules/@claude-flow/security/dist/index.d.ts +1 -1
  65. package/node_modules/@claude-flow/security/dist/index.d.ts.map +1 -1
  66. package/node_modules/@claude-flow/security/dist/index.js +1 -1
  67. package/node_modules/@claude-flow/security/dist/index.js.map +1 -1
  68. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts +9 -1
  69. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts.map +1 -1
  70. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js +15 -2
  71. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js.map +1 -1
  72. package/node_modules/@claude-flow/security/package.json +1 -1
  73. package/package.json +5 -5
@@ -17,11 +17,16 @@ import { fileURLToPath } from 'url';
17
17
 
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = dirname(__filename);
20
+ // Executable modules resolve only from the helper's own install root. A
21
+ // home-level helper can serve an untrusted project, so CLAUDE_PROJECT_DIR must
22
+ // never decide which JavaScript gets imported.
23
+ const MODULE_ROOT = join(__dirname, '../..');
20
24
  // Home-level helpers can serve a different project. Keep the memory writer on
21
25
  // the same project root as intelligence.cjs, which reads CLAUDE_PROJECT_DIR.
26
+ // PROJECT_ROOT is for data only.
22
27
  const PROJECT_ROOT = process.env.CLAUDE_PROJECT_DIR
23
28
  ? resolve(process.env.CLAUDE_PROJECT_DIR)
24
- : join(__dirname, '../..');
29
+ : MODULE_ROOT;
25
30
  const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
26
31
  const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
27
32
 
@@ -41,7 +46,7 @@ const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
41
46
  // told exactly how to fix it (on both stdout, so it shows in the Claude Code hook
42
47
  // transcript, and stderr, per the issue's requested channel).
43
48
  function warnMemoryUnavailable() {
44
- const line1 = `[AutoMemory] @claude-flow/memory not resolvable from ${PROJECT_ROOT} — self-learning imports are DISABLED.`;
49
+ const line1 = `[AutoMemory] @claude-flow/memory not resolvable from ${MODULE_ROOT} — self-learning imports are DISABLED.`;
45
50
  const line2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
46
51
  console.log(`${YELLOW}${line1}${RESET}`);
47
52
  console.log(`${YELLOW}${line2}${RESET}`);
@@ -175,7 +180,7 @@ async function loadMemoryPackage() {
175
180
  // project — so init resolves it from the CLI's own context and records the
176
181
  // absolute path here. This is the only strategy that works on that install.
177
182
  try {
178
- const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
183
+ const sidecar = join(MODULE_ROOT, '.claude-flow', 'memory-package.json');
179
184
  if (existsSync(sidecar)) {
180
185
  const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
181
186
  if (rec?.distPath && existsSync(rec.distPath)) {
@@ -185,7 +190,7 @@ async function loadMemoryPackage() {
185
190
  } catch { /* fall through */ }
186
191
 
187
192
  // Strategy 1: Local dev (built dist)
188
- const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
193
+ const localDist = join(MODULE_ROOT, 'v3/@claude-flow/memory/dist/index.js');
189
194
  if (existsSync(localDist)) {
190
195
  try {
191
196
  return await import(`file://${localDist}`);
@@ -196,7 +201,7 @@ async function loadMemoryPackage() {
196
201
  // when installed as a transitive dependency via npx ruflo / npx @claude-flow/cli@latest)
197
202
  try {
198
203
  const { createRequire } = await import('module');
199
- const require = createRequire(join(PROJECT_ROOT, 'package.json'));
204
+ const require = createRequire(join(MODULE_ROOT, 'package.json'));
200
205
  return require('@claude-flow/memory');
201
206
  } catch { /* fall through */ }
202
207
 
@@ -205,8 +210,8 @@ async function loadMemoryPackage() {
205
210
  return await import('@claude-flow/memory');
206
211
  } catch { /* fall through */ }
207
212
 
208
- // Strategy 4: Walk up from PROJECT_ROOT looking for @claude-flow/memory in any node_modules
209
- let searchDir = PROJECT_ROOT;
213
+ // Strategy 4: Walk up from MODULE_ROOT looking for @claude-flow/memory in any node_modules
214
+ let searchDir = MODULE_ROOT;
210
215
  const { parse } = await import('path');
211
216
  while (searchDir !== parse(searchDir).root) {
212
217
  const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
@@ -378,7 +383,7 @@ async function doStatus() {
378
383
  const memPkg = await loadMemoryPackage();
379
384
  const config = readConfig();
380
385
 
381
- const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
386
+ const sidecar = join(MODULE_ROOT, '.claude-flow', 'memory-package.json');
382
387
  const hasSidecar = existsSync(sidecar);
383
388
 
384
389
  console.log('\n=== Auto Memory Bridge Status ===\n');
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.56.3",
3
+ "version": "3.56.5",
4
4
  "files": {
5
- "auto-memory-hook.mjs": "c8d218526a3c34034c2a69de23f507f0eb0f70d59c16426a8edc849e6ac22529",
6
- "hook-handler.cjs": "34d2aacfb59a73ff94b1467bb57ab9d9c4af89c0e470aadf3bd0d74a503a658d",
5
+ "auto-memory-hook.mjs": "381412515fb490def7d821ddfe1ebf8d9bee76e49a86e182806c0274f568bf15",
6
+ "hook-handler.cjs": "38fc235079ddcf74b62452a91b9e56703026a1a7682dcce3c635be5ad1c8ea19",
7
7
  "intelligence.cjs": "ac5033a77bcea9796856629d5d319bde9ab6b7edf0dcd0d4af187ce30af90836",
8
- "statusline.cjs": "4a48353b4f1566fa4379b00fd0321b6676a22b6cc91fbbd8380ac5183d619468",
8
+ "statusline.cjs": "d72417ecb38634011a19f095a30e07b9929e6f97663520172ece9be7491b2eaa",
9
9
  "router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
10
10
  }
11
11
  },
12
- "signature": "U1PReRNeRDIa3/64YWz99uxvI1OmVc3d53VBaVg9neZlrSC5LNaCGtMcGyIcrB2bA6jl4SHsHlbDvNcVH38UCw==",
12
+ "signature": "/MW5zFGHYeVuoNFAcDlUhkjIdDhPmBiUQTVJTz8Ku20SpFOoxpIXbgv01PxS5IloBqfmuDLieu++2cG55SIzAg==",
13
13
  "algorithm": "ed25519"
14
14
  }
@@ -18,6 +18,10 @@ const fs = require('fs');
18
18
  const os = require('os');
19
19
 
20
20
  const helpersDir = __dirname;
21
+ // The helper's own install root: the project for a project-local helper,
22
+ // $HOME for a home-level one. CLI resolution starts here, never at the opened
23
+ // project, so a home-level helper does not run code the project supplies.
24
+ const helperRoot = path.resolve(helpersDir, '..', '..');
21
25
 
22
26
  // Resolve an installed @claude-flow/cli (or ruflo) bin — mirrors
23
27
  // statusline-generator.ts's resolveCliBin() candidate list. Used only to
@@ -38,12 +42,11 @@ const helpersDir = __dirname;
38
42
  function resolveCliBinForHook() {
39
43
  try {
40
44
  const home = os.homedir();
41
- const cwd = process.cwd();
42
45
  const candidates = [
43
46
  path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'bin', 'cli.js'),
44
- path.join(cwd, 'node_modules', '@claude-flow', 'cli', 'bin', 'cli.js'),
45
- path.join(cwd, 'node_modules', 'ruflo', 'bin', 'cli.js'),
46
- path.join(cwd, 'v3', '@claude-flow', 'cli', 'bin', 'cli.js'),
47
+ path.join(helperRoot, 'node_modules', '@claude-flow', 'cli', 'bin', 'cli.js'),
48
+ path.join(helperRoot, 'node_modules', 'ruflo', 'bin', 'cli.js'),
49
+ path.join(helperRoot, 'v3', '@claude-flow', 'cli', 'bin', 'cli.js'),
47
50
  // helpersDir is .claude/helpers/ inside the package itself when this
48
51
  // file is running from a real @claude-flow/cli install (not a project
49
52
  // that merely copied the helper) — its bin/ is two levels up.
@@ -77,14 +80,19 @@ function resolveCliBinForHook() {
77
80
  // render path, where local-first exists purely for per-render latency.
78
81
  // `--prefer-offline` avoids a registry round trip for the tarball when
79
82
  // already cached while still resolving the current `@latest` version.
83
+ //
84
+ // `--prefix helperRoot` makes npx resolve @claude-flow/cli from the helper's
85
+ // install root instead of the cwd (the opened project); the spawned CLI still
86
+ // runs with the project as its cwd.
87
+ function cliSpawnArgs(cliBin, args, platform = process.platform) {
88
+ if (cliBin) return [process.execPath, [cliBin, ...args]];
89
+ return [platform === 'win32' ? 'npx.cmd' : 'npx', ['--prefer-offline', '--prefix', helperRoot, '@claude-flow/cli', ...args]];
90
+ }
91
+
80
92
  function spawnDetachedHookRefresh(subcommand) {
81
93
  try {
82
94
  const { spawn } = require('child_process');
83
- const cliBin = resolveCliBinForHook();
84
- const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
85
- const spawnArgs = cliBin
86
- ? [process.execPath, [cliBin, 'hooks', subcommand, '--quiet']]
87
- : [cmd, ['--prefer-offline', '@claude-flow/cli', 'hooks', subcommand, '--quiet']];
95
+ const spawnArgs = cliSpawnArgs(resolveCliBinForHook(), ['hooks', subcommand, '--quiet']);
88
96
  const child = spawn(spawnArgs[0], spawnArgs[1], {
89
97
  detached: true,
90
98
  stdio: 'ignore',
@@ -151,10 +159,7 @@ function firstRunAutoEnableIfEligible() {
151
159
  const cliBin = resolveCliBinForHook();
152
160
  const runDetached = (args) => {
153
161
  try {
154
- const spawnArgs = cliBin
155
- ? [process.execPath, [cliBin, ...args]]
156
- : [process.platform === 'win32' ? 'npx.cmd' : 'npx',
157
- ['--prefer-offline', '@claude-flow/cli', ...args]];
162
+ const spawnArgs = cliSpawnArgs(cliBin, args);
158
163
  const child = spawn(spawnArgs[0], spawnArgs[1], {
159
164
  detached: true, stdio: 'ignore', env: process.env, windowsHide: true,
160
165
  });
@@ -772,4 +777,4 @@ if (require.main === module) {
772
777
  // Which sibling helpers loaded (all CommonJS, shipped as .cjs — #3555).
773
778
  const loadedHelpers = { router: !!router, session: !!session, memory: !!memory, intelligence: !!intelligence };
774
779
 
775
- module.exports = { runWithTimeout, INTELLIGENCE_TIMEOUT_MS, loadedHelpers, ownedByMod, MOD_OWNABLE_EVENTS };
780
+ module.exports = { runWithTimeout, INTELLIGENCE_TIMEOUT_MS, loadedHelpers, ownedByMod, MOD_OWNABLE_EVENTS, resolveCliBinForHook, cliSpawnArgs, helperRoot };
@@ -26,9 +26,12 @@ import Database from 'better-sqlite3';
26
26
 
27
27
  const __filename = fileURLToPath(import.meta.url);
28
28
  const __dirname = dirname(__filename);
29
+ // Executable modules resolve only from the helper's own install root; a
30
+ // home-level helper can serve an untrusted project. PROJECT_ROOT is data only.
31
+ const MODULE_ROOT = join(__dirname, '../..');
29
32
  const PROJECT_ROOT = process.env.CLAUDE_PROJECT_DIR
30
33
  ? resolve(process.env.CLAUDE_PROJECT_DIR)
31
- : join(__dirname, '../..');
34
+ : MODULE_ROOT;
32
35
  const DATA_DIR = join(PROJECT_ROOT, '.claude-flow/learning');
33
36
  const DB_PATH = join(DATA_DIR, 'patterns.db');
34
37
  const METRICS_PATH = join(DATA_DIR, 'learning-metrics.json');
@@ -463,7 +466,7 @@ class EmbeddingService {
463
466
 
464
467
  try {
465
468
  // Dynamically import agentic-flow OptimizedEmbedder
466
- const agenticFlowPath = join(PROJECT_ROOT, 'node_modules/agentic-flow/dist/embeddings/optimized-embedder.js');
469
+ const agenticFlowPath = join(MODULE_ROOT, 'node_modules/agentic-flow/dist/embeddings/optimized-embedder.js');
467
470
 
468
471
  if (existsSync(agenticFlowPath)) {
469
472
  const { getOptimizedEmbedder } = await import(agenticFlowPath);
@@ -6,9 +6,41 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const crypto = require('crypto');
9
10
 
10
11
  const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions');
11
12
  const SESSION_FILE = path.join(SESSION_DIR, 'current.json');
13
+ // Archive names are generated by start(); anything else in current.json is
14
+ // untrusted project content and must never pick a path.
15
+ const SESSION_ID = /^session-\d+$/;
16
+
17
+ // current.json lives in the opened project. A symlink there must never be read
18
+ // or written through, so only a regular file counts as an active session.
19
+ function hasSessionFile() {
20
+ try { return fs.lstatSync(SESSION_FILE).isFile(); } catch { return false; }
21
+ }
22
+
23
+ function archiveId(id) {
24
+ return SESSION_ID.test(String(id)) ? String(id) : `session-${Date.now()}`;
25
+ }
26
+
27
+ // The session directory must resolve inside the project (no symlinked escape).
28
+ function assertSessionDirContained() {
29
+ const root = fs.realpathSync(process.cwd());
30
+ // Resolve the deepest part of the path that exists (lstat: a symlink counts),
31
+ // so a symlinked .claude-flow is refused before anything is created under it.
32
+ let dir = SESSION_DIR;
33
+ for (;;) {
34
+ try { fs.lstatSync(dir); break; } catch { /* not created yet */ }
35
+ const parent = path.dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ }
39
+ const rel = path.relative(root, fs.realpathSync(dir));
40
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
41
+ throw new Error('Session directory resolves outside the project');
42
+ }
43
+ }
12
44
 
13
45
  // #2307: Atomic write — serialize to a per-process temp file, then rename()
14
46
  // into place. rename() is atomic on the same filesystem, so concurrent writers
@@ -18,9 +50,12 @@ const SESSION_FILE = path.join(SESSION_DIR, 'current.json');
18
50
  // tail dangling past the end (valid JSON + trailing garbage = parse error).
19
51
  // Temp name includes process.pid so concurrent writers don't collide on it.
20
52
  // Same class as #1707 (metrics) / #1637 (daemon-state) — session.js was missed.
53
+ // The temp file is created exclusively (never through a planted symlink) and
54
+ // rename() replaces a symlinked target instead of writing through it.
21
55
  function atomicWrite(file, data) {
22
- const tmp = `${file}.tmp.${process.pid}`;
23
- fs.writeFileSync(tmp, data);
56
+ assertSessionDirContained();
57
+ const tmp = `${file}.tmp.${process.pid}.${crypto.randomBytes(6).toString('hex')}`;
58
+ fs.writeFileSync(tmp, data, { flag: 'wx' });
24
59
  fs.renameSync(tmp, file);
25
60
  }
26
61
 
@@ -40,6 +75,7 @@ const commands = {
40
75
  },
41
76
  };
42
77
 
78
+ assertSessionDirContained();
43
79
  fs.mkdirSync(SESSION_DIR, { recursive: true });
44
80
  atomicWrite(SESSION_FILE, JSON.stringify(session, null, 2));
45
81
 
@@ -48,7 +84,7 @@ const commands = {
48
84
  },
49
85
 
50
86
  restore: () => {
51
- if (!fs.existsSync(SESSION_FILE)) {
87
+ if (!hasSessionFile()) {
52
88
  console.log('No session to restore');
53
89
  return null;
54
90
  }
@@ -70,7 +106,7 @@ const commands = {
70
106
  },
71
107
 
72
108
  end: () => {
73
- if (!fs.existsSync(SESSION_FILE)) {
109
+ if (!hasSessionFile()) {
74
110
  console.log('No active session');
75
111
  return null;
76
112
  }
@@ -79,12 +115,12 @@ const commands = {
79
115
  session.endedAt = new Date().toISOString();
80
116
  session.duration = Date.now() - new Date(session.startedAt).getTime();
81
117
 
82
- // Archive session
83
- const archivePath = path.join(SESSION_DIR, `${session.id}.json`);
84
- atomicWrite(archivePath, JSON.stringify(session, null, 2));
118
+ // Archive session under a validated name; a crafted id cannot pick the path.
119
+ const id = archiveId(session.id);
120
+ atomicWrite(path.join(SESSION_DIR, `${id}.json`), JSON.stringify(session, null, 2));
85
121
  fs.unlinkSync(SESSION_FILE);
86
122
 
87
- console.log(`Session ended: ${session.id}`);
123
+ console.log(`Session ended: ${id}`);
88
124
  console.log(`Duration: ${Math.round(session.duration / 1000 / 60)} minutes`);
89
125
  console.log(`Metrics: ${JSON.stringify(session.metrics)}`);
90
126
 
@@ -92,7 +128,7 @@ const commands = {
92
128
  },
93
129
 
94
130
  status: () => {
95
- if (!fs.existsSync(SESSION_FILE)) {
131
+ if (!hasSessionFile()) {
96
132
  console.log('No active session');
97
133
  return null;
98
134
  }
@@ -109,7 +145,7 @@ const commands = {
109
145
  },
110
146
 
111
147
  update: (key, value) => {
112
- if (!fs.existsSync(SESSION_FILE)) {
148
+ if (!hasSessionFile()) {
113
149
  console.log('No active session');
114
150
  return null;
115
151
  }
@@ -123,7 +159,7 @@ const commands = {
123
159
  },
124
160
 
125
161
  get: (key) => {
126
- if (!fs.existsSync(SESSION_FILE)) return null;
162
+ if (!hasSessionFile()) return null;
127
163
  try {
128
164
  const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
129
165
  return key ? (session.context || {})[key] : session.context;
@@ -131,7 +167,7 @@ const commands = {
131
167
  },
132
168
 
133
169
  metric: (name) => {
134
- if (!fs.existsSync(SESSION_FILE)) {
170
+ if (!hasSessionFile()) {
135
171
  return null;
136
172
  }
137
173
 
@@ -43,6 +43,10 @@ const CONFIG = {
43
43
  };
44
44
 
45
45
  const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
46
+ // The helper's own install root: the project for a project-local helper,
47
+ // $HOME for a home-level one. The CLI is resolved from here, never from CWD,
48
+ // so a home-level statusline does not run code the opened project supplies.
49
+ const HELPER_ROOT = path.resolve(__dirname, '..', '..');
46
50
  // Replaced by statusline-generator with the package root of the CLI that
47
51
  // installed this helper. This survives custom npm prefixes and bundled Node
48
52
  // runtimes whose process.execPath belongs to a different tree (#2811).
@@ -111,9 +115,9 @@ function resolveCliBinCandidates() {
111
115
  const home = os.homedir();
112
116
  candidates.push(
113
117
  path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'bin', 'cli.js'),
114
- path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'bin', 'cli.js'),
115
- path.join(CWD, 'node_modules', 'ruflo', 'bin', 'cli.js'),
116
- path.join(CWD, 'v3', '@claude-flow', 'cli', 'bin', 'cli.js'),
118
+ path.join(HELPER_ROOT, 'node_modules', '@claude-flow', 'cli', 'bin', 'cli.js'),
119
+ path.join(HELPER_ROOT, 'node_modules', 'ruflo', 'bin', 'cli.js'),
120
+ path.join(HELPER_ROOT, 'v3', '@claude-flow', 'cli', 'bin', 'cli.js'),
117
121
  );
118
122
  try {
119
123
  const binDir = path.dirname(process.execPath);
@@ -237,9 +241,15 @@ function getStatuslineData() {
237
241
  // intelligence and an empty promo row (the memo cache that keeps the row
238
242
  // populated across CLI hiccups is only ever written from a SUCCESSFUL
239
243
  // delegation, so it could never get seeded on Windows either).
244
+ // These command lines go through a shell, so a path the shell would interpret
245
+ // (a quote, $, backtick, %, !, newline, or a backslash on POSIX) is never
246
+ // interpolated: the directory name is chosen by whoever made the checkout.
247
+ const shellSafe = (p) => !/["`$%!\r\n]/.test(p) && (process.platform === 'win32' || !p.includes('\\'));
240
248
  const cmds = resolveCliBinCandidates()
249
+ .filter((bin) => shellSafe(bin) && shellSafe(process.execPath))
241
250
  .map((bin) => '"' + process.execPath + '" "' + bin + '" hooks statusline --json')
242
- .concat(['npx --prefer-offline @claude-flow/cli hooks statusline --json']);
251
+ // --prefix: npx resolves the package from HELPER_ROOT, not the project.
252
+ .concat(shellSafe(HELPER_ROOT) ? ['npx --prefer-offline --prefix "' + HELPER_ROOT + '" @claude-flow/cli hooks statusline --json'] : []);
243
253
  for (const cmd of cmds) {
244
254
  try {
245
255
  const raw = execSync(
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-09T21:52:00.476Z",
5
- "gitSha": "8c60c8c7",
4
+ "generatedAt": "2026-10-11T02:06:08.884Z",
5
+ "gitSha": "72255a2f",
6
6
  "catalog": {
7
7
  "agents": 176,
8
8
  "tools": 423,
@@ -9,6 +9,7 @@ import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } fro
9
9
  import { join, dirname } from 'path';
10
10
  import { fileURLToPath, pathToFileURL } from 'url';
11
11
  import { createHash } from 'crypto';
12
+ import { ownInstallAncestors } from '../plugins/own-install.js';
12
13
  import { execSync, exec, execFile } from 'child_process';
13
14
  import { promisify } from 'util';
14
15
  import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
@@ -2045,26 +2046,19 @@ async function checkMetaharnessIntegration() {
2045
2046
  // `dist/src/commands/doctor.js` (built) or `src/commands/doctor.ts`
2046
2047
  // (dev) until we find a directory containing `plugins/ruflo-metaharness/`.
2047
2048
  const candidates = [];
2048
- // Strategy 1: walk up from this module's own URL — covers npx + global install.
2049
+ // Walk up from this module's own URL — covers npx, global and project-local
2050
+ // installs and monorepo dev. The plugin found here is imported below, so it
2051
+ // must come from this CLI's own install, never from the cwd (the project).
2049
2052
  try {
2053
+ // Bounded at node_modules: a project-local install must not reach the project.
2050
2054
  const selfDir = dirname(fileURLToPath(import.meta.url));
2051
- let q = selfDir;
2052
- for (let i = 0; i < 8; i++) {
2055
+ for (const q of ownInstallAncestors(selfDir)) {
2053
2056
  candidates.push(join(q, 'plugins', 'ruflo-metaharness'));
2054
- q = dirname(q);
2055
2057
  }
2056
2058
  }
2057
2059
  catch {
2058
- // import.meta.url unavailable under some bundlers — fall through to cwd walk.
2059
- }
2060
- // Strategy 2: walk up from cwd — covers monorepo dev (running from a sub-package).
2061
- let p = process.cwd();
2062
- for (let i = 0; i < 8; i++) {
2063
- candidates.push(join(p, 'plugins', 'ruflo-metaharness'));
2064
- p = dirname(p);
2060
+ // import.meta.url unavailable under some bundlers — the plugin is reported absent.
2065
2061
  }
2066
- // Strategy 3: explicit node_modules path relative to cwd — covers project-local install.
2067
- candidates.push(join(process.cwd(), 'node_modules', '@claude-flow', 'cli', 'plugins', 'ruflo-metaharness'));
2068
2062
  let pluginDir = null;
2069
2063
  for (const c of candidates) {
2070
2064
  if (existsSync(join(c, 'scripts', '_harness.mjs'))) {
@@ -4171,18 +4171,15 @@ const tokenOptimizeCommand = {
4171
4171
  try {
4172
4172
  // Check if agentic-flow v3 is available
4173
4173
  const rb = await import('agentic-flow/reasoningbank').catch(() => null);
4174
+ // No root-entry fallback: importing 'agentic-flow' runs its CLI main()
4175
+ // in 3.0.0-alpha.x, and the root re-exports this same reasoningbank
4176
+ // module, so it cannot load when this subpath did not.
4174
4177
  if (rb) {
4175
4178
  agenticFlowAvailable = true;
4176
4179
  if (typeof rb.retrieveMemories === 'function') {
4177
4180
  reasoningBank = rb;
4178
4181
  }
4179
4182
  }
4180
- else {
4181
- // Legacy check for older agentic-flow
4182
- const af = await import('agentic-flow').catch(() => null);
4183
- if (af)
4184
- agenticFlowAvailable = true;
4185
- }
4186
4183
  const versionLabel = agenticFlowAvailable ? `agentic-flow v3 detected (ReasoningBank: ${reasoningBank ? 'active' : 'unavailable'})` : 'agentic-flow not available (using fallbacks)';
4187
4184
  spinner.succeed(versionLabel);
4188
4185
  output.writeln();
@@ -50,16 +50,9 @@ async function resolveCodexInitializer(cwd) {
50
50
  const resolutionStrategies = [
51
51
  // Strategy 1: Direct import (works if installed as CLI dependency)
52
52
  async () => (await import(codexModuleId)).CodexInitializer,
53
- // Strategy 2: Project node_modules (works if installed in user's project)
54
- async () => {
55
- const projectPath = path.join(cwd, 'node_modules', '@claude-flow', 'codex', 'dist', 'index.js');
56
- if (fs.existsSync(projectPath)) {
57
- const mod = await import(`file://${projectPath}`);
58
- return mod.CodexInitializer;
59
- }
60
- throw new Error('Not found in project');
61
- },
62
- // Strategy 3: Global node_modules
53
+ // (No project node_modules strategy: init must not import code the
54
+ // target project supplies.)
55
+ // Strategy 2: Global node_modules
63
56
  async () => {
64
57
  const { execSync } = await import('child_process');
65
58
  const globalPath = execSync('npm root -g', { encoding: 'utf-8' }).trim();
@@ -38,6 +38,8 @@ import { spawnSync } from 'child_process';
38
38
  import { existsSync, readFileSync } from 'fs';
39
39
  import { join, dirname, resolve } from 'path';
40
40
  import { fileURLToPath } from 'url';
41
+ import { homedir } from 'os';
42
+ import { ownPluginDirs } from '../plugins/own-install.js';
41
43
  import { runFlywheelWorker } from '../services/harness-flywheel-runtime.js';
42
44
  import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, resetSequentialEvidence, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
43
45
  import { evaluatePolicyRequest } from '../services/policy-runtime.js';
@@ -213,20 +215,13 @@ async function dispatchFlywheel(operation, positional, flags) {
213
215
  * `prepublishOnly`).
214
216
  */
215
217
  function locatePluginScripts(requiredScript) {
216
- const candidates = [];
217
- // Up from the cli dist dir
218
- let p = resolve(__dirname);
219
- for (let i = 0; i < 8; i++) {
220
- candidates.push(join(p, 'plugins', 'ruflo-metaharness', 'scripts'));
221
- candidates.push(join(p, '..', 'plugins', 'ruflo-metaharness', 'scripts'));
222
- p = dirname(p);
223
- }
224
- // Also try from cwd (covers the "npx ruflo" case where the user is
225
- // sitting in their own repo and `npx ruflo metaharness score` should
226
- // score THAT repo using the LOCAL plugin if present).
227
- const cwd = process.cwd();
228
- candidates.push(join(cwd, 'plugins', 'ruflo-metaharness', 'scripts'));
229
- candidates.push(join(cwd, 'node_modules', '@claude-flow', 'cli', 'plugins', 'ruflo-metaharness', 'scripts'));
218
+ // Up from the cli dist dir, bounded at node_modules: a project-local install
219
+ // must not reach the project.
220
+ const candidates = ownPluginDirs(__dirname, 'ruflo-metaharness', 'scripts');
221
+ // The scripts are executed, so they come from this CLI's own install (or the
222
+ // user's marketplace checkout), never from the cwd: scoring a repo must not
223
+ // run plugin code that repo supplies. The repo is still the scoring target.
224
+ candidates.push(join(homedir(), '.claude', 'plugins', 'marketplaces', 'ruflo', 'plugins', 'ruflo-metaharness', 'scripts'));
230
225
  for (const c of candidates) {
231
226
  if (!existsSync(join(c, '_harness.mjs')))
232
227
  continue;