@getmarrow/install 0.1.52 → 0.1.53

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/src/installer.js CHANGED
@@ -5,12 +5,13 @@ const crypto = require('node:crypto');
5
5
  const { version: INSTALLER_ADAPTER_VERSION } = require('../package.json');
6
6
  const { controllerStatus, controllerSupportedPlatform, ensureGovernanceController } = require('./controller-manager');
7
7
  const { firstCapturePath, harnessReloadPlan } = require('./first-hour');
8
+ const { evidence: localControlEvidence } = require('./control-state');
8
9
 
9
10
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
10
11
  const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
11
12
  const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
12
- const MCP_ADAPTER_VERSION = '3.9.74';
13
- const MCP_ADAPTER_SOURCE_SHA = 'a34a87ee1d6ea16d8ebcc26aa0e66bf7dcb5e23f';
13
+ const MCP_ADAPTER_VERSION = '3.9.75';
14
+ const MCP_ADAPTER_SOURCE_SHA = '7548a7ae2d7e95bb16c62470da41af06cb62c5c1';
14
15
  const SDK_ADAPTER_VERSION = '3.7.62';
15
16
  const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
16
17
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
@@ -33,21 +34,75 @@ const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mc
33
34
  const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
34
35
  const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
35
36
  const MCP_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp session-hook`;
37
+ const CODEX_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-context-hook`;
38
+ const CODEX_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-pre-action-hook`;
39
+ const CODEX_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-hook`;
40
+ const CODEX_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-session-hook`;
41
+ const CURSOR_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-pre-action-hook`;
42
+ const CURSOR_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-hook`;
43
+ const CURSOR_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-session-hook`;
44
+ const CLINE_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook`;
45
+ const CLINE_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-hook`;
46
+ const CLINE_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-session-hook`;
47
+ const WINDSURF_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-pre-action-hook`;
48
+ const WINDSURF_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-hook`;
49
+ const WINDSURF_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-session-hook`;
50
+ const WINDSURF_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
51
+ const WINDSURF_PRE_ACTION_HOOK_COMMAND = `sh -c 'stderr="$(${WINDSURF_PRE_ACTION_ENTRYPOINT} 2>&1 >/dev/null)"; status=$?; if [ "$status" -eq 0 ]; then exit 0; fi; if [ "$status" -eq 2 ]; then printf "%s\\n" "$stderr" >&2; exit 2; fi; printf "%s\\n" "${WINDSURF_LAUNCH_FAILURE}" >&2; exit 2'`;
52
+ const WINDSURF_ACTION_RESULT_HOOK_COMMAND = `sh -c '${WINDSURF_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
53
+ const WINDSURF_SESSION_END_HOOK_COMMAND = `sh -c '${WINDSURF_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
54
+ const GEMINI_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-pre-action-hook`;
55
+ const GEMINI_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-hook`;
56
+ const GEMINI_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-session-hook`;
57
+ const GEMINI_FIXED_DENIAL = 'Marrow blocked this action because required governance approval or proof is unavailable.';
58
+ const GEMINI_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
59
+ const GEMINI_PRE_ACTION_HOOK_COMMAND = `sh -c 'output="$(${GEMINI_PRE_ACTION_ENTRYPOINT} 2>/dev/null)" && { case "$output" in "{\\"decision\\":\\"allow\\"}"|"{\\"decision\\":\\"deny\\",\\"reason\\":\\"${GEMINI_FIXED_DENIAL}\\"}") printf "%s\\n" "$output"; exit 0 ;; esac; }; printf "%s\\n" "${GEMINI_LAUNCH_FAILURE}" >&2; exit 2'`;
60
+ const GEMINI_ACTION_RESULT_HOOK_COMMAND = `sh -c '${GEMINI_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
61
+ const GEMINI_SESSION_END_HOOK_COMMAND = `sh -c '${GEMINI_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
62
+ const GROK_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-context-hook`;
63
+ const GROK_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-hook`;
64
+ const GROK_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-session-hook`;
65
+ const GROK_FIXED_DENIAL = 'Marrow blocked this protected action.';
66
+ const GROK_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
67
+ const GROK_PRE_ACTION_GUARD_SOURCE = [
68
+ 'const {spawn}=require("node:child_process");',
69
+ `const valid=new Set([${JSON.stringify('{"decision":"allow"}')},${JSON.stringify(`{"decision":"deny","reason":"${GROK_FIXED_DENIAL}"}`)}]);`,
70
+ 'let child=null,timer=null,done=false,input=[],inputBytes=0,output="",outputBytes=0;',
71
+ `const fail=()=>{if(done)return;done=true;if(timer)clearTimeout(timer);if(child&&!child.killed)child.kill("SIGKILL");process.stderr.write(${JSON.stringify(`${GROK_LAUNCH_FAILURE}\n`)});process.exitCode=2;process.stdin.destroy();};`,
72
+ 'process.stdin.on("error",fail);',
73
+ 'process.stdin.on("data",chunk=>{const value=Buffer.from(chunk);inputBytes+=value.length;if(inputBytes>65536){fail();return;}input.push(value);});',
74
+ 'process.stdin.on("end",()=>{if(done)return;try{',
75
+ `child=spawn(process.platform==="win32"?"npx.cmd":"npx",${JSON.stringify(['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp', 'grok-pre-action-hook'])},{stdio:["pipe","pipe","ignore"]});`,
76
+ 'timer=setTimeout(fail,5000);',
77
+ 'child.stdout.on("data",chunk=>{if(done)return;outputBytes+=chunk.length;if(outputBytes>512){fail();return;}output+=chunk.toString("utf8");});',
78
+ 'child.on("error",fail);child.stdin.on("error",fail);',
79
+ 'child.on("close",code=>{if(done)return;if(code!==0||!valid.has(output)){fail();return;}done=true;if(timer)clearTimeout(timer);process.stdout.write(output);});',
80
+ 'child.stdin.end(Buffer.concat(input));}catch{fail();}});',
81
+ ].join('');
82
+ const GROK_PRE_ACTION_HOOK_COMMAND = `node -e '${GROK_PRE_ACTION_GUARD_SOURCE}'`;
36
83
  const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*';
84
+ const CODEX_NATIVE_HOOK_MATCHER = 'Bash|apply_patch|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*|functions\\.(?!marrow_).*';
85
+ const CURSOR_NATIVE_HOOK_MATCHER = 'Shell|Write|Delete|Task|MCP:(?!marrow(?:_.*|:marrow_.*)$).*';
86
+ const GEMINI_NATIVE_HOOK_MATCHER = '^(?:run_shell_command|write_file|replace|edit_file|delete_file|mcp_(?!marrow_marrow_)[A-Za-z0-9_]{1,192})$';
87
+ const GROK_NATIVE_HOOK_MATCHER = 'run_terminal_command|search_replace|write|spawn_subagent|use_tool|workflow|image_gen|image_edit|image_to_video|reference_to_video';
88
+ const CODEX_HOOK_TIMEOUT_SECONDS = 5;
89
+ const CODEX_SESSION_TIMEOUT_SECONDS = 3;
90
+ const GEMINI_HOOK_TIMEOUT_MS = 5000;
91
+ const GEMINI_CLOSEOUT_TIMEOUT_MS = 3000;
37
92
  const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
38
93
  const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'mcp', 'ci', 'custom', 'unknown']);
39
94
  const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
40
95
  { client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
41
- { client: 'cursor', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
42
- { client: 'composer', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
43
- { client: 'cline', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
44
- { client: 'windsurf', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
45
- { client: 'codex', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
96
+ { client: 'cursor', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
97
+ { client: 'composer', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
98
+ { client: 'cline', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'cancel_closeout'], install_surface: 'mcp' },
99
+ { client: 'windsurf', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'response_closeout'], install_surface: 'mcp' },
100
+ { client: 'codex', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
46
101
  { client: 'opencode', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
47
102
  { client: 'hermes', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
48
103
  { client: 'openclaw', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
49
- { client: 'gemini', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
50
- { client: 'grok', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
104
+ { client: 'gemini', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
105
+ { client: 'grok', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
51
106
  { client: 'deepseek', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
52
107
  { client: 'qwen', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
53
108
  { client: 'kimi', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
@@ -269,41 +324,69 @@ function parseArgs(argv) {
269
324
  activate: false,
270
325
  controller: true,
271
326
  };
327
+ let explicitOperation = false;
272
328
 
273
329
  for (let i = 0; i < argv.length; i += 1) {
274
330
  const arg = argv[i];
275
331
  if (arg === 'activate' || arg === '--activate') {
332
+ explicitOperation = true;
276
333
  options.activate = true;
277
334
  options.yes = true;
278
335
  options.selfTest = true;
279
336
  }
280
- else if (arg === '--yes' || arg === '-y') options.yes = true;
337
+ else if (arg === '--yes' || arg === '-y') {
338
+ explicitOperation = true;
339
+ options.yes = true;
340
+ }
281
341
  else if (arg === '--repair' || arg === 'repair' || arg === 'update' || arg === '--update') {
342
+ explicitOperation = true;
282
343
  options.repair = true;
283
344
  options.yes = true;
284
345
  options.update = true;
285
346
  }
286
- else if (arg === '--dry-run') options.dryRun = true;
287
- else if (arg === '--doctor' || arg === 'doctor' || arg === 'check') options.doctor = true;
347
+ else if (arg === '--dry-run') {
348
+ explicitOperation = true;
349
+ options.dryRun = true;
350
+ }
351
+ else if (arg === '--doctor' || arg === 'doctor' || arg === 'check') {
352
+ explicitOperation = true;
353
+ options.doctor = true;
354
+ }
288
355
  else if (arg === '--json') options.json = true;
289
356
  else if (arg === '--no-self-test') {
357
+ explicitOperation = true;
290
358
  options.selfTest = false;
291
359
  options.selfTestExplicitlyDisabled = true;
292
360
  }
293
361
  else if (arg === '--no-controller') options.controller = false;
294
362
  else if (arg === '--self-test') options.selfTest = true;
295
363
  else if (arg === '--cwd') options.cwd = path.resolve(argv[++i] || options.cwd);
296
- else if (arg === '--mode') options.mode = argv[++i] || options.mode;
364
+ else if (arg === '--mode') {
365
+ explicitOperation = true;
366
+ options.mode = argv[++i] || options.mode;
367
+ }
297
368
  else if (arg === '--key') {
298
369
  options.apiKey = argv[++i] || options.apiKey;
299
370
  options.keyFromArg = true;
300
371
  }
301
372
  else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
302
373
  else if (arg === '--agent-id') options.agentId = argv[++i] || options.agentId;
303
- else if (arg === '--mcp') options.mode = 'mcp';
304
- else if (arg === '--sdk') options.mode = 'sdk';
305
- else if (arg === '--md' || arg === '--instructions') options.mode = 'md';
306
- else if (arg === '--both') options.mode = 'both';
374
+ else if (arg === '--mcp') {
375
+ explicitOperation = true;
376
+ options.mode = 'mcp';
377
+ }
378
+ else if (arg === '--sdk') {
379
+ explicitOperation = true;
380
+ options.mode = 'sdk';
381
+ }
382
+ else if (arg === '--md' || arg === '--instructions') {
383
+ explicitOperation = true;
384
+ options.mode = 'md';
385
+ }
386
+ else if (arg === '--both') {
387
+ explicitOperation = true;
388
+ options.mode = 'both';
389
+ }
307
390
  else if (arg === '--help' || arg === '-h') {
308
391
  options.help = true;
309
392
  } else {
@@ -314,6 +397,12 @@ function parseArgs(argv) {
314
397
  if (!['auto', 'mcp', 'sdk', 'both', 'md'].includes(options.mode)) {
315
398
  throw new Error('--mode must be one of auto, mcp, sdk, both, md');
316
399
  }
400
+ if (!explicitOperation) {
401
+ options.activate = true;
402
+ options.yes = true;
403
+ options.selfTest = true;
404
+ }
405
+ if (options.dryRun) options.selfTest = false;
317
406
  if (options.activate && options.selfTestExplicitlyDisabled) {
318
407
  throw new Error('activate cannot be combined with --no-self-test because server verification is required');
319
408
  }
@@ -326,6 +415,7 @@ function parseArgs(argv) {
326
415
 
327
416
  function usage() {
328
417
  return `Usage:
418
+ npx @getmarrow/install
329
419
  npx @getmarrow/install --dry-run
330
420
  npx @getmarrow/install activate
331
421
  npx @getmarrow/install --yes
@@ -336,6 +426,7 @@ function usage() {
336
426
  npx @getmarrow/install --sdk --yes
337
427
 
338
428
  Options:
429
+ (no command) Detect, install, self-test, and start the supported persistent controller
339
430
  activate Detect, install, self-test, and return a server-confirmed activation receipt
340
431
  --dry-run Print planned changes without writing
341
432
  --doctor Check install health without writing
@@ -361,6 +452,9 @@ function detectedClient(detection) {
361
452
  if (detection.openclaw) return 'openclaw';
362
453
  if (detection.claudeCode) return 'claude-code';
363
454
  if (detection.cursor) return 'cursor';
455
+ if (detection.cline) return 'cline';
456
+ if (detection.windsurf) return 'windsurf';
457
+ if (detection.gemini) return 'gemini';
364
458
  if (detection.codex) return 'codex';
365
459
  return 'custom';
366
460
  }
@@ -402,6 +496,14 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
402
496
  requirements: path.join(root, 'requirements.txt'),
403
497
  setupPy: path.join(root, 'setup.py'),
404
498
  claudeSettings: path.join(root, '.claude', 'settings.json'),
499
+ codexHooks: path.join(root, '.codex', 'hooks.json'),
500
+ cursorHooks: path.join(root, '.cursor', 'hooks.json'),
501
+ windsurfHooks: path.join(root, '.windsurf', 'hooks.json'),
502
+ geminiSettings: path.join(root, '.gemini', 'settings.json'),
503
+ grokHooks: path.join(home, '.grok', 'hooks', 'marrow.json'),
504
+ clinePreToolUseHook: path.join(root, '.clinerules', 'hooks', 'PreToolUse'),
505
+ clinePostToolUseHook: path.join(root, '.clinerules', 'hooks', 'PostToolUse'),
506
+ clineTaskCancelHook: path.join(root, '.clinerules', 'hooks', 'TaskCancel'),
405
507
  claudeMd: path.join(root, 'CLAUDE.md'),
406
508
  agentsMd: path.join(root, 'AGENTS.md'),
407
509
  cursorRules: path.join(root, '.cursor', 'rules', 'marrow.mdc'),
@@ -420,6 +522,10 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
420
522
  python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
421
523
  claudeCode: exists(paths.claudeSettings) || exists(paths.claudeMd),
422
524
  cursor: exists(path.join(root, '.cursor')),
525
+ cline: exists(path.join(root, '.clinerules')),
526
+ windsurf: exists(path.join(root, '.windsurf')),
527
+ gemini: exists(path.join(root, '.gemini')),
528
+ grok: exists(path.join(home, '.grok')) || exists(path.join(root, '.grok')),
423
529
  codex: exists(paths.agentsMd) || exists(path.join(root, '.codex')),
424
530
  mcpConfig: exists(paths.mcpJson) || exists(paths.cursorMcp) || exists(paths.claudeSettings),
425
531
  openclaw: exists(paths.openclawJson) || Boolean(env.OPENCLAW_HOME || env.OPENCLAW_AGENT_ID),
@@ -622,12 +728,12 @@ Marrow should run passively after install:
622
728
 
623
729
  - Use MCP plus these instructions in every workspace: \`npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup\`.
624
730
  - Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
625
- - Native Claude hooks install only when \`.claude\` is present. Cursor, Composer, Cline, and Windsurf get MCP tools on demand. Codex, Grok, Gemini, and similar CLI harnesses use the governed wrapper. Hermes, OpenClaw, and custom hosts need a bounded event adapter.
731
+ - Native Claude hooks install only when \`.claude\` is present. Codex native hooks install into \`.codex/hooks.json\`; Cursor and Composer use \`.cursor/hooks.json\`; Cline uses non-overwriting executable files under \`.clinerules/hooks/\`; Windsurf uses \`.windsurf/hooks.json\`; Gemini CLI uses \`.gemini/settings.json\`; Grok uses trusted global \`~/.grok/hooks/marrow.json\`. Restart the host, enable/review hooks, disable Windsurf Restricted Mode where native hooks are required, and trust the workspace before claiming runtime coverage. The governed wrapper remains an explicit bounded fallback. Hermes, OpenClaw, and custom hosts need a bounded event adapter.
626
732
  - Keep passive token/model usage proof enabled. Empty savings stay zero until observed model usage lands. Do not invent token, cost, or time savings.
627
733
  - Before risky work, use Marrow's decision brief or passive prompt hook.
628
734
  - After activate writes MCP or hooks, restart this harness, then run \`npx @getmarrow/install@latest doctor --self-test\`. Do not claim Marrow is live in a process that has not reloaded.
629
735
  - First turn: tell the owner the gate is live and empty savings are honest. The next deploy, merge, or publish must go through Marrow.
630
- - Cursor, Composer, Cline, and Windsurf: call \`marrow_agent_runtime\` before those actions. Codex, Grok, Gemini, and similar CLIs: wrap them with \`npx @getmarrow/install run --agent <agent-id> -- -- <command>\`.
736
+ - Cursor and Composer use configured native hooks after restart and \`/hooks\` trust review. Cline uses its configured PreToolUse, PostToolUse, and TaskCancel hooks only after Enable Hooks, restart, executable trust, and workspace trust; TaskComplete is not claimed as current coverage. Windsurf uses configured native pre-action, success-result, and response-closeout hooks only after restart, trust review, and leaving Restricted Mode. Gemini CLI uses configured BeforeTool, AfterTool, and AfterAgent hooks only after restart and project fingerprint review and approval in \`/hooks panel\`; explicit user disablement is preserved. Grok uses global native PreToolUse, PostToolUse/PostToolUseFailure, and one nonblocking Stop closeout only after restart and \`/hooks\` inspection; Grok hooks remain user-toggleable. MCP tools remain on demand. Codex uses configured native hooks after restart and \`/hooks\` trust review. The governed wrapper remains an explicit bounded fallback.
631
737
  - Before the session ends, close open work with session-end auto-commit or \`marrow_commit\`. Record model usage only when the host response includes counts.
632
738
  - After meaningful work, record the outcome so future agents learn from it.
633
739
  - After Marrow blocks, warns, or requires review, use the decision trace receipt to tell the operator what changed and which recorded workflow or proof is required. Stay quiet for routine low-risk work.
@@ -757,12 +863,12 @@ function exactHookDescriptors(settings, eventName, command, matcher) {
757
863
  function marrowHookSubcommand(command) {
758
864
  if (typeof command !== 'string') return null;
759
865
  const match = command.trim().match(
760
- /^npx\s+(?:-y\s+)?(?:--package=@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp|@getmarrow\/mcp(?:@[^\s]+)?)\s+(context-hook|pre-action-hook|hook|session-hook)$/,
866
+ /^npx\s+(?:-y\s+)?(?:--package=@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp|@getmarrow\/mcp(?:@[^\s]+)?)\s+(?:(?:claude|codex|cursor|grok)-)?(context-hook|pre-action-hook|hook|session-hook)$/,
761
867
  );
762
868
  return match?.[1] || null;
763
869
  }
764
870
 
765
- function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
871
+ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher, handlerOptions = {}) {
766
872
  const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
767
873
  let preferredHandler = null;
768
874
  const retained = [];
@@ -786,7 +892,7 @@ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, ma
786
892
  }
787
893
  if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
788
894
  }
789
- const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
895
+ const canonical = { hooks: [{ ...(preferredHandler || {}), ...handlerOptions, type: 'command', command }] };
790
896
  if (matcher != null) canonical.matcher = matcher;
791
897
  retained.push(canonical);
792
898
  return retained;
@@ -882,6 +988,439 @@ function upsertClaudeHooks(settingsPath) {
882
988
  return JSON.stringify(settings, null, 2) + '\n';
883
989
  }
884
990
 
991
+ function codexNativeHookFingerprint(settings) {
992
+ const contract = {
993
+ schema: 'marrow-codex-native-hooks.v1',
994
+ adapter_version: MCP_ADAPTER_VERSION,
995
+ expected_hooks: NATIVE_EXPECTED_HOOKS,
996
+ configured: {
997
+ prompt: exactHookConfigured(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
998
+ pre_action: exactHookConfigured(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
999
+ action_result: exactHookConfigured(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
1000
+ session_end: exactHookConfigured(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
1001
+ },
1002
+ descriptors: {
1003
+ prompt: exactHookDescriptors(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
1004
+ pre_action: exactHookDescriptors(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
1005
+ action_result: exactHookDescriptors(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
1006
+ session_end: exactHookDescriptors(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
1007
+ },
1008
+ active_marrow_handlers: {
1009
+ prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
1010
+ pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
1011
+ action_result: marrowHookDescriptors(settings, 'PostToolUse'),
1012
+ session_end: marrowHookDescriptors(settings, 'SessionEnd'),
1013
+ },
1014
+ };
1015
+ return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
1016
+ }
1017
+
1018
+ function upsertCodexHooks(hooksPath) {
1019
+ const settings = parseJsonObject(hooksPath);
1020
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1021
+ ? settings.hooks
1022
+ : {};
1023
+ settings.hooks = {
1024
+ ...hooks,
1025
+ UserPromptSubmit: reconcileMarrowCommandHook(
1026
+ settings, 'UserPromptSubmit', 'context-hook', CODEX_CONTEXT_HOOK_COMMAND, undefined,
1027
+ { timeout: CODEX_HOOK_TIMEOUT_SECONDS },
1028
+ ),
1029
+ PreToolUse: reconcileMarrowCommandHook(
1030
+ settings, 'PreToolUse', 'pre-action-hook', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
1031
+ { timeout: CODEX_HOOK_TIMEOUT_SECONDS, async: false },
1032
+ ),
1033
+ PostToolUse: reconcileMarrowCommandHook(
1034
+ settings, 'PostToolUse', 'hook', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
1035
+ { timeout: CODEX_HOOK_TIMEOUT_SECONDS },
1036
+ ),
1037
+ SessionEnd: reconcileMarrowCommandHook(
1038
+ settings, 'SessionEnd', 'session-hook', CODEX_SESSION_END_HOOK_COMMAND, undefined,
1039
+ { timeout: CODEX_SESSION_TIMEOUT_SECONDS },
1040
+ ),
1041
+ };
1042
+ return JSON.stringify(settings, null, 2) + '\n';
1043
+ }
1044
+
1045
+ function reconcileCursorHook(settings, eventName, subcommand, canonical) {
1046
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1047
+ const retained = original.filter((entry) => !(
1048
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1049
+ && marrowHookSubcommand(entry.command)
1050
+ ));
1051
+ return [...retained, canonical];
1052
+ }
1053
+
1054
+ function exactCursorHookConfigured(settings, eventName, command, matcher, required = {}) {
1055
+ const entries = settings?.hooks?.[eventName];
1056
+ if (!Array.isArray(entries)) return false;
1057
+ return entries.some((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
1058
+ && entry.command === command
1059
+ && (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
1060
+ && Object.entries(required).every(([key, value]) => entry[key] === value));
1061
+ }
1062
+
1063
+ function cursorHookDescriptors(settings, eventName) {
1064
+ const entries = settings?.hooks?.[eventName];
1065
+ if (!Array.isArray(entries)) return [];
1066
+ return entries.flatMap((entry) => {
1067
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !marrowHookSubcommand(entry.command)) return [];
1068
+ return [{
1069
+ matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
1070
+ command: String(entry.command).trim(),
1071
+ timeout: typeof entry.timeout === 'number' && Number.isFinite(entry.timeout) ? entry.timeout : null,
1072
+ failClosed: entry.failClosed === true,
1073
+ async: entry.async === false ? false : null,
1074
+ }];
1075
+ });
1076
+ }
1077
+
1078
+ function cursorNativeHookFingerprint(settings) {
1079
+ const contract = {
1080
+ schema: 'marrow-cursor-native-hooks.v1',
1081
+ adapter_version: MCP_ADAPTER_VERSION,
1082
+ expected_hooks: ['pre_action', 'action_result', 'outcome_closure'],
1083
+ configured: {
1084
+ pre_action: exactCursorHookConfigured(settings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1085
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1086
+ failClosed: true,
1087
+ async: false,
1088
+ }),
1089
+ action_result_success: exactCursorHookConfigured(settings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1090
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1091
+ }),
1092
+ action_result_failure: exactCursorHookConfigured(settings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1093
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1094
+ }),
1095
+ outcome_closure: exactCursorHookConfigured(settings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1096
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1097
+ }),
1098
+ },
1099
+ descriptors: {
1100
+ pre_action: cursorHookDescriptors(settings, 'preToolUse'),
1101
+ action_result_success: cursorHookDescriptors(settings, 'postToolUse'),
1102
+ action_result_failure: cursorHookDescriptors(settings, 'postToolUseFailure'),
1103
+ outcome_closure: cursorHookDescriptors(settings, 'stop'),
1104
+ },
1105
+ };
1106
+ return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
1107
+ }
1108
+
1109
+ function upsertCursorHooks(hooksPath) {
1110
+ const settings = parseJsonObject(hooksPath);
1111
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1112
+ ? settings.hooks
1113
+ : {};
1114
+ settings.version = 1;
1115
+ settings.hooks = {
1116
+ ...hooks,
1117
+ preToolUse: reconcileCursorHook(settings, 'preToolUse', 'pre-action-hook', {
1118
+ command: CURSOR_PRE_ACTION_HOOK_COMMAND,
1119
+ matcher: CURSOR_NATIVE_HOOK_MATCHER,
1120
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1121
+ failClosed: true,
1122
+ async: false,
1123
+ }),
1124
+ postToolUse: reconcileCursorHook(settings, 'postToolUse', 'hook', {
1125
+ command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
1126
+ matcher: CURSOR_NATIVE_HOOK_MATCHER,
1127
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1128
+ }),
1129
+ postToolUseFailure: reconcileCursorHook(settings, 'postToolUseFailure', 'hook', {
1130
+ command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
1131
+ matcher: CURSOR_NATIVE_HOOK_MATCHER,
1132
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1133
+ }),
1134
+ stop: reconcileCursorHook(settings, 'stop', 'session-hook', {
1135
+ command: CURSOR_SESSION_END_HOOK_COMMAND,
1136
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1137
+ }),
1138
+ };
1139
+ return JSON.stringify(settings, null, 2) + '\n';
1140
+ }
1141
+
1142
+ const WINDSURF_PRE_EVENTS = ['pre_write_code', 'pre_run_command', 'pre_mcp_tool_use'];
1143
+ const WINDSURF_POST_EVENTS = ['post_write_code', 'post_run_command', 'post_mcp_tool_use'];
1144
+
1145
+ function windsurfMarrowHookEntrypoint(command) {
1146
+ if (typeof command !== 'string') return null;
1147
+ const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+windsurf-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
1148
+ return match?.[1] || null;
1149
+ }
1150
+
1151
+ function reconcileWindsurfHook(settings, eventName, command) {
1152
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1153
+ const retained = original.filter((entry) => !(
1154
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1155
+ && windsurfMarrowHookEntrypoint(entry.command)
1156
+ ));
1157
+ return [...retained, { command, show_output: false }];
1158
+ }
1159
+
1160
+ function exactWindsurfHookConfigured(settings, eventName, command) {
1161
+ const entries = settings?.hooks?.[eventName];
1162
+ return Array.isArray(entries) && entries.some((entry) => (
1163
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1164
+ && entry.command === command
1165
+ && entry.show_output === false
1166
+ ));
1167
+ }
1168
+
1169
+ function windsurfNativeHookFingerprint(settings) {
1170
+ const configured = Object.fromEntries([
1171
+ ...WINDSURF_PRE_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND)]),
1172
+ ...WINDSURF_POST_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND)]),
1173
+ ['post_cascade_response', exactWindsurfHookConfigured(settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND)],
1174
+ ]);
1175
+ return crypto.createHash('sha256').update(JSON.stringify({
1176
+ schema: 'marrow-windsurf-native-hooks.v1',
1177
+ adapter_version: MCP_ADAPTER_VERSION,
1178
+ expected_hooks: ['pre_action', 'action_result', 'response_closeout'],
1179
+ restricted_mode_disables_hooks: true,
1180
+ configured,
1181
+ })).digest('hex');
1182
+ }
1183
+
1184
+ function upsertWindsurfHooks(hooksPath) {
1185
+ const settings = parseJsonObject(hooksPath);
1186
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1187
+ ? settings.hooks
1188
+ : {};
1189
+ settings.hooks = { ...hooks };
1190
+ for (const eventName of WINDSURF_PRE_EVENTS) {
1191
+ settings.hooks[eventName] = reconcileWindsurfHook(
1192
+ settings, eventName, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1193
+ );
1194
+ }
1195
+ for (const eventName of WINDSURF_POST_EVENTS) {
1196
+ settings.hooks[eventName] = reconcileWindsurfHook(
1197
+ settings, eventName, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1198
+ );
1199
+ }
1200
+ settings.hooks.post_cascade_response = reconcileWindsurfHook(
1201
+ settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1202
+ );
1203
+ return JSON.stringify(settings, null, 2) + '\n';
1204
+ }
1205
+
1206
+ function geminiMarrowHookEntrypoint(command) {
1207
+ if (typeof command !== 'string') return null;
1208
+ const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+gemini-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
1209
+ return match?.[1] || null;
1210
+ }
1211
+
1212
+ function reconcileGeminiHook(settings, eventName, canonical) {
1213
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1214
+ const retained = [];
1215
+ for (const group of original) {
1216
+ if (!group || typeof group !== 'object' || Array.isArray(group) || !Array.isArray(group.hooks)) {
1217
+ retained.push(group);
1218
+ continue;
1219
+ }
1220
+ const hooks = group.hooks.filter((handler) => !(
1221
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1222
+ && (String(handler.name || '').startsWith('marrow-') || geminiMarrowHookEntrypoint(handler.command))
1223
+ ));
1224
+ if (hooks.length > 0) retained.push({ ...group, hooks });
1225
+ }
1226
+ return [...retained, canonical];
1227
+ }
1228
+
1229
+ function exactGeminiHookConfigured(settings, eventName, name, command, matcher, timeout) {
1230
+ const groups = settings?.hooks?.[eventName];
1231
+ if (!Array.isArray(groups)) return false;
1232
+ return groups.some((group) => (
1233
+ group && typeof group === 'object' && !Array.isArray(group)
1234
+ && (matcher === undefined ? group.matcher === undefined : group.matcher === matcher)
1235
+ && Array.isArray(group.hooks)
1236
+ && group.hooks.some((handler) => (
1237
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1238
+ && handler.name === name
1239
+ && handler.type === 'command'
1240
+ && handler.command === command
1241
+ && handler.timeout === timeout
1242
+ ))
1243
+ ));
1244
+ }
1245
+
1246
+ function geminiHooksExplicitlyDisabled(settings) {
1247
+ return settings?.hooksConfig?.enabled === false;
1248
+ }
1249
+
1250
+ function geminiNativeHookFingerprint(settings) {
1251
+ return crypto.createHash('sha256').update(JSON.stringify({
1252
+ schema: 'marrow-gemini-native-hooks.v1',
1253
+ adapter_version: MCP_ADAPTER_VERSION,
1254
+ expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
1255
+ explicitly_enabled: settings?.hooksConfig?.enabled === true,
1256
+ explicitly_disabled: geminiHooksExplicitlyDisabled(settings),
1257
+ configured: {
1258
+ pre_action: exactGeminiHookConfigured(
1259
+ settings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1260
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1261
+ ),
1262
+ action_result: exactGeminiHookConfigured(
1263
+ settings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1264
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1265
+ ),
1266
+ turn_closeout: exactGeminiHookConfigured(
1267
+ settings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1268
+ undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1269
+ ),
1270
+ },
1271
+ session_end_claimed: false,
1272
+ })).digest('hex');
1273
+ }
1274
+
1275
+ function exactGrokHookConfigured(settings, eventName, command, matcher, timeout) {
1276
+ const entries = settings?.hooks?.[eventName];
1277
+ if (!Array.isArray(entries)) return false;
1278
+ return entries.some((entry) => (
1279
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1280
+ && (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
1281
+ && Array.isArray(entry.hooks)
1282
+ && entry.hooks.some((handler) => (
1283
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1284
+ && handler.type === 'command'
1285
+ && handler.command === command
1286
+ && handler.timeout === timeout
1287
+ ))
1288
+ ));
1289
+ }
1290
+
1291
+ function grokHasDuplicateSessionEnd(settings) {
1292
+ const entries = settings?.hooks?.SessionEnd;
1293
+ if (!Array.isArray(entries)) return false;
1294
+ return entries.some((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((handler) => (
1295
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1296
+ && typeof handler.command === 'string'
1297
+ && /marrow-mcp\s+grok-session-hook(?:\s|['"]|$)/.test(handler.command)
1298
+ )));
1299
+ }
1300
+
1301
+ function grokNativeHookFingerprint(settings) {
1302
+ return crypto.createHash('sha256').update(JSON.stringify({
1303
+ schema: 'marrow-grok-native-hooks.v1',
1304
+ adapter_version: MCP_ADAPTER_VERSION,
1305
+ expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
1306
+ configured: {
1307
+ context: exactGrokHookConfigured(settings, 'UserPromptSubmit', GROK_CONTEXT_HOOK_COMMAND, undefined, 5),
1308
+ pre_action: exactGrokHookConfigured(settings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7),
1309
+ action_result_success: exactGrokHookConfigured(settings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1310
+ action_result_failure: exactGrokHookConfigured(settings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1311
+ turn_closeout: exactGrokHookConfigured(settings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3),
1312
+ duplicate_session_end: grokHasDuplicateSessionEnd(settings),
1313
+ },
1314
+ })).digest('hex');
1315
+ }
1316
+
1317
+ function upsertGeminiHooks(settingsPath) {
1318
+ const settings = parseJsonObject(settingsPath);
1319
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1320
+ ? settings.hooks
1321
+ : {};
1322
+ settings.hooks = {
1323
+ ...hooks,
1324
+ BeforeTool: reconcileGeminiHook(settings, 'BeforeTool', {
1325
+ matcher: GEMINI_NATIVE_HOOK_MATCHER,
1326
+ hooks: [{
1327
+ name: 'marrow-before-tool',
1328
+ type: 'command',
1329
+ command: GEMINI_PRE_ACTION_HOOK_COMMAND,
1330
+ timeout: GEMINI_HOOK_TIMEOUT_MS,
1331
+ }],
1332
+ }),
1333
+ AfterTool: reconcileGeminiHook(settings, 'AfterTool', {
1334
+ matcher: GEMINI_NATIVE_HOOK_MATCHER,
1335
+ hooks: [{
1336
+ name: 'marrow-after-tool',
1337
+ type: 'command',
1338
+ command: GEMINI_ACTION_RESULT_HOOK_COMMAND,
1339
+ timeout: GEMINI_HOOK_TIMEOUT_MS,
1340
+ }],
1341
+ }),
1342
+ AfterAgent: reconcileGeminiHook(settings, 'AfterAgent', {
1343
+ hooks: [{
1344
+ name: 'marrow-after-agent',
1345
+ type: 'command',
1346
+ command: GEMINI_SESSION_END_HOOK_COMMAND,
1347
+ timeout: GEMINI_CLOSEOUT_TIMEOUT_MS,
1348
+ }],
1349
+ }),
1350
+ };
1351
+ return JSON.stringify(settings, null, 2) + '\n';
1352
+ }
1353
+
1354
+ function clinePreToolUseHookSource() {
1355
+ return `#!/bin/sh
1356
+ output="$(npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook 2>/dev/null)" || output=""
1357
+ if [ -n "$output" ]; then
1358
+ validated="$(printf '%s' "$output" | NODE_OPTIONS= node -e 'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>{try{const v=JSON.parse(s);const keys=Object.keys(v).sort();const allow=v.cancel===false&&keys.length===1&&keys[0]==="cancel";const deny=v.cancel===true&&typeof v.errorMessage==="string"&&v.errorMessage.length>0&&v.errorMessage.length<=500&&keys.length===2&&keys[0]==="cancel"&&keys[1]==="errorMessage";if(!allow&&!deny)process.exit(1);process.stdout.write(JSON.stringify(v));}catch{process.exit(1);}});' 2>/dev/null)" || validated=""
1359
+ if [ -n "$validated" ]; then
1360
+ printf '%s\n' "$validated"
1361
+ exit 0
1362
+ fi
1363
+ fi
1364
+ printf '%s\n' '{"cancel":true,"errorMessage":"Marrow governance did not return a valid decision. Restore trusted configuration and retry."}'
1365
+ exit 0
1366
+ `;
1367
+ }
1368
+
1369
+ function clineTelemetryHookSource(entrypoint) {
1370
+ return `#!/bin/sh
1371
+ npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp ${entrypoint} >/dev/null 2>&1 || :
1372
+ exit 0
1373
+ `;
1374
+ }
1375
+
1376
+ function clineHookContract(detection) {
1377
+ return [
1378
+ {
1379
+ stage: 'pre_action',
1380
+ path: detection.paths.clinePreToolUseHook,
1381
+ label: 'Cline PreToolUse native hook',
1382
+ content: clinePreToolUseHookSource(),
1383
+ },
1384
+ {
1385
+ stage: 'action_result',
1386
+ path: detection.paths.clinePostToolUseHook,
1387
+ label: 'Cline PostToolUse native hook',
1388
+ content: clineTelemetryHookSource('cline-hook'),
1389
+ },
1390
+ {
1391
+ stage: 'cancel_closeout',
1392
+ path: detection.paths.clineTaskCancelHook,
1393
+ label: 'Cline TaskCancel native hook',
1394
+ content: clineTelemetryHookSource('cline-session-hook'),
1395
+ },
1396
+ ];
1397
+ }
1398
+
1399
+ function exactExecutableFile(filePath, content) {
1400
+ try {
1401
+ const stat = fs.lstatSync(filePath);
1402
+ return stat.isFile() && !stat.isSymbolicLink() && (stat.mode & 0o111) !== 0 && safeRead(filePath) === content;
1403
+ } catch {
1404
+ return false;
1405
+ }
1406
+ }
1407
+
1408
+ function clineNativeHookFingerprint(detection) {
1409
+ const hooks = clineHookContract(detection).map((hook) => ({
1410
+ stage: hook.stage,
1411
+ configured: exactExecutableFile(hook.path, hook.content),
1412
+ content_sha256: exactExecutableFile(hook.path, hook.content)
1413
+ ? crypto.createHash('sha256').update(hook.content).digest('hex')
1414
+ : null,
1415
+ }));
1416
+ return crypto.createHash('sha256').update(JSON.stringify({
1417
+ schema: 'marrow-cline-native-hooks.v1',
1418
+ adapter_version: MCP_ADAPTER_VERSION,
1419
+ task_complete_support: 'coming_soon_not_configured',
1420
+ hooks,
1421
+ })).digest('hex');
1422
+ }
1423
+
885
1424
  function activationProfile(detection, plan, changes, client) {
886
1425
  const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
887
1426
  || HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
@@ -899,15 +1438,78 @@ function activationProfile(detection, plan, changes, client) {
899
1438
  : INSTALLER_ADAPTER_VERSION;
900
1439
  const observedHooks = [];
901
1440
  const claudeSettings = safeJsonObject(detection.paths.claudeSettings);
902
- if (capabilityLevel === 'native_hooks'
903
- && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
904
- if (capabilityLevel === 'native_hooks'
905
- && exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
906
- if (capabilityLevel === 'native_hooks'
907
- && exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
908
- && exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
909
- if (capabilityLevel === 'native_hooks'
910
- && exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1441
+ const codexSettings = safeJsonObject(detection.paths.codexHooks);
1442
+ const cursorSettings = safeJsonObject(detection.paths.cursorHooks);
1443
+ const windsurfSettings = safeJsonObject(detection.paths.windsurfHooks);
1444
+ const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
1445
+ const grokSettings = safeJsonObject(detection.paths.grokHooks);
1446
+ if (client === 'codex') {
1447
+ if (exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1448
+ if (exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1449
+ if (exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1450
+ if (exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1451
+ } else if (client === 'cursor' || client === 'composer') {
1452
+ if (exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1453
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
1454
+ })) observedHooks.push('pre_action');
1455
+ if (exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1456
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1457
+ }) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1458
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1459
+ })) observedHooks.push('action_result');
1460
+ if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1461
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1462
+ })) observedHooks.push('outcome_closure');
1463
+ } else if (client === 'cline') {
1464
+ for (const hook of clineHookContract(detection)) {
1465
+ if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
1466
+ }
1467
+ } else if (client === 'windsurf') {
1468
+ if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
1469
+ windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1470
+ ))) observedHooks.push('pre_action');
1471
+ if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
1472
+ windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1473
+ ))) observedHooks.push('action_result');
1474
+ if (exactWindsurfHookConfigured(
1475
+ windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1476
+ )) observedHooks.push('response_closeout');
1477
+ } else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
1478
+ if (exactGeminiHookConfigured(
1479
+ geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1480
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1481
+ )) observedHooks.push('pre_action');
1482
+ if (exactGeminiHookConfigured(
1483
+ geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1484
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1485
+ )) observedHooks.push('action_result');
1486
+ if (exactGeminiHookConfigured(
1487
+ geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1488
+ undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1489
+ )) observedHooks.push('turn_closeout');
1490
+ } else if (client === 'grok') {
1491
+ if (exactGrokHookConfigured(
1492
+ grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
1493
+ )) observedHooks.push('pre_action');
1494
+ if (exactGrokHookConfigured(
1495
+ grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1496
+ ) && exactGrokHookConfigured(
1497
+ grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1498
+ )) observedHooks.push('action_result');
1499
+ if (exactGrokHookConfigured(
1500
+ grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
1501
+ ) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
1502
+ } else {
1503
+ if (capabilityLevel === 'native_hooks'
1504
+ && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1505
+ if (capabilityLevel === 'native_hooks'
1506
+ && exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1507
+ if (capabilityLevel === 'native_hooks'
1508
+ && exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
1509
+ && exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1510
+ if (capabilityLevel === 'native_hooks'
1511
+ && exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1512
+ }
911
1513
  const passiveRuntime = safeRead(detection.paths.passiveRuntime);
912
1514
  if (capabilityLevel === 'sdk_passive_runtime'
913
1515
  && sdkDependency.present
@@ -930,17 +1532,40 @@ function activationProfile(detection, plan, changes, client) {
930
1532
  .sort()
931
1533
  .join('|');
932
1534
  const configFingerprint = capabilityLevel === 'native_hooks'
933
- ? claudeNativeHookFingerprint(claudeSettings)
1535
+ ? client === 'codex' ? codexNativeHookFingerprint(codexSettings)
1536
+ : client === 'cursor' || client === 'composer' ? cursorNativeHookFingerprint(cursorSettings)
1537
+ : client === 'cline' ? clineNativeHookFingerprint(detection)
1538
+ : client === 'windsurf' ? windsurfNativeHookFingerprint(windsurfSettings)
1539
+ : client === 'gemini' ? geminiNativeHookFingerprint(geminiSettings)
1540
+ : client === 'grok' ? grokNativeHookFingerprint(grokSettings)
1541
+ : claudeNativeHookFingerprint(claudeSettings)
934
1542
  : crypto.createHash('sha256')
935
1543
  .update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
936
1544
  .digest('hex');
937
1545
  const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
1546
+ const clineConflicts = changes
1547
+ .filter((change) => change.hook_conflict)
1548
+ .map((change) => change.label);
938
1549
  const exactFix = complete
939
- ? null
1550
+ ? client === 'cline'
1551
+ ? 'Enable Hooks in Cline, trust the project hook executables and workspace, then restart Cline. TaskComplete remains unverified and is not configured.'
1552
+ : client === 'windsurf'
1553
+ ? 'Restart Windsurf, trust the workspace hook configuration, and leave Restricted Mode before expecting hooks to run. MCP tools remain on demand.'
1554
+ : client === 'gemini'
1555
+ ? 'Restart Gemini CLI, open /hooks panel, and review and approve the project hook fingerprints. MCP tools remain on demand.'
1556
+ : client === 'grok'
1557
+ ? 'Restart Grok, inspect the installed global hooks with /hooks, and confirm they are enabled. Configuration remains client-self-reported and does not verify observed coverage.'
1558
+ : null
940
1559
  : capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
941
1560
  ? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
942
1561
  : capabilityLevel === 'governed_wrapper'
943
1562
  ? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
1563
+ : client === 'cline' && clineConflicts.length > 0
1564
+ ? 'Move or remove the conflicting owner-managed Cline hook file after owner review, then run npx @getmarrow/install --repair. Marrow will never overwrite or compose it.'
1565
+ : client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
1566
+ ? 'Hooks are explicitly disabled. After owner review, run /hooks enable-all, open /hooks panel, review and approve the project hook fingerprints, then restart Gemini CLI.'
1567
+ : client === 'grok'
1568
+ ? `Run npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
944
1569
  : 'npx @getmarrow/install --repair';
945
1570
  return {
946
1571
  adapter_version: adapterVersion,
@@ -950,9 +1575,42 @@ function activationProfile(detection, plan, changes, client) {
950
1575
  observed_hooks: observedHooks,
951
1576
  evidence_authority: 'client_self_reported',
952
1577
  coverage_verified: false,
1578
+ passive_live: false,
953
1579
  configuration_complete: complete,
954
1580
  complete,
955
1581
  exact_fix: exactFix,
1582
+ ...(client === 'cline' ? {
1583
+ hook_conflicts: clineConflicts,
1584
+ task_complete_support: 'coming_soon_not_configured',
1585
+ task_completion_closure_verified: false,
1586
+ enable_hooks_required: true,
1587
+ executable_trust_required: true,
1588
+ } : {}),
1589
+ ...(client === 'windsurf' ? {
1590
+ restricted_mode_disables_hooks: true,
1591
+ restart_required: true,
1592
+ workspace_trust_required: true,
1593
+ mcp_tools: 'on_demand',
1594
+ } : {}),
1595
+ ...(client === 'gemini' ? {
1596
+ hooks_enabled: !geminiHooksExplicitlyDisabled(geminiSettings),
1597
+ explicit_disable_preserved: geminiHooksExplicitlyDisabled(geminiSettings),
1598
+ trust_review_required: true,
1599
+ restart_required: true,
1600
+ mcp_tools: 'on_demand',
1601
+ session_end_delivery_claimed: false,
1602
+ deterministic_closeout: 'AfterAgent',
1603
+ } : {}),
1604
+ ...(client === 'grok' ? {
1605
+ hooks_user_toggleable: true,
1606
+ hook_review_required: true,
1607
+ restart_required: true,
1608
+ global_hook_path: detection.paths.grokHooks,
1609
+ mcp_tools: 'on_demand',
1610
+ duplicate_session_end_configured: grokHasDuplicateSessionEnd(grokSettings),
1611
+ deterministic_closeout: 'Stop',
1612
+ governed_wrapper_fallback: 'explicit_bounded_only',
1613
+ } : {}),
956
1614
  };
957
1615
  }
958
1616
 
@@ -1075,7 +1733,74 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
1075
1733
  const detected = detectedClient(detection) === entry.client
1076
1734
  || (entry.client === 'claude-code' && detection.claudeCode)
1077
1735
  || (entry.client === 'cursor' && detection.cursor)
1736
+ || (entry.client === 'cline' && detection.cline)
1737
+ || (entry.client === 'windsurf' && detection.windsurf)
1738
+ || (entry.client === 'gemini' && detection.gemini)
1739
+ || (entry.client === 'grok' && detection.grok)
1078
1740
  || (entry.client === 'codex' && detection.codex);
1741
+ const codexSettings = entry.client === 'codex' ? safeJsonObject(detection.paths.codexHooks) : null;
1742
+ const codexConfigured = Boolean(codexSettings
1743
+ && exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)
1744
+ && exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
1745
+ && exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
1746
+ && exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND));
1747
+ const cursorSettings = ['cursor', 'composer'].includes(entry.client) ? safeJsonObject(detection.paths.cursorHooks) : null;
1748
+ const cursorConfigured = Boolean(cursorSettings
1749
+ && exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1750
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
1751
+ })
1752
+ && exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1753
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1754
+ })
1755
+ && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1756
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1757
+ })
1758
+ && exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1759
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1760
+ }));
1761
+ const clineConfigured = entry.client === 'cline'
1762
+ && clineHookContract(detection).every((hook) => exactExecutableFile(hook.path, hook.content));
1763
+ const windsurfSettings = entry.client === 'windsurf' ? safeJsonObject(detection.paths.windsurfHooks) : null;
1764
+ const windsurfConfigured = Boolean(windsurfSettings
1765
+ && WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
1766
+ windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1767
+ ))
1768
+ && WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
1769
+ windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1770
+ ))
1771
+ && exactWindsurfHookConfigured(
1772
+ windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1773
+ ));
1774
+ const geminiSettings = entry.client === 'gemini' ? safeJsonObject(detection.paths.geminiSettings) : null;
1775
+ const geminiConfigured = Boolean(geminiSettings
1776
+ && !geminiHooksExplicitlyDisabled(geminiSettings)
1777
+ && exactGeminiHookConfigured(
1778
+ geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1779
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1780
+ )
1781
+ && exactGeminiHookConfigured(
1782
+ geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1783
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1784
+ )
1785
+ && exactGeminiHookConfigured(
1786
+ geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1787
+ undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1788
+ ));
1789
+ const grokSettings = entry.client === 'grok' ? safeJsonObject(detection.paths.grokHooks) : null;
1790
+ const grokConfigured = Boolean(grokSettings
1791
+ && exactGrokHookConfigured(
1792
+ grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
1793
+ )
1794
+ && exactGrokHookConfigured(
1795
+ grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1796
+ )
1797
+ && exactGrokHookConfigured(
1798
+ grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1799
+ )
1800
+ && exactGrokHookConfigured(
1801
+ grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
1802
+ )
1803
+ && !grokHasDuplicateSessionEnd(grokSettings));
1079
1804
  return {
1080
1805
  client: entry.client,
1081
1806
  capability_level: entry.capability_level,
@@ -1085,12 +1810,35 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
1085
1810
  mcp: true,
1086
1811
  instructions: true,
1087
1812
  sdk_passive_runtime: node,
1088
- native_hooks: entry.capability_level === 'native_hooks' && Boolean(detection.claudeCode),
1813
+ native_hooks: entry.capability_level === 'native_hooks' && Boolean(
1814
+ entry.client === 'claude-code' ? detection.claudeCode
1815
+ : entry.client === 'codex' ? detection.codex
1816
+ : ['cursor', 'composer'].includes(entry.client) ? detection.cursor
1817
+ : entry.client === 'cline' ? detection.cline
1818
+ : entry.client === 'windsurf' ? detection.windsurf
1819
+ : entry.client === 'gemini' ? detection.gemini
1820
+ : entry.client === 'grok' ? detection.grok
1821
+ : false,
1822
+ ),
1089
1823
  governed_wrapper: entry.capability_level === 'governed_wrapper',
1090
1824
  },
1091
- configured_locally: detected && entry.automatic.length > 0,
1825
+ configured_locally: entry.client === 'codex' ? codexConfigured
1826
+ : ['cursor', 'composer'].includes(entry.client) ? cursorConfigured
1827
+ : entry.client === 'cline' ? clineConfigured
1828
+ : entry.client === 'windsurf' ? windsurfConfigured
1829
+ : entry.client === 'gemini' ? geminiConfigured
1830
+ : entry.client === 'grok' ? grokConfigured
1831
+ : detected && entry.automatic.length > 0,
1092
1832
  verified_passive: false,
1093
- unsupported_claim: entry.capability_level === 'event_contract'
1833
+ unsupported_claim: entry.client === 'cline'
1834
+ ? 'TaskComplete is documented as coming soon and is not configured or counted as observed coverage.'
1835
+ : entry.client === 'windsurf'
1836
+ ? 'Restricted Mode disables hooks; configuration requires restart and trust review and never verifies passive coverage.'
1837
+ : entry.client === 'gemini'
1838
+ ? 'Project hooks require restart and project fingerprint review and approval in /hooks panel; explicit hooksConfig.enabled=false is preserved and SessionEnd delivery is not claimed.'
1839
+ : entry.client === 'grok'
1840
+ ? 'Global hooks are user-toggleable; restart and /hooks inspection are required, configuration remains client-self-reported, and duplicate SessionEnd closeout is forbidden.'
1841
+ : entry.capability_level === 'event_contract'
1094
1842
  ? 'Needs a bounded event adapter. MCP tools remain on demand.'
1095
1843
  : null,
1096
1844
  };
@@ -1131,6 +1879,42 @@ function buildPlan(detection, options) {
1131
1879
  transform: upsertClaudeHooks,
1132
1880
  });
1133
1881
  }
1882
+ if (detection.codex) {
1883
+ writes.push({
1884
+ type: 'json-transform',
1885
+ path: detection.paths.codexHooks,
1886
+ label: 'Codex native hooks',
1887
+ transform: upsertCodexHooks,
1888
+ });
1889
+ }
1890
+ if (detection.cline) {
1891
+ for (const hook of clineHookContract(detection)) {
1892
+ writes.push({
1893
+ type: 'owned-executable',
1894
+ path: hook.path,
1895
+ label: hook.label,
1896
+ content: hook.content,
1897
+ mode: 0o755,
1898
+ conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
1899
+ });
1900
+ }
1901
+ }
1902
+ if (detection.windsurf) {
1903
+ writes.push({
1904
+ type: 'json-transform',
1905
+ path: detection.paths.windsurfHooks,
1906
+ label: 'Windsurf native hooks',
1907
+ transform: upsertWindsurfHooks,
1908
+ });
1909
+ }
1910
+ if (detection.gemini) {
1911
+ writes.push({
1912
+ type: 'json-transform',
1913
+ path: detection.paths.geminiSettings,
1914
+ label: 'Gemini CLI native hooks',
1915
+ transform: upsertGeminiHooks,
1916
+ });
1917
+ }
1134
1918
  writes.push({
1135
1919
  type: 'json-transform',
1136
1920
  path: detection.paths.mcpJson,
@@ -1138,6 +1922,12 @@ function buildPlan(detection, options) {
1138
1922
  transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
1139
1923
  });
1140
1924
  if (detection.cursor) {
1925
+ writes.push({
1926
+ type: 'json-transform',
1927
+ path: detection.paths.cursorHooks,
1928
+ label: 'Cursor native hooks',
1929
+ transform: upsertCursorHooks,
1930
+ });
1141
1931
  writes.push({
1142
1932
  type: 'json-transform',
1143
1933
  path: detection.paths.cursorMcp,
@@ -1229,8 +2019,10 @@ function applyPlan(plan, options) {
1229
2019
  const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
1230
2020
  for (const write of plan.writes) assertContainedManagedTarget(root, write.path);
1231
2021
  const prepared = plan.writes.map((write) => {
2022
+ const fileExists = exists(write.path);
1232
2023
  const before = safeRead(write.path);
1233
2024
  let after;
2025
+ let hookConflict = false;
1234
2026
  if (write.type === 'file') {
1235
2027
  if (write.overwrite === false && before) {
1236
2028
  after = before;
@@ -1241,27 +2033,43 @@ function applyPlan(plan, options) {
1241
2033
  after = upsertBlock(before, write.block);
1242
2034
  } else if (write.type === 'json-transform') {
1243
2035
  after = write.transform(write.path);
2036
+ } else if (write.type === 'owned-executable') {
2037
+ if (fileExists && before !== write.content) {
2038
+ after = before;
2039
+ hookConflict = true;
2040
+ } else {
2041
+ after = write.content;
2042
+ }
1244
2043
  } else {
1245
2044
  throw new Error(`Unknown write type: ${write.type}`);
1246
2045
  }
1247
2046
 
1248
- return { write, before, after };
2047
+ const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
2048
+ const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
2049
+ return { write, before, after, hookConflict, modeChanged };
1249
2050
  });
1250
2051
 
1251
2052
  const changes = [];
1252
- for (const { write, before, after } of prepared) {
1253
- const changed = before !== after;
1254
- const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor);
2053
+ for (const { write, before, after, hookConflict, modeChanged } of prepared) {
2054
+ const contentChanged = before !== after;
2055
+ const changed = !hookConflict && (contentChanged || modeChanged);
2056
+ const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor && !hookConflict);
1255
2057
  changes.push({
1256
2058
  path: write.path,
1257
2059
  label: write.label,
1258
2060
  changed,
1259
2061
  applied: changed && writeApplied,
1260
- already_present: !changed,
2062
+ already_present: !changed && !hookConflict,
2063
+ hook_conflict: hookConflict,
2064
+ ...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
1261
2065
  });
1262
- if (changed && writeApplied) {
2066
+ if (contentChanged && writeApplied) {
1263
2067
  atomicWriteManagedFile(root, write.path, after);
1264
2068
  }
2069
+ if (modeChanged && writeApplied) {
2070
+ assertContainedManagedTarget(root, write.path);
2071
+ fs.chmodSync(write.path, write.mode);
2072
+ }
1265
2073
  }
1266
2074
  return changes;
1267
2075
  }
@@ -1836,6 +2644,9 @@ async function install(options) {
1836
2644
  if (options.activate && options.selfTest === false) {
1837
2645
  throw new Error('activate requires the server self-test');
1838
2646
  }
2647
+ if (options.activate && !String(options.apiKey || '').trim()) {
2648
+ throw new Error('activation requires MARROW_API_KEY from the process environment or trusted secret storage');
2649
+ }
1839
2650
  if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
1840
2651
  throw new Error('repair requires explicit write authorization (--yes)');
1841
2652
  }
@@ -1860,6 +2671,7 @@ async function install(options) {
1860
2671
  configuration_complete: changes.every((change) => change.applied || change.already_present),
1861
2672
  evidence_authority: 'client_self_reported',
1862
2673
  coverage_verified: false,
2674
+ passive_live: false,
1863
2675
  adapter_version: profile.adapter_version,
1864
2676
  capability_level: profile.capability_level,
1865
2677
  config_fingerprint: profile.config_fingerprint,
@@ -1892,12 +2704,14 @@ async function install(options) {
1892
2704
  const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
1893
2705
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
1894
2706
  const controllerPlatform = options.controllerPlatform || process.platform;
2707
+ const localControl = localControlEvidence({ apiKey: options.apiKey, home: options.controlHome });
1895
2708
  let controller = await controllerStatus({
1896
2709
  root: detection.root,
1897
2710
  agentId: options.agentId,
1898
2711
  platform: controllerPlatform,
1899
2712
  });
1900
2713
  const shouldEnsureController = options.controller !== false
2714
+ && localControl.enabled
1901
2715
  && controllerSupportedPlatform(controllerPlatform)
1902
2716
  && Boolean(options.apiKey)
1903
2717
  && selfTestPassed
@@ -1953,6 +2767,7 @@ async function install(options) {
1953
2767
  python: detection.python,
1954
2768
  claudeCode: detection.claudeCode,
1955
2769
  cursor: detection.cursor,
2770
+ grok: detection.grok,
1956
2771
  codex: detection.codex,
1957
2772
  openclaw: detection.openclaw,
1958
2773
  mcpConfig: detection.mcpConfig,
@@ -1991,6 +2806,7 @@ async function install(options) {
1991
2806
  configRepairs,
1992
2807
  sdkDependency,
1993
2808
  controller,
2809
+ local_control: localControl,
1994
2810
  selfTest,
1995
2811
  warnings: [
1996
2812
  ...(options.keyFromArg
@@ -2038,6 +2854,17 @@ module.exports = {
2038
2854
  stableAgentId,
2039
2855
  activationProfile,
2040
2856
  claudeNativeHookFingerprint,
2857
+ codexNativeHookFingerprint,
2858
+ cursorNativeHookFingerprint,
2859
+ clineNativeHookFingerprint,
2860
+ windsurfNativeHookFingerprint,
2861
+ geminiNativeHookFingerprint,
2862
+ grokNativeHookFingerprint,
2863
+ GROK_CONTEXT_HOOK_COMMAND,
2864
+ GROK_PRE_ACTION_HOOK_COMMAND,
2865
+ GROK_ACTION_RESULT_HOOK_COMMAND,
2866
+ GROK_SESSION_END_HOOK_COMMAND,
2867
+ GROK_NATIVE_HOOK_MATCHER,
2041
2868
  printReport,
2042
2869
  ADAPTER_PROVENANCE,
2043
2870
  HARNESS_CAPABILITY_REGISTRY,