@getmarrow/install 0.1.52 → 0.1.54

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,14 @@ 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.77';
14
+ const MCP_ADAPTER_SOURCE_SHA = '1e782d8ba6bbb54bfaa322f0300565c7176f1969';
15
+ const MCP_ADAPTER_INTEGRITY = 'sha512-4SVvnyTaDh/mhVUry1LaGCYKXSB/mt5NCHijIOLP336jILSqe7ezjZ5/EwEOMTQ2JvLm6w0QE/jmGbcksqe86w==';
14
16
  const SDK_ADAPTER_VERSION = '3.7.62';
15
17
  const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
16
18
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
@@ -20,8 +22,8 @@ const ADAPTER_PROVENANCE = Object.freeze({
20
22
  package: '@getmarrow/mcp',
21
23
  version: MCP_ADAPTER_VERSION,
22
24
  source_sha: MCP_ADAPTER_SOURCE_SHA,
23
- integrity: null,
24
- integrity_state: 'registry_unavailable_until_publish',
25
+ integrity: MCP_ADAPTER_INTEGRITY,
26
+ integrity_state: 'sealed_local_candidate',
25
27
  }),
26
28
  sdk: Object.freeze({
27
29
  package: '@getmarrow/sdk',
@@ -33,21 +35,97 @@ const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mc
33
35
  const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
34
36
  const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
35
37
  const MCP_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp session-hook`;
38
+ const CODEX_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-context-hook`;
39
+ const CODEX_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-pre-action-hook`;
40
+ const CODEX_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-hook`;
41
+ const CODEX_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-session-hook`;
42
+ const CURSOR_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-pre-action-hook`;
43
+ const CURSOR_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-hook`;
44
+ const CURSOR_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-session-hook`;
45
+ const CLINE_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook`;
46
+ const CLINE_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-hook`;
47
+ const CLINE_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-session-hook`;
48
+ const WINDSURF_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-pre-action-hook`;
49
+ const WINDSURF_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-hook`;
50
+ const WINDSURF_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-session-hook`;
51
+ const WINDSURF_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
52
+ 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'`;
53
+ const WINDSURF_ACTION_RESULT_HOOK_COMMAND = `sh -c '${WINDSURF_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
54
+ const WINDSURF_SESSION_END_HOOK_COMMAND = `sh -c '${WINDSURF_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
55
+ const GEMINI_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-pre-action-hook`;
56
+ const GEMINI_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-hook`;
57
+ const GEMINI_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-session-hook`;
58
+ const GEMINI_FIXED_DENIAL = 'Marrow blocked this action because required governance approval or proof is unavailable.';
59
+ const GEMINI_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
60
+ 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'`;
61
+ const GEMINI_ACTION_RESULT_HOOK_COMMAND = `sh -c '${GEMINI_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
62
+ const GEMINI_SESSION_END_HOOK_COMMAND = `sh -c '${GEMINI_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
63
+ const GROK_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-context-hook`;
64
+ const GROK_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-hook`;
65
+ const GROK_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-session-hook`;
66
+ const GROK_FIXED_DENIAL = 'Marrow blocked this protected action.';
67
+ const GROK_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
68
+ const GROK_PRE_ACTION_GUARD_SOURCE = [
69
+ 'const {spawn}=require("node:child_process");',
70
+ `const valid=new Set([${JSON.stringify('{"decision":"allow"}')},${JSON.stringify(`{"decision":"deny","reason":"${GROK_FIXED_DENIAL}"}`)}]);`,
71
+ 'let child=null,timer=null,done=false,input=[],inputBytes=0,output="",outputBytes=0;',
72
+ `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();};`,
73
+ 'process.stdin.on("error",fail);',
74
+ 'process.stdin.on("data",chunk=>{const value=Buffer.from(chunk);inputBytes+=value.length;if(inputBytes>65536){fail();return;}input.push(value);});',
75
+ 'process.stdin.on("end",()=>{if(done)return;try{',
76
+ `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"]});`,
77
+ 'timer=setTimeout(fail,5000);',
78
+ 'child.stdout.on("data",chunk=>{if(done)return;outputBytes+=chunk.length;if(outputBytes>512){fail();return;}output+=chunk.toString("utf8");});',
79
+ 'child.on("error",fail);child.stdin.on("error",fail);',
80
+ '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);});',
81
+ 'child.stdin.end(Buffer.concat(input));}catch{fail();}});',
82
+ ].join('');
83
+ const GROK_PRE_ACTION_HOOK_COMMAND = `node -e '${GROK_PRE_ACTION_GUARD_SOURCE}'`;
36
84
  const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*';
85
+ const CODEX_NATIVE_HOOK_MATCHER = 'Bash|apply_patch|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*|functions\\.(?!marrow_).*';
86
+ const CURSOR_NATIVE_HOOK_MATCHER = 'Shell|Write|Delete|Task|MCP:(?!marrow(?:_.*|:marrow_.*)$).*';
87
+ const GEMINI_NATIVE_HOOK_MATCHER = '^(?:run_shell_command|write_file|replace|edit_file|delete_file|mcp_(?!marrow_marrow_)[A-Za-z0-9_]{1,192})$';
88
+ 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';
89
+ const CODEX_HOOK_TIMEOUT_SECONDS = 5;
90
+ const CODEX_SESSION_TIMEOUT_SECONDS = 3;
91
+ const GEMINI_HOOK_TIMEOUT_MS = 5000;
92
+ const GEMINI_CLOSEOUT_TIMEOUT_MS = 3000;
37
93
  const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
38
94
  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']);
95
+ const TOOL_PROFILES = new Set(['primary', 'core', 'full']);
96
+ const TOOL_PROFILE_EXPECTED_COUNTS = Object.freeze({ primary: 17, core: 7, full: null });
97
+ const TOOL_PROFILE_EXACT_FIX = 'Unset MARROW_TOOL_PROFILE to use primary, or set MARROW_TOOL_PROFILE=core or MARROW_TOOL_PROFILE=full, then restart the owning harness and run npx @getmarrow/install@latest doctor --self-test.';
98
+ const PRIMARY_TOOL_NAMES = Object.freeze([
99
+ 'marrow_agent_runtime',
100
+ 'marrow_arbitrate',
101
+ 'marrow_coordinate',
102
+ 'marrow_replay_compare',
103
+ 'marrow_decision_brief',
104
+ 'marrow_think',
105
+ 'marrow_commit',
106
+ 'marrow_workflow_gate',
107
+ 'marrow_completion_contracts',
108
+ 'marrow_evaluate_completion_contract',
109
+ 'marrow_agent_status',
110
+ 'marrow_value_report',
111
+ 'marrow_buyer_proof',
112
+ 'marrow_governance_timeline',
113
+ 'marrow_decision_trace',
114
+ 'marrow_fleet_lessons',
115
+ 'marrow_model_usage',
116
+ ]);
39
117
  const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
40
118
  { 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' },
119
+ { client: 'cursor', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
120
+ { client: 'composer', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
121
+ { client: 'cline', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'cancel_closeout'], install_surface: 'mcp' },
122
+ { client: 'windsurf', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'response_closeout'], install_surface: 'mcp' },
123
+ { client: 'codex', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
46
124
  { client: 'opencode', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
47
125
  { client: 'hermes', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
48
126
  { 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' },
127
+ { client: 'gemini', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
128
+ { client: 'grok', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
51
129
  { client: 'deepseek', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
52
130
  { client: 'qwen', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
53
131
  { client: 'kimi', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
@@ -63,6 +141,148 @@ function explicitMcpVersion(command) {
63
141
  return match ? match[1] : null;
64
142
  }
65
143
 
144
+ function resolveToolProfile(value) {
145
+ const structured = value !== null && typeof value === 'object' && !Array.isArray(value);
146
+ const candidate = structured
147
+ ? value.configured_profile
148
+ : value;
149
+ const absent = !structured && candidate === undefined;
150
+ const structuredUnset = structured && candidate === 'unset';
151
+ const configuredProfile = absent || structuredUnset
152
+ ? 'unset'
153
+ : candidate;
154
+ if (!absent && !structuredUnset && !TOOL_PROFILES.has(configuredProfile)) {
155
+ throw new Error(`Invalid MARROW_TOOL_PROFILE. ${TOOL_PROFILE_EXACT_FIX}`);
156
+ }
157
+ const effectiveProfile = configuredProfile === 'unset' ? 'primary' : configuredProfile;
158
+ return {
159
+ configured_profile: configuredProfile,
160
+ effective_profile: effectiveProfile,
161
+ expected_visible_count: TOOL_PROFILE_EXPECTED_COUNTS[effectiveProfile],
162
+ };
163
+ }
164
+
165
+ function normalizePrimaryToolAvailability(value) {
166
+ if (!value || typeof value !== 'object' || Array.isArray(value) || value.profile !== 'primary') return null;
167
+ const evidence = value.entitlement_evidence;
168
+ const counts = value.counts;
169
+ const tools = Array.isArray(value.tools) ? value.tools : [];
170
+ if (!evidence || typeof evidence !== 'object' || evidence.authorizing !== false) return null;
171
+ if (!['available', 'unavailable'].includes(evidence.state)) return null;
172
+ if (!counts || counts.total !== 17 || !Number.isInteger(counts.entitled) || !Number.isInteger(counts.upgrade_required)) return null;
173
+ if (counts.entitled + counts.upgrade_required !== counts.total || tools.length !== counts.total) return null;
174
+ const normalizedTools = [];
175
+ const seen = new Set();
176
+ for (const tool of tools) {
177
+ if (!tool || typeof tool !== 'object' || !PRIMARY_TOOL_NAMES.includes(tool.name) || seen.has(tool.name)) return null;
178
+ if (!['entitled', 'upgrade_required'].includes(tool.state) || typeof tool.always_available !== 'boolean') return null;
179
+ seen.add(tool.name);
180
+ normalizedTools.push({
181
+ name: tool.name,
182
+ state: tool.state,
183
+ always_available: tool.always_available,
184
+ plan_feature: typeof tool.plan_feature === 'string' ? tool.plan_feature : null,
185
+ minimum_plan: typeof tool.minimum_plan === 'string' ? tool.minimum_plan : null,
186
+ owner_management_url: typeof tool.owner_management_url === 'string' ? tool.owner_management_url : '',
187
+ });
188
+ }
189
+ if (PRIMARY_TOOL_NAMES.some((name) => !seen.has(name))) return null;
190
+ const entitled = normalizedTools.filter((tool) => tool.state === 'entitled').length;
191
+ const upgradeRequired = normalizedTools.filter((tool) => tool.state === 'upgrade_required').length;
192
+ if (entitled !== counts.entitled || upgradeRequired !== counts.upgrade_required) return null;
193
+ return {
194
+ profile: 'primary',
195
+ current_plan: typeof value.current_plan === 'string' ? value.current_plan : null,
196
+ owner_management_url: typeof value.owner_management_url === 'string' ? value.owner_management_url : '',
197
+ entitlement_evidence: {
198
+ state: evidence.state,
199
+ source: typeof evidence.source === 'string' ? evidence.source : 'entitlement_read_unavailable',
200
+ authoritative: evidence.authoritative === true,
201
+ authorizing: false,
202
+ },
203
+ counts: { total: 17, entitled, upgrade_required: upgradeRequired },
204
+ tools: normalizedTools,
205
+ };
206
+ }
207
+
208
+ function backendEntitlementProjection(statusProfile, contextProjection) {
209
+ const freshProjection = normalizePrimaryToolAvailability(contextProjection);
210
+ if (freshProjection) {
211
+ return {
212
+ evidence_state: freshProjection.entitlement_evidence.state,
213
+ source: 'authenticated_backend',
214
+ authorizes_calls: false,
215
+ primary_tool_availability: freshProjection,
216
+ };
217
+ }
218
+ const envelope = statusProfile?.backend_entitlement_projection;
219
+ const projected = normalizePrimaryToolAvailability(envelope?.primary_tool_availability);
220
+ const source = ['authenticated_backend', 'cached_or_stale_status', 'backend_projection_not_provided'].includes(envelope?.source)
221
+ ? envelope.source
222
+ : 'backend_projection_not_provided';
223
+ const available = envelope?.authorizes_calls === false
224
+ && envelope?.evidence_state === 'available'
225
+ && source === 'authenticated_backend'
226
+ && projected?.entitlement_evidence.state === 'available';
227
+ return {
228
+ evidence_state: available ? 'available' : 'unavailable',
229
+ source,
230
+ authorizes_calls: false,
231
+ primary_tool_availability: projected,
232
+ };
233
+ }
234
+
235
+ function buildMcpToolProfileReport(value, statusProfile = null, contextProjection = null, forceReload = false) {
236
+ const expected = resolveToolProfile(value);
237
+ const reportedNames = Array.isArray(statusProfile?.visible_tool_names)
238
+ ? statusProfile.visible_tool_names.filter((name) => typeof name === 'string')
239
+ : [];
240
+ const reportedCount = statusProfile?.visible_tool_count;
241
+ const reportedConfigured = statusProfile?.configured_profile;
242
+ const reportedEffective = statusProfile?.effective_profile;
243
+ const uniqueNames = new Set(reportedNames);
244
+ const profileIdentityMatches = reportedConfigured === expected.configured_profile
245
+ && reportedEffective === expected.effective_profile;
246
+ const reportedCatalogIsConsistent = Number.isInteger(reportedCount)
247
+ && reportedCount >= 0
248
+ && reportedNames.length === reportedCount
249
+ && uniqueNames.size === reportedCount;
250
+ const expectedCount = expected.effective_profile === 'full'
251
+ && profileIdentityMatches
252
+ && reportedCatalogIsConsistent
253
+ ? reportedCount
254
+ : expected.expected_visible_count;
255
+ const expectedPrimaryNames = expected.effective_profile !== 'primary'
256
+ || (reportedNames.length === PRIMARY_TOOL_NAMES.length
257
+ && PRIMARY_TOOL_NAMES.every((name) => uniqueNames.has(name)));
258
+ const visibilityLive = !forceReload
259
+ && profileIdentityMatches
260
+ && statusProfile?.local_visibility_grants_entitlement === false
261
+ && reportedCatalogIsConsistent
262
+ && reportedCount === expectedCount
263
+ && expectedPrimaryNames;
264
+ return {
265
+ configured_profile: expected.configured_profile,
266
+ effective_profile: expected.effective_profile,
267
+ expected_visible_count: expectedCount,
268
+ visible_tool_count: visibilityLive ? reportedCount : null,
269
+ actual_visible_count: visibilityLive ? reportedCount : null,
270
+ visible_tool_names: visibilityLive ? reportedNames : [],
271
+ local_visibility_grants_entitlement: false,
272
+ visibility_live: visibilityLive,
273
+ reload_required: !visibilityLive,
274
+ reported_configured_profile: typeof reportedConfigured === 'string' ? reportedConfigured : null,
275
+ reported_effective_profile: typeof reportedEffective === 'string' ? reportedEffective : null,
276
+ backend_entitlement_projection: backendEntitlementProjection(statusProfile, contextProjection),
277
+ };
278
+ }
279
+
280
+ function initialToolProfileReport(value) {
281
+ return {
282
+ ...buildMcpToolProfileReport(value),
283
+ };
284
+ }
285
+
66
286
  function readMcpPackageVersion(packageRoot) {
67
287
  try {
68
288
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
@@ -252,7 +472,7 @@ function sourceClient() {
252
472
  return aliases[raw] || (SOURCE_CLIENTS.has(raw) ? raw : 'custom');
253
473
  }
254
474
 
255
- function parseArgs(argv) {
475
+ function parseArgs(argv, env = process.env) {
256
476
  const options = {
257
477
  cwd: process.cwd(),
258
478
  yes: false,
@@ -260,50 +480,79 @@ function parseArgs(argv) {
260
480
  doctor: false,
261
481
  repair: false,
262
482
  mode: 'auto',
263
- apiKey: process.env.MARROW_API_KEY || '',
264
- baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
265
- agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
483
+ apiKey: env.MARROW_API_KEY || '',
484
+ baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
485
+ agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
486
+ toolProfile: resolveToolProfile(env.MARROW_TOOL_PROFILE),
266
487
  selfTest: true,
267
488
  selfTestExplicitlyDisabled: false,
268
489
  json: false,
269
490
  activate: false,
270
491
  controller: true,
271
492
  };
493
+ let explicitOperation = false;
272
494
 
273
495
  for (let i = 0; i < argv.length; i += 1) {
274
496
  const arg = argv[i];
275
497
  if (arg === 'activate' || arg === '--activate') {
498
+ explicitOperation = true;
276
499
  options.activate = true;
277
500
  options.yes = true;
278
501
  options.selfTest = true;
279
502
  }
280
- else if (arg === '--yes' || arg === '-y') options.yes = true;
503
+ else if (arg === '--yes' || arg === '-y') {
504
+ explicitOperation = true;
505
+ options.yes = true;
506
+ }
281
507
  else if (arg === '--repair' || arg === 'repair' || arg === 'update' || arg === '--update') {
508
+ explicitOperation = true;
282
509
  options.repair = true;
283
510
  options.yes = true;
284
511
  options.update = true;
285
512
  }
286
- else if (arg === '--dry-run') options.dryRun = true;
287
- else if (arg === '--doctor' || arg === 'doctor' || arg === 'check') options.doctor = true;
513
+ else if (arg === '--dry-run') {
514
+ explicitOperation = true;
515
+ options.dryRun = true;
516
+ }
517
+ else if (arg === '--doctor' || arg === 'doctor' || arg === 'check') {
518
+ explicitOperation = true;
519
+ options.doctor = true;
520
+ }
288
521
  else if (arg === '--json') options.json = true;
289
522
  else if (arg === '--no-self-test') {
523
+ explicitOperation = true;
290
524
  options.selfTest = false;
291
525
  options.selfTestExplicitlyDisabled = true;
292
526
  }
293
527
  else if (arg === '--no-controller') options.controller = false;
294
528
  else if (arg === '--self-test') options.selfTest = true;
295
529
  else if (arg === '--cwd') options.cwd = path.resolve(argv[++i] || options.cwd);
296
- else if (arg === '--mode') options.mode = argv[++i] || options.mode;
530
+ else if (arg === '--mode') {
531
+ explicitOperation = true;
532
+ options.mode = argv[++i] || options.mode;
533
+ }
297
534
  else if (arg === '--key') {
298
535
  options.apiKey = argv[++i] || options.apiKey;
299
536
  options.keyFromArg = true;
300
537
  }
301
538
  else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
302
539
  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';
540
+ else if (arg === '--mcp') {
541
+ explicitOperation = true;
542
+ options.mode = 'mcp';
543
+ }
544
+ else if (arg === '--sdk') {
545
+ explicitOperation = true;
546
+ options.mode = 'sdk';
547
+ }
548
+ else if (arg === '--md' || arg === '--instructions') {
549
+ explicitOperation = true;
550
+ options.mode = 'md';
551
+ }
552
+ else if (arg === '--both') {
553
+ explicitOperation = true;
554
+ options.mode = 'both';
555
+ }
307
556
  else if (arg === '--help' || arg === '-h') {
308
557
  options.help = true;
309
558
  } else {
@@ -314,6 +563,12 @@ function parseArgs(argv) {
314
563
  if (!['auto', 'mcp', 'sdk', 'both', 'md'].includes(options.mode)) {
315
564
  throw new Error('--mode must be one of auto, mcp, sdk, both, md');
316
565
  }
566
+ if (!explicitOperation) {
567
+ options.activate = true;
568
+ options.yes = true;
569
+ options.selfTest = true;
570
+ }
571
+ if (options.dryRun) options.selfTest = false;
317
572
  if (options.activate && options.selfTestExplicitlyDisabled) {
318
573
  throw new Error('activate cannot be combined with --no-self-test because server verification is required');
319
574
  }
@@ -326,6 +581,7 @@ function parseArgs(argv) {
326
581
 
327
582
  function usage() {
328
583
  return `Usage:
584
+ npx @getmarrow/install
329
585
  npx @getmarrow/install --dry-run
330
586
  npx @getmarrow/install activate
331
587
  npx @getmarrow/install --yes
@@ -336,6 +592,7 @@ function usage() {
336
592
  npx @getmarrow/install --sdk --yes
337
593
 
338
594
  Options:
595
+ (no command) Detect, install, self-test, and start the supported persistent controller
339
596
  activate Detect, install, self-test, and return a server-confirmed activation receipt
340
597
  --dry-run Print planned changes without writing
341
598
  --doctor Check install health without writing
@@ -348,6 +605,10 @@ Options:
348
605
  --agent-id <id> Agent/fleet id for self-test headers
349
606
  --no-controller Do not start the local background controller during install/repair
350
607
  --no-self-test Skip API smoke/self-test
608
+
609
+ Environment:
610
+ MARROW_TOOL_PROFILE Leave unset for primary (17 tools), or explicitly set primary, core, or full.
611
+ Visibility never grants entitlement; backend plans and permissions authorize calls.
351
612
  `;
352
613
  }
353
614
 
@@ -361,6 +622,9 @@ function detectedClient(detection) {
361
622
  if (detection.openclaw) return 'openclaw';
362
623
  if (detection.claudeCode) return 'claude-code';
363
624
  if (detection.cursor) return 'cursor';
625
+ if (detection.cline) return 'cline';
626
+ if (detection.windsurf) return 'windsurf';
627
+ if (detection.gemini) return 'gemini';
364
628
  if (detection.codex) return 'codex';
365
629
  return 'custom';
366
630
  }
@@ -402,6 +666,14 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
402
666
  requirements: path.join(root, 'requirements.txt'),
403
667
  setupPy: path.join(root, 'setup.py'),
404
668
  claudeSettings: path.join(root, '.claude', 'settings.json'),
669
+ codexHooks: path.join(root, '.codex', 'hooks.json'),
670
+ cursorHooks: path.join(root, '.cursor', 'hooks.json'),
671
+ windsurfHooks: path.join(root, '.windsurf', 'hooks.json'),
672
+ geminiSettings: path.join(root, '.gemini', 'settings.json'),
673
+ grokHooks: path.join(home, '.grok', 'hooks', 'marrow.json'),
674
+ clinePreToolUseHook: path.join(root, '.clinerules', 'hooks', 'PreToolUse'),
675
+ clinePostToolUseHook: path.join(root, '.clinerules', 'hooks', 'PostToolUse'),
676
+ clineTaskCancelHook: path.join(root, '.clinerules', 'hooks', 'TaskCancel'),
405
677
  claudeMd: path.join(root, 'CLAUDE.md'),
406
678
  agentsMd: path.join(root, 'AGENTS.md'),
407
679
  cursorRules: path.join(root, '.cursor', 'rules', 'marrow.mdc'),
@@ -420,6 +692,10 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
420
692
  python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
421
693
  claudeCode: exists(paths.claudeSettings) || exists(paths.claudeMd),
422
694
  cursor: exists(path.join(root, '.cursor')),
695
+ cline: exists(path.join(root, '.clinerules')),
696
+ windsurf: exists(path.join(root, '.windsurf')),
697
+ gemini: exists(path.join(root, '.gemini')),
698
+ grok: exists(path.join(home, '.grok')) || exists(path.join(root, '.grok')),
423
699
  codex: exists(paths.agentsMd) || exists(path.join(root, '.codex')),
424
700
  mcpConfig: exists(paths.mcpJson) || exists(paths.cursorMcp) || exists(paths.claudeSettings),
425
701
  openclaw: exists(paths.openclawJson) || Boolean(env.OPENCLAW_HOME || env.OPENCLAW_AGENT_ID),
@@ -621,13 +897,14 @@ function passiveInstructions() {
621
897
  Marrow should run passively after install:
622
898
 
623
899
  - Use MCP plus these instructions in every workspace: \`npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup\`.
900
+ - Leave \`MARROW_TOOL_PROFILE\` unset for the 17-tool primary surface. Set \`MARROW_TOOL_PROFILE=core\` or \`MARROW_TOOL_PROFILE=full\` only as an explicit opt-in; backend plans and permissions still enforce access to every visible tool.
624
901
  - 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.
902
+ - 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
903
  - 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
904
  - Before risky work, use Marrow's decision brief or passive prompt hook.
628
905
  - 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
906
  - 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>\`.
907
+ - 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
908
  - 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
909
  - After meaningful work, record the outcome so future agents learn from it.
633
910
  - 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.
@@ -684,6 +961,7 @@ function envExample(options = {}) {
684
961
  MARROW_BASE_URL=${JSON.stringify(baseUrl)}
685
962
  MARROW_FLEET_AGENT_ID=${JSON.stringify(agentId)}
686
963
  MARROW_CLIENT=${JSON.stringify(client)}
964
+ # MARROW_TOOL_PROFILE is intentionally unset: ordinary setup uses primary. Set core or full only as an explicit opt-in.
687
965
  MARROW_ENFORCEMENT_MODE=auto
688
966
  MARROW_PASSIVE_BRIEF=auto
689
967
  MARROW_PASSIVE_VALUE_REPORT=true
@@ -757,12 +1035,12 @@ function exactHookDescriptors(settings, eventName, command, matcher) {
757
1035
  function marrowHookSubcommand(command) {
758
1036
  if (typeof command !== 'string') return null;
759
1037
  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)$/,
1038
+ /^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
1039
  );
762
1040
  return match?.[1] || null;
763
1041
  }
764
1042
 
765
- function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
1043
+ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher, handlerOptions = {}) {
766
1044
  const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
767
1045
  let preferredHandler = null;
768
1046
  const retained = [];
@@ -786,7 +1064,7 @@ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, ma
786
1064
  }
787
1065
  if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
788
1066
  }
789
- const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
1067
+ const canonical = { hooks: [{ ...(preferredHandler || {}), ...handlerOptions, type: 'command', command }] };
790
1068
  if (matcher != null) canonical.matcher = matcher;
791
1069
  retained.push(canonical);
792
1070
  return retained;
@@ -882,6 +1160,439 @@ function upsertClaudeHooks(settingsPath) {
882
1160
  return JSON.stringify(settings, null, 2) + '\n';
883
1161
  }
884
1162
 
1163
+ function codexNativeHookFingerprint(settings) {
1164
+ const contract = {
1165
+ schema: 'marrow-codex-native-hooks.v1',
1166
+ adapter_version: MCP_ADAPTER_VERSION,
1167
+ expected_hooks: NATIVE_EXPECTED_HOOKS,
1168
+ configured: {
1169
+ prompt: exactHookConfigured(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
1170
+ pre_action: exactHookConfigured(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
1171
+ action_result: exactHookConfigured(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
1172
+ session_end: exactHookConfigured(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
1173
+ },
1174
+ descriptors: {
1175
+ prompt: exactHookDescriptors(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
1176
+ pre_action: exactHookDescriptors(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
1177
+ action_result: exactHookDescriptors(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
1178
+ session_end: exactHookDescriptors(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
1179
+ },
1180
+ active_marrow_handlers: {
1181
+ prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
1182
+ pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
1183
+ action_result: marrowHookDescriptors(settings, 'PostToolUse'),
1184
+ session_end: marrowHookDescriptors(settings, 'SessionEnd'),
1185
+ },
1186
+ };
1187
+ return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
1188
+ }
1189
+
1190
+ function upsertCodexHooks(hooksPath) {
1191
+ const settings = parseJsonObject(hooksPath);
1192
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1193
+ ? settings.hooks
1194
+ : {};
1195
+ settings.hooks = {
1196
+ ...hooks,
1197
+ UserPromptSubmit: reconcileMarrowCommandHook(
1198
+ settings, 'UserPromptSubmit', 'context-hook', CODEX_CONTEXT_HOOK_COMMAND, undefined,
1199
+ { timeout: CODEX_HOOK_TIMEOUT_SECONDS },
1200
+ ),
1201
+ PreToolUse: reconcileMarrowCommandHook(
1202
+ settings, 'PreToolUse', 'pre-action-hook', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
1203
+ { timeout: CODEX_HOOK_TIMEOUT_SECONDS, async: false },
1204
+ ),
1205
+ PostToolUse: reconcileMarrowCommandHook(
1206
+ settings, 'PostToolUse', 'hook', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
1207
+ { timeout: CODEX_HOOK_TIMEOUT_SECONDS },
1208
+ ),
1209
+ SessionEnd: reconcileMarrowCommandHook(
1210
+ settings, 'SessionEnd', 'session-hook', CODEX_SESSION_END_HOOK_COMMAND, undefined,
1211
+ { timeout: CODEX_SESSION_TIMEOUT_SECONDS },
1212
+ ),
1213
+ };
1214
+ return JSON.stringify(settings, null, 2) + '\n';
1215
+ }
1216
+
1217
+ function reconcileCursorHook(settings, eventName, subcommand, canonical) {
1218
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1219
+ const retained = original.filter((entry) => !(
1220
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1221
+ && marrowHookSubcommand(entry.command)
1222
+ ));
1223
+ return [...retained, canonical];
1224
+ }
1225
+
1226
+ function exactCursorHookConfigured(settings, eventName, command, matcher, required = {}) {
1227
+ const entries = settings?.hooks?.[eventName];
1228
+ if (!Array.isArray(entries)) return false;
1229
+ return entries.some((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
1230
+ && entry.command === command
1231
+ && (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
1232
+ && Object.entries(required).every(([key, value]) => entry[key] === value));
1233
+ }
1234
+
1235
+ function cursorHookDescriptors(settings, eventName) {
1236
+ const entries = settings?.hooks?.[eventName];
1237
+ if (!Array.isArray(entries)) return [];
1238
+ return entries.flatMap((entry) => {
1239
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !marrowHookSubcommand(entry.command)) return [];
1240
+ return [{
1241
+ matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
1242
+ command: String(entry.command).trim(),
1243
+ timeout: typeof entry.timeout === 'number' && Number.isFinite(entry.timeout) ? entry.timeout : null,
1244
+ failClosed: entry.failClosed === true,
1245
+ async: entry.async === false ? false : null,
1246
+ }];
1247
+ });
1248
+ }
1249
+
1250
+ function cursorNativeHookFingerprint(settings) {
1251
+ const contract = {
1252
+ schema: 'marrow-cursor-native-hooks.v1',
1253
+ adapter_version: MCP_ADAPTER_VERSION,
1254
+ expected_hooks: ['pre_action', 'action_result', 'outcome_closure'],
1255
+ configured: {
1256
+ pre_action: exactCursorHookConfigured(settings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1257
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1258
+ failClosed: true,
1259
+ async: false,
1260
+ }),
1261
+ action_result_success: exactCursorHookConfigured(settings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1262
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1263
+ }),
1264
+ action_result_failure: exactCursorHookConfigured(settings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1265
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1266
+ }),
1267
+ outcome_closure: exactCursorHookConfigured(settings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1268
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1269
+ }),
1270
+ },
1271
+ descriptors: {
1272
+ pre_action: cursorHookDescriptors(settings, 'preToolUse'),
1273
+ action_result_success: cursorHookDescriptors(settings, 'postToolUse'),
1274
+ action_result_failure: cursorHookDescriptors(settings, 'postToolUseFailure'),
1275
+ outcome_closure: cursorHookDescriptors(settings, 'stop'),
1276
+ },
1277
+ };
1278
+ return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
1279
+ }
1280
+
1281
+ function upsertCursorHooks(hooksPath) {
1282
+ const settings = parseJsonObject(hooksPath);
1283
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1284
+ ? settings.hooks
1285
+ : {};
1286
+ settings.version = 1;
1287
+ settings.hooks = {
1288
+ ...hooks,
1289
+ preToolUse: reconcileCursorHook(settings, 'preToolUse', 'pre-action-hook', {
1290
+ command: CURSOR_PRE_ACTION_HOOK_COMMAND,
1291
+ matcher: CURSOR_NATIVE_HOOK_MATCHER,
1292
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1293
+ failClosed: true,
1294
+ async: false,
1295
+ }),
1296
+ postToolUse: reconcileCursorHook(settings, 'postToolUse', 'hook', {
1297
+ command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
1298
+ matcher: CURSOR_NATIVE_HOOK_MATCHER,
1299
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1300
+ }),
1301
+ postToolUseFailure: reconcileCursorHook(settings, 'postToolUseFailure', 'hook', {
1302
+ command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
1303
+ matcher: CURSOR_NATIVE_HOOK_MATCHER,
1304
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1305
+ }),
1306
+ stop: reconcileCursorHook(settings, 'stop', 'session-hook', {
1307
+ command: CURSOR_SESSION_END_HOOK_COMMAND,
1308
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1309
+ }),
1310
+ };
1311
+ return JSON.stringify(settings, null, 2) + '\n';
1312
+ }
1313
+
1314
+ const WINDSURF_PRE_EVENTS = ['pre_write_code', 'pre_run_command', 'pre_mcp_tool_use'];
1315
+ const WINDSURF_POST_EVENTS = ['post_write_code', 'post_run_command', 'post_mcp_tool_use'];
1316
+
1317
+ function windsurfMarrowHookEntrypoint(command) {
1318
+ if (typeof command !== 'string') return null;
1319
+ const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+windsurf-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
1320
+ return match?.[1] || null;
1321
+ }
1322
+
1323
+ function reconcileWindsurfHook(settings, eventName, command) {
1324
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1325
+ const retained = original.filter((entry) => !(
1326
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1327
+ && windsurfMarrowHookEntrypoint(entry.command)
1328
+ ));
1329
+ return [...retained, { command, show_output: false }];
1330
+ }
1331
+
1332
+ function exactWindsurfHookConfigured(settings, eventName, command) {
1333
+ const entries = settings?.hooks?.[eventName];
1334
+ return Array.isArray(entries) && entries.some((entry) => (
1335
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1336
+ && entry.command === command
1337
+ && entry.show_output === false
1338
+ ));
1339
+ }
1340
+
1341
+ function windsurfNativeHookFingerprint(settings) {
1342
+ const configured = Object.fromEntries([
1343
+ ...WINDSURF_PRE_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND)]),
1344
+ ...WINDSURF_POST_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND)]),
1345
+ ['post_cascade_response', exactWindsurfHookConfigured(settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND)],
1346
+ ]);
1347
+ return crypto.createHash('sha256').update(JSON.stringify({
1348
+ schema: 'marrow-windsurf-native-hooks.v1',
1349
+ adapter_version: MCP_ADAPTER_VERSION,
1350
+ expected_hooks: ['pre_action', 'action_result', 'response_closeout'],
1351
+ restricted_mode_disables_hooks: true,
1352
+ configured,
1353
+ })).digest('hex');
1354
+ }
1355
+
1356
+ function upsertWindsurfHooks(hooksPath) {
1357
+ const settings = parseJsonObject(hooksPath);
1358
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1359
+ ? settings.hooks
1360
+ : {};
1361
+ settings.hooks = { ...hooks };
1362
+ for (const eventName of WINDSURF_PRE_EVENTS) {
1363
+ settings.hooks[eventName] = reconcileWindsurfHook(
1364
+ settings, eventName, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1365
+ );
1366
+ }
1367
+ for (const eventName of WINDSURF_POST_EVENTS) {
1368
+ settings.hooks[eventName] = reconcileWindsurfHook(
1369
+ settings, eventName, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1370
+ );
1371
+ }
1372
+ settings.hooks.post_cascade_response = reconcileWindsurfHook(
1373
+ settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1374
+ );
1375
+ return JSON.stringify(settings, null, 2) + '\n';
1376
+ }
1377
+
1378
+ function geminiMarrowHookEntrypoint(command) {
1379
+ if (typeof command !== 'string') return null;
1380
+ const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+gemini-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
1381
+ return match?.[1] || null;
1382
+ }
1383
+
1384
+ function reconcileGeminiHook(settings, eventName, canonical) {
1385
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1386
+ const retained = [];
1387
+ for (const group of original) {
1388
+ if (!group || typeof group !== 'object' || Array.isArray(group) || !Array.isArray(group.hooks)) {
1389
+ retained.push(group);
1390
+ continue;
1391
+ }
1392
+ const hooks = group.hooks.filter((handler) => !(
1393
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1394
+ && (String(handler.name || '').startsWith('marrow-') || geminiMarrowHookEntrypoint(handler.command))
1395
+ ));
1396
+ if (hooks.length > 0) retained.push({ ...group, hooks });
1397
+ }
1398
+ return [...retained, canonical];
1399
+ }
1400
+
1401
+ function exactGeminiHookConfigured(settings, eventName, name, command, matcher, timeout) {
1402
+ const groups = settings?.hooks?.[eventName];
1403
+ if (!Array.isArray(groups)) return false;
1404
+ return groups.some((group) => (
1405
+ group && typeof group === 'object' && !Array.isArray(group)
1406
+ && (matcher === undefined ? group.matcher === undefined : group.matcher === matcher)
1407
+ && Array.isArray(group.hooks)
1408
+ && group.hooks.some((handler) => (
1409
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1410
+ && handler.name === name
1411
+ && handler.type === 'command'
1412
+ && handler.command === command
1413
+ && handler.timeout === timeout
1414
+ ))
1415
+ ));
1416
+ }
1417
+
1418
+ function geminiHooksExplicitlyDisabled(settings) {
1419
+ return settings?.hooksConfig?.enabled === false;
1420
+ }
1421
+
1422
+ function geminiNativeHookFingerprint(settings) {
1423
+ return crypto.createHash('sha256').update(JSON.stringify({
1424
+ schema: 'marrow-gemini-native-hooks.v1',
1425
+ adapter_version: MCP_ADAPTER_VERSION,
1426
+ expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
1427
+ explicitly_enabled: settings?.hooksConfig?.enabled === true,
1428
+ explicitly_disabled: geminiHooksExplicitlyDisabled(settings),
1429
+ configured: {
1430
+ pre_action: exactGeminiHookConfigured(
1431
+ settings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1432
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1433
+ ),
1434
+ action_result: exactGeminiHookConfigured(
1435
+ settings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1436
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1437
+ ),
1438
+ turn_closeout: exactGeminiHookConfigured(
1439
+ settings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1440
+ undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1441
+ ),
1442
+ },
1443
+ session_end_claimed: false,
1444
+ })).digest('hex');
1445
+ }
1446
+
1447
+ function exactGrokHookConfigured(settings, eventName, command, matcher, timeout) {
1448
+ const entries = settings?.hooks?.[eventName];
1449
+ if (!Array.isArray(entries)) return false;
1450
+ return entries.some((entry) => (
1451
+ entry && typeof entry === 'object' && !Array.isArray(entry)
1452
+ && (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
1453
+ && Array.isArray(entry.hooks)
1454
+ && entry.hooks.some((handler) => (
1455
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1456
+ && handler.type === 'command'
1457
+ && handler.command === command
1458
+ && handler.timeout === timeout
1459
+ ))
1460
+ ));
1461
+ }
1462
+
1463
+ function grokHasDuplicateSessionEnd(settings) {
1464
+ const entries = settings?.hooks?.SessionEnd;
1465
+ if (!Array.isArray(entries)) return false;
1466
+ return entries.some((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((handler) => (
1467
+ handler && typeof handler === 'object' && !Array.isArray(handler)
1468
+ && typeof handler.command === 'string'
1469
+ && /marrow-mcp\s+grok-session-hook(?:\s|['"]|$)/.test(handler.command)
1470
+ )));
1471
+ }
1472
+
1473
+ function grokNativeHookFingerprint(settings) {
1474
+ return crypto.createHash('sha256').update(JSON.stringify({
1475
+ schema: 'marrow-grok-native-hooks.v1',
1476
+ adapter_version: MCP_ADAPTER_VERSION,
1477
+ expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
1478
+ configured: {
1479
+ context: exactGrokHookConfigured(settings, 'UserPromptSubmit', GROK_CONTEXT_HOOK_COMMAND, undefined, 5),
1480
+ pre_action: exactGrokHookConfigured(settings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7),
1481
+ action_result_success: exactGrokHookConfigured(settings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1482
+ action_result_failure: exactGrokHookConfigured(settings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1483
+ turn_closeout: exactGrokHookConfigured(settings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3),
1484
+ duplicate_session_end: grokHasDuplicateSessionEnd(settings),
1485
+ },
1486
+ })).digest('hex');
1487
+ }
1488
+
1489
+ function upsertGeminiHooks(settingsPath) {
1490
+ const settings = parseJsonObject(settingsPath);
1491
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1492
+ ? settings.hooks
1493
+ : {};
1494
+ settings.hooks = {
1495
+ ...hooks,
1496
+ BeforeTool: reconcileGeminiHook(settings, 'BeforeTool', {
1497
+ matcher: GEMINI_NATIVE_HOOK_MATCHER,
1498
+ hooks: [{
1499
+ name: 'marrow-before-tool',
1500
+ type: 'command',
1501
+ command: GEMINI_PRE_ACTION_HOOK_COMMAND,
1502
+ timeout: GEMINI_HOOK_TIMEOUT_MS,
1503
+ }],
1504
+ }),
1505
+ AfterTool: reconcileGeminiHook(settings, 'AfterTool', {
1506
+ matcher: GEMINI_NATIVE_HOOK_MATCHER,
1507
+ hooks: [{
1508
+ name: 'marrow-after-tool',
1509
+ type: 'command',
1510
+ command: GEMINI_ACTION_RESULT_HOOK_COMMAND,
1511
+ timeout: GEMINI_HOOK_TIMEOUT_MS,
1512
+ }],
1513
+ }),
1514
+ AfterAgent: reconcileGeminiHook(settings, 'AfterAgent', {
1515
+ hooks: [{
1516
+ name: 'marrow-after-agent',
1517
+ type: 'command',
1518
+ command: GEMINI_SESSION_END_HOOK_COMMAND,
1519
+ timeout: GEMINI_CLOSEOUT_TIMEOUT_MS,
1520
+ }],
1521
+ }),
1522
+ };
1523
+ return JSON.stringify(settings, null, 2) + '\n';
1524
+ }
1525
+
1526
+ function clinePreToolUseHookSource() {
1527
+ return `#!/bin/sh
1528
+ output="$(npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook 2>/dev/null)" || output=""
1529
+ if [ -n "$output" ]; then
1530
+ 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=""
1531
+ if [ -n "$validated" ]; then
1532
+ printf '%s\n' "$validated"
1533
+ exit 0
1534
+ fi
1535
+ fi
1536
+ printf '%s\n' '{"cancel":true,"errorMessage":"Marrow governance did not return a valid decision. Restore trusted configuration and retry."}'
1537
+ exit 0
1538
+ `;
1539
+ }
1540
+
1541
+ function clineTelemetryHookSource(entrypoint) {
1542
+ return `#!/bin/sh
1543
+ npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp ${entrypoint} >/dev/null 2>&1 || :
1544
+ exit 0
1545
+ `;
1546
+ }
1547
+
1548
+ function clineHookContract(detection) {
1549
+ return [
1550
+ {
1551
+ stage: 'pre_action',
1552
+ path: detection.paths.clinePreToolUseHook,
1553
+ label: 'Cline PreToolUse native hook',
1554
+ content: clinePreToolUseHookSource(),
1555
+ },
1556
+ {
1557
+ stage: 'action_result',
1558
+ path: detection.paths.clinePostToolUseHook,
1559
+ label: 'Cline PostToolUse native hook',
1560
+ content: clineTelemetryHookSource('cline-hook'),
1561
+ },
1562
+ {
1563
+ stage: 'cancel_closeout',
1564
+ path: detection.paths.clineTaskCancelHook,
1565
+ label: 'Cline TaskCancel native hook',
1566
+ content: clineTelemetryHookSource('cline-session-hook'),
1567
+ },
1568
+ ];
1569
+ }
1570
+
1571
+ function exactExecutableFile(filePath, content) {
1572
+ try {
1573
+ const stat = fs.lstatSync(filePath);
1574
+ return stat.isFile() && !stat.isSymbolicLink() && (stat.mode & 0o111) !== 0 && safeRead(filePath) === content;
1575
+ } catch {
1576
+ return false;
1577
+ }
1578
+ }
1579
+
1580
+ function clineNativeHookFingerprint(detection) {
1581
+ const hooks = clineHookContract(detection).map((hook) => ({
1582
+ stage: hook.stage,
1583
+ configured: exactExecutableFile(hook.path, hook.content),
1584
+ content_sha256: exactExecutableFile(hook.path, hook.content)
1585
+ ? crypto.createHash('sha256').update(hook.content).digest('hex')
1586
+ : null,
1587
+ }));
1588
+ return crypto.createHash('sha256').update(JSON.stringify({
1589
+ schema: 'marrow-cline-native-hooks.v1',
1590
+ adapter_version: MCP_ADAPTER_VERSION,
1591
+ task_complete_support: 'coming_soon_not_configured',
1592
+ hooks,
1593
+ })).digest('hex');
1594
+ }
1595
+
885
1596
  function activationProfile(detection, plan, changes, client) {
886
1597
  const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
887
1598
  || HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
@@ -899,15 +1610,78 @@ function activationProfile(detection, plan, changes, client) {
899
1610
  : INSTALLER_ADAPTER_VERSION;
900
1611
  const observedHooks = [];
901
1612
  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');
1613
+ const codexSettings = safeJsonObject(detection.paths.codexHooks);
1614
+ const cursorSettings = safeJsonObject(detection.paths.cursorHooks);
1615
+ const windsurfSettings = safeJsonObject(detection.paths.windsurfHooks);
1616
+ const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
1617
+ const grokSettings = safeJsonObject(detection.paths.grokHooks);
1618
+ if (client === 'codex') {
1619
+ if (exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1620
+ if (exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1621
+ if (exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1622
+ if (exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1623
+ } else if (client === 'cursor' || client === 'composer') {
1624
+ if (exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1625
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
1626
+ })) observedHooks.push('pre_action');
1627
+ if (exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1628
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1629
+ }) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1630
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1631
+ })) observedHooks.push('action_result');
1632
+ if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1633
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1634
+ })) observedHooks.push('outcome_closure');
1635
+ } else if (client === 'cline') {
1636
+ for (const hook of clineHookContract(detection)) {
1637
+ if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
1638
+ }
1639
+ } else if (client === 'windsurf') {
1640
+ if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
1641
+ windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1642
+ ))) observedHooks.push('pre_action');
1643
+ if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
1644
+ windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1645
+ ))) observedHooks.push('action_result');
1646
+ if (exactWindsurfHookConfigured(
1647
+ windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1648
+ )) observedHooks.push('response_closeout');
1649
+ } else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
1650
+ if (exactGeminiHookConfigured(
1651
+ geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1652
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1653
+ )) observedHooks.push('pre_action');
1654
+ if (exactGeminiHookConfigured(
1655
+ geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1656
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1657
+ )) observedHooks.push('action_result');
1658
+ if (exactGeminiHookConfigured(
1659
+ geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1660
+ undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1661
+ )) observedHooks.push('turn_closeout');
1662
+ } else if (client === 'grok') {
1663
+ if (exactGrokHookConfigured(
1664
+ grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
1665
+ )) observedHooks.push('pre_action');
1666
+ if (exactGrokHookConfigured(
1667
+ grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1668
+ ) && exactGrokHookConfigured(
1669
+ grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1670
+ )) observedHooks.push('action_result');
1671
+ if (exactGrokHookConfigured(
1672
+ grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
1673
+ ) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
1674
+ } else {
1675
+ if (capabilityLevel === 'native_hooks'
1676
+ && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1677
+ if (capabilityLevel === 'native_hooks'
1678
+ && exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1679
+ if (capabilityLevel === 'native_hooks'
1680
+ && exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
1681
+ && exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1682
+ if (capabilityLevel === 'native_hooks'
1683
+ && exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1684
+ }
911
1685
  const passiveRuntime = safeRead(detection.paths.passiveRuntime);
912
1686
  if (capabilityLevel === 'sdk_passive_runtime'
913
1687
  && sdkDependency.present
@@ -930,17 +1704,40 @@ function activationProfile(detection, plan, changes, client) {
930
1704
  .sort()
931
1705
  .join('|');
932
1706
  const configFingerprint = capabilityLevel === 'native_hooks'
933
- ? claudeNativeHookFingerprint(claudeSettings)
1707
+ ? client === 'codex' ? codexNativeHookFingerprint(codexSettings)
1708
+ : client === 'cursor' || client === 'composer' ? cursorNativeHookFingerprint(cursorSettings)
1709
+ : client === 'cline' ? clineNativeHookFingerprint(detection)
1710
+ : client === 'windsurf' ? windsurfNativeHookFingerprint(windsurfSettings)
1711
+ : client === 'gemini' ? geminiNativeHookFingerprint(geminiSettings)
1712
+ : client === 'grok' ? grokNativeHookFingerprint(grokSettings)
1713
+ : claudeNativeHookFingerprint(claudeSettings)
934
1714
  : crypto.createHash('sha256')
935
1715
  .update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
936
1716
  .digest('hex');
937
1717
  const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
1718
+ const clineConflicts = changes
1719
+ .filter((change) => change.hook_conflict)
1720
+ .map((change) => change.label);
938
1721
  const exactFix = complete
939
- ? null
1722
+ ? client === 'cline'
1723
+ ? 'Enable Hooks in Cline, trust the project hook executables and workspace, then restart Cline. TaskComplete remains unverified and is not configured.'
1724
+ : client === 'windsurf'
1725
+ ? 'Restart Windsurf, trust the workspace hook configuration, and leave Restricted Mode before expecting hooks to run. MCP tools remain on demand.'
1726
+ : client === 'gemini'
1727
+ ? 'Restart Gemini CLI, open /hooks panel, and review and approve the project hook fingerprints. MCP tools remain on demand.'
1728
+ : client === 'grok'
1729
+ ? '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.'
1730
+ : null
940
1731
  : capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
941
1732
  ? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
942
1733
  : capabilityLevel === 'governed_wrapper'
943
1734
  ? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
1735
+ : client === 'cline' && clineConflicts.length > 0
1736
+ ? '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.'
1737
+ : client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
1738
+ ? '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.'
1739
+ : client === 'grok'
1740
+ ? `Run npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
944
1741
  : 'npx @getmarrow/install --repair';
945
1742
  return {
946
1743
  adapter_version: adapterVersion,
@@ -950,9 +1747,42 @@ function activationProfile(detection, plan, changes, client) {
950
1747
  observed_hooks: observedHooks,
951
1748
  evidence_authority: 'client_self_reported',
952
1749
  coverage_verified: false,
1750
+ passive_live: false,
953
1751
  configuration_complete: complete,
954
1752
  complete,
955
1753
  exact_fix: exactFix,
1754
+ ...(client === 'cline' ? {
1755
+ hook_conflicts: clineConflicts,
1756
+ task_complete_support: 'coming_soon_not_configured',
1757
+ task_completion_closure_verified: false,
1758
+ enable_hooks_required: true,
1759
+ executable_trust_required: true,
1760
+ } : {}),
1761
+ ...(client === 'windsurf' ? {
1762
+ restricted_mode_disables_hooks: true,
1763
+ restart_required: true,
1764
+ workspace_trust_required: true,
1765
+ mcp_tools: 'on_demand',
1766
+ } : {}),
1767
+ ...(client === 'gemini' ? {
1768
+ hooks_enabled: !geminiHooksExplicitlyDisabled(geminiSettings),
1769
+ explicit_disable_preserved: geminiHooksExplicitlyDisabled(geminiSettings),
1770
+ trust_review_required: true,
1771
+ restart_required: true,
1772
+ mcp_tools: 'on_demand',
1773
+ session_end_delivery_claimed: false,
1774
+ deterministic_closeout: 'AfterAgent',
1775
+ } : {}),
1776
+ ...(client === 'grok' ? {
1777
+ hooks_user_toggleable: true,
1778
+ hook_review_required: true,
1779
+ restart_required: true,
1780
+ global_hook_path: detection.paths.grokHooks,
1781
+ mcp_tools: 'on_demand',
1782
+ duplicate_session_end_configured: grokHasDuplicateSessionEnd(grokSettings),
1783
+ deterministic_closeout: 'Stop',
1784
+ governed_wrapper_fallback: 'explicit_bounded_only',
1785
+ } : {}),
956
1786
  };
957
1787
  }
958
1788
 
@@ -964,13 +1794,18 @@ function upsertMcpServerConfig(filePath, options = {}) {
964
1794
  const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
965
1795
  ? config.mcpServers
966
1796
  : {};
1797
+ const existingProfile = resolveToolProfile(servers.marrow?.env?.MARROW_TOOL_PROFILE).configured_profile;
1798
+ const requestedProfile = resolveToolProfile(options.toolProfile).configured_profile;
1799
+ const configuredProfile = requestedProfile === 'unset' ? existingProfile : requestedProfile;
1800
+ const env = {
1801
+ MARROW_BASE_URL: baseUrl,
1802
+ MARROW_FLEET_AGENT_ID: agentId,
1803
+ };
1804
+ if (configuredProfile !== 'unset') env.MARROW_TOOL_PROFILE = configuredProfile;
967
1805
  servers.marrow = {
968
1806
  command: 'npx',
969
1807
  args: ['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp'],
970
- env: {
971
- MARROW_BASE_URL: baseUrl,
972
- MARROW_FLEET_AGENT_ID: agentId,
973
- },
1808
+ env,
974
1809
  };
975
1810
  config.mcpServers = servers;
976
1811
  return JSON.stringify(config, null, 2) + '\n';
@@ -1075,7 +1910,74 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
1075
1910
  const detected = detectedClient(detection) === entry.client
1076
1911
  || (entry.client === 'claude-code' && detection.claudeCode)
1077
1912
  || (entry.client === 'cursor' && detection.cursor)
1913
+ || (entry.client === 'cline' && detection.cline)
1914
+ || (entry.client === 'windsurf' && detection.windsurf)
1915
+ || (entry.client === 'gemini' && detection.gemini)
1916
+ || (entry.client === 'grok' && detection.grok)
1078
1917
  || (entry.client === 'codex' && detection.codex);
1918
+ const codexSettings = entry.client === 'codex' ? safeJsonObject(detection.paths.codexHooks) : null;
1919
+ const codexConfigured = Boolean(codexSettings
1920
+ && exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)
1921
+ && exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
1922
+ && exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
1923
+ && exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND));
1924
+ const cursorSettings = ['cursor', 'composer'].includes(entry.client) ? safeJsonObject(detection.paths.cursorHooks) : null;
1925
+ const cursorConfigured = Boolean(cursorSettings
1926
+ && exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1927
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
1928
+ })
1929
+ && exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1930
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1931
+ })
1932
+ && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1933
+ timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1934
+ })
1935
+ && exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1936
+ timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1937
+ }));
1938
+ const clineConfigured = entry.client === 'cline'
1939
+ && clineHookContract(detection).every((hook) => exactExecutableFile(hook.path, hook.content));
1940
+ const windsurfSettings = entry.client === 'windsurf' ? safeJsonObject(detection.paths.windsurfHooks) : null;
1941
+ const windsurfConfigured = Boolean(windsurfSettings
1942
+ && WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
1943
+ windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1944
+ ))
1945
+ && WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
1946
+ windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1947
+ ))
1948
+ && exactWindsurfHookConfigured(
1949
+ windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1950
+ ));
1951
+ const geminiSettings = entry.client === 'gemini' ? safeJsonObject(detection.paths.geminiSettings) : null;
1952
+ const geminiConfigured = Boolean(geminiSettings
1953
+ && !geminiHooksExplicitlyDisabled(geminiSettings)
1954
+ && exactGeminiHookConfigured(
1955
+ geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1956
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1957
+ )
1958
+ && exactGeminiHookConfigured(
1959
+ geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1960
+ GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1961
+ )
1962
+ && exactGeminiHookConfigured(
1963
+ geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1964
+ undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1965
+ ));
1966
+ const grokSettings = entry.client === 'grok' ? safeJsonObject(detection.paths.grokHooks) : null;
1967
+ const grokConfigured = Boolean(grokSettings
1968
+ && exactGrokHookConfigured(
1969
+ grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
1970
+ )
1971
+ && exactGrokHookConfigured(
1972
+ grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1973
+ )
1974
+ && exactGrokHookConfigured(
1975
+ grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1976
+ )
1977
+ && exactGrokHookConfigured(
1978
+ grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
1979
+ )
1980
+ && !grokHasDuplicateSessionEnd(grokSettings));
1079
1981
  return {
1080
1982
  client: entry.client,
1081
1983
  capability_level: entry.capability_level,
@@ -1085,12 +1987,35 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
1085
1987
  mcp: true,
1086
1988
  instructions: true,
1087
1989
  sdk_passive_runtime: node,
1088
- native_hooks: entry.capability_level === 'native_hooks' && Boolean(detection.claudeCode),
1990
+ native_hooks: entry.capability_level === 'native_hooks' && Boolean(
1991
+ entry.client === 'claude-code' ? detection.claudeCode
1992
+ : entry.client === 'codex' ? detection.codex
1993
+ : ['cursor', 'composer'].includes(entry.client) ? detection.cursor
1994
+ : entry.client === 'cline' ? detection.cline
1995
+ : entry.client === 'windsurf' ? detection.windsurf
1996
+ : entry.client === 'gemini' ? detection.gemini
1997
+ : entry.client === 'grok' ? detection.grok
1998
+ : false,
1999
+ ),
1089
2000
  governed_wrapper: entry.capability_level === 'governed_wrapper',
1090
2001
  },
1091
- configured_locally: detected && entry.automatic.length > 0,
2002
+ configured_locally: entry.client === 'codex' ? codexConfigured
2003
+ : ['cursor', 'composer'].includes(entry.client) ? cursorConfigured
2004
+ : entry.client === 'cline' ? clineConfigured
2005
+ : entry.client === 'windsurf' ? windsurfConfigured
2006
+ : entry.client === 'gemini' ? geminiConfigured
2007
+ : entry.client === 'grok' ? grokConfigured
2008
+ : detected && entry.automatic.length > 0,
1092
2009
  verified_passive: false,
1093
- unsupported_claim: entry.capability_level === 'event_contract'
2010
+ unsupported_claim: entry.client === 'cline'
2011
+ ? 'TaskComplete is documented as coming soon and is not configured or counted as observed coverage.'
2012
+ : entry.client === 'windsurf'
2013
+ ? 'Restricted Mode disables hooks; configuration requires restart and trust review and never verifies passive coverage.'
2014
+ : entry.client === 'gemini'
2015
+ ? '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.'
2016
+ : entry.client === 'grok'
2017
+ ? 'Global hooks are user-toggleable; restart and /hooks inspection are required, configuration remains client-self-reported, and duplicate SessionEnd closeout is forbidden.'
2018
+ : entry.capability_level === 'event_contract'
1094
2019
  ? 'Needs a bounded event adapter. MCP tools remain on demand.'
1095
2020
  : null,
1096
2021
  };
@@ -1131,18 +2056,60 @@ function buildPlan(detection, options) {
1131
2056
  transform: upsertClaudeHooks,
1132
2057
  });
1133
2058
  }
2059
+ if (detection.codex) {
2060
+ writes.push({
2061
+ type: 'json-transform',
2062
+ path: detection.paths.codexHooks,
2063
+ label: 'Codex native hooks',
2064
+ transform: upsertCodexHooks,
2065
+ });
2066
+ }
2067
+ if (detection.cline) {
2068
+ for (const hook of clineHookContract(detection)) {
2069
+ writes.push({
2070
+ type: 'owned-executable',
2071
+ path: hook.path,
2072
+ label: hook.label,
2073
+ content: hook.content,
2074
+ mode: 0o755,
2075
+ conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
2076
+ });
2077
+ }
2078
+ }
2079
+ if (detection.windsurf) {
2080
+ writes.push({
2081
+ type: 'json-transform',
2082
+ path: detection.paths.windsurfHooks,
2083
+ label: 'Windsurf native hooks',
2084
+ transform: upsertWindsurfHooks,
2085
+ });
2086
+ }
2087
+ if (detection.gemini) {
2088
+ writes.push({
2089
+ type: 'json-transform',
2090
+ path: detection.paths.geminiSettings,
2091
+ label: 'Gemini CLI native hooks',
2092
+ transform: upsertGeminiHooks,
2093
+ });
2094
+ }
1134
2095
  writes.push({
1135
2096
  type: 'json-transform',
1136
2097
  path: detection.paths.mcpJson,
1137
2098
  label: 'Project MCP server config',
1138
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
2099
+ transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
1139
2100
  });
1140
2101
  if (detection.cursor) {
2102
+ writes.push({
2103
+ type: 'json-transform',
2104
+ path: detection.paths.cursorHooks,
2105
+ label: 'Cursor native hooks',
2106
+ transform: upsertCursorHooks,
2107
+ });
1141
2108
  writes.push({
1142
2109
  type: 'json-transform',
1143
2110
  path: detection.paths.cursorMcp,
1144
2111
  label: 'Cursor MCP server config',
1145
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
2112
+ transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
1146
2113
  });
1147
2114
  }
1148
2115
  }
@@ -1229,8 +2196,10 @@ function applyPlan(plan, options) {
1229
2196
  const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
1230
2197
  for (const write of plan.writes) assertContainedManagedTarget(root, write.path);
1231
2198
  const prepared = plan.writes.map((write) => {
2199
+ const fileExists = exists(write.path);
1232
2200
  const before = safeRead(write.path);
1233
2201
  let after;
2202
+ let hookConflict = false;
1234
2203
  if (write.type === 'file') {
1235
2204
  if (write.overwrite === false && before) {
1236
2205
  after = before;
@@ -1241,27 +2210,43 @@ function applyPlan(plan, options) {
1241
2210
  after = upsertBlock(before, write.block);
1242
2211
  } else if (write.type === 'json-transform') {
1243
2212
  after = write.transform(write.path);
2213
+ } else if (write.type === 'owned-executable') {
2214
+ if (fileExists && before !== write.content) {
2215
+ after = before;
2216
+ hookConflict = true;
2217
+ } else {
2218
+ after = write.content;
2219
+ }
1244
2220
  } else {
1245
2221
  throw new Error(`Unknown write type: ${write.type}`);
1246
2222
  }
1247
2223
 
1248
- return { write, before, after };
2224
+ const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
2225
+ const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
2226
+ return { write, before, after, hookConflict, modeChanged };
1249
2227
  });
1250
2228
 
1251
2229
  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);
2230
+ for (const { write, before, after, hookConflict, modeChanged } of prepared) {
2231
+ const contentChanged = before !== after;
2232
+ const changed = !hookConflict && (contentChanged || modeChanged);
2233
+ const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor && !hookConflict);
1255
2234
  changes.push({
1256
2235
  path: write.path,
1257
2236
  label: write.label,
1258
2237
  changed,
1259
2238
  applied: changed && writeApplied,
1260
- already_present: !changed,
2239
+ already_present: !changed && !hookConflict,
2240
+ hook_conflict: hookConflict,
2241
+ ...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
1261
2242
  });
1262
- if (changed && writeApplied) {
2243
+ if (contentChanged && writeApplied) {
1263
2244
  atomicWriteManagedFile(root, write.path, after);
1264
2245
  }
2246
+ if (modeChanged && writeApplied) {
2247
+ assertContainedManagedTarget(root, write.path);
2248
+ fs.chmodSync(write.path, write.mode);
2249
+ }
1265
2250
  }
1266
2251
  return changes;
1267
2252
  }
@@ -1298,12 +2283,14 @@ function runtimeGateVerified(runtime) {
1298
2283
  }
1299
2284
 
1300
2285
  async function runSelfTest(options) {
1301
- if (!options.selfTest) return { skipped: true, reason: 'disabled' };
2286
+ const initialProfile = initialToolProfileReport(options.toolProfile);
2287
+ if (!options.selfTest) return { skipped: true, reason: 'disabled', mcp_tool_profile: initialProfile };
1302
2288
  if (!options.apiKey) {
1303
2289
  return {
1304
2290
  skipped: true,
1305
2291
  reason: 'missing MARROW_API_KEY',
1306
2292
  exact_fix: 'export MARROW_API_KEY=mrw_live_... && npx @getmarrow/install --repair',
2293
+ mcp_tool_profile: initialProfile,
1307
2294
  };
1308
2295
  }
1309
2296
 
@@ -1349,6 +2336,14 @@ async function runSelfTest(options) {
1349
2336
  });
1350
2337
 
1351
2338
  const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
2339
+ const context = await requestJson(`${baseUrl}/v1/agent/context`, { headers })
2340
+ .catch(() => null);
2341
+ const toolProfile = buildMcpToolProfileReport(
2342
+ options.toolProfile,
2343
+ status.mcp_tool_profile,
2344
+ context?.primary_tool_availability,
2345
+ Boolean(options.activation),
2346
+ );
1352
2347
  const runtime = await requestJson(`${baseUrl}/v1/agent/runtime`, {
1353
2348
  method: 'POST',
1354
2349
  headers,
@@ -1483,6 +2478,7 @@ async function runSelfTest(options) {
1483
2478
  }
1484
2479
  return {
1485
2480
  skipped: false,
2481
+ mcp_tool_profile: toolProfile,
1486
2482
  decision_id: decisionId,
1487
2483
  active: Boolean(status.enabled ?? status.ok),
1488
2484
  health: status.health || null,
@@ -1677,6 +2673,24 @@ function printReport(report) {
1677
2673
  }
1678
2674
 
1679
2675
  process.stdout.write('\nSelf-test:\n');
2676
+ const toolProfile = report.selfTest.mcp_tool_profile || report.toolProfile;
2677
+ if (toolProfile) {
2678
+ process.stdout.write(`- configured tool profile: ${toolProfile.configured_profile}\n`);
2679
+ process.stdout.write(`- effective tool profile: ${toolProfile.effective_profile}\n`);
2680
+ process.stdout.write(`- expected visible tools: ${toolProfile.expected_visible_count == null ? 'complete catalog (awaiting reloaded MCP count)' : toolProfile.expected_visible_count}\n`);
2681
+ process.stdout.write(`- actual visible tools: ${toolProfile.actual_visible_count == null ? 'unavailable until process reload' : toolProfile.actual_visible_count}\n`);
2682
+ process.stdout.write(`- visible tool names: ${toolProfile.visibility_live ? toolProfile.visible_tool_names.join(', ') : 'unavailable until process reload'}\n`);
2683
+ process.stdout.write(`- profile live: ${toolProfile.visibility_live ? 'yes' : 'no'}\n`);
2684
+ const projection = toolProfile.backend_entitlement_projection;
2685
+ const availability = projection?.primary_tool_availability;
2686
+ if (projection?.evidence_state === 'available' && availability?.entitlement_evidence?.state === 'available') {
2687
+ process.stdout.write(`- backend-projected entitled tools: ${availability.counts.entitled}\n`);
2688
+ process.stdout.write(`- backend-projected upgrade-required tools: ${availability.counts.upgrade_required}\n`);
2689
+ process.stdout.write(`- backend projection source: ${projection.source}; authorizes calls: no\n`);
2690
+ } else {
2691
+ process.stdout.write(`- backend-projected entitlements: unavailable (source: ${projection?.source || 'backend_projection_not_provided'}; non-authorizing)\n`);
2692
+ }
2693
+ }
1680
2694
  if (report.selfTest.skipped) {
1681
2695
  process.stdout.write(`- skipped: ${report.selfTest.reason}\n`);
1682
2696
  if (report.selfTest.exact_fix) process.stdout.write(`- exact fix: ${report.selfTest.exact_fix}\n`);
@@ -1836,9 +2850,15 @@ async function install(options) {
1836
2850
  if (options.activate && options.selfTest === false) {
1837
2851
  throw new Error('activate requires the server self-test');
1838
2852
  }
2853
+ if (options.activate && !String(options.apiKey || '').trim()) {
2854
+ throw new Error('activation requires MARROW_API_KEY from the process environment or trusted secret storage');
2855
+ }
1839
2856
  if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
1840
2857
  throw new Error('repair requires explicit write authorization (--yes)');
1841
2858
  }
2859
+ options.toolProfile = resolveToolProfile(options.toolProfile === undefined
2860
+ ? process.env.MARROW_TOOL_PROFILE
2861
+ : options.toolProfile);
1842
2862
  const detection = detectEnvironment(options.cwd);
1843
2863
  const client = detectedClient(detection);
1844
2864
  options.client = client;
@@ -1860,6 +2880,7 @@ async function install(options) {
1860
2880
  configuration_complete: changes.every((change) => change.applied || change.already_present),
1861
2881
  evidence_authority: 'client_self_reported',
1862
2882
  coverage_verified: false,
2883
+ passive_live: false,
1863
2884
  adapter_version: profile.adapter_version,
1864
2885
  capability_level: profile.capability_level,
1865
2886
  config_fingerprint: profile.config_fingerprint,
@@ -1884,20 +2905,38 @@ async function install(options) {
1884
2905
  } catch (error) {
1885
2906
  const message = error instanceof Error ? error.message : String(error);
1886
2907
  if (options.activate) throw new Error(`Marrow activation failed: ${message}`);
1887
- selfTest = { skipped: false, active: false, error: message };
2908
+ selfTest = {
2909
+ skipped: false,
2910
+ active: false,
2911
+ error: message,
2912
+ mcp_tool_profile: initialToolProfileReport(options.toolProfile),
2913
+ };
1888
2914
  }
1889
2915
  if (options.activate && !selfTest.activation_verified) {
1890
2916
  throw new Error('Marrow activation failed: server confirmation was not returned');
1891
2917
  }
2918
+ const harnessReload = harnessReloadPlan(detection, changes);
2919
+ if (harnessReload.required && selfTest.mcp_tool_profile) {
2920
+ selfTest.mcp_tool_profile = {
2921
+ ...selfTest.mcp_tool_profile,
2922
+ visible_tool_count: null,
2923
+ actual_visible_count: null,
2924
+ visible_tool_names: [],
2925
+ visibility_live: false,
2926
+ reload_required: true,
2927
+ };
2928
+ }
1892
2929
  const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
1893
2930
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
1894
2931
  const controllerPlatform = options.controllerPlatform || process.platform;
2932
+ const localControl = localControlEvidence({ apiKey: options.apiKey, home: options.controlHome });
1895
2933
  let controller = await controllerStatus({
1896
2934
  root: detection.root,
1897
2935
  agentId: options.agentId,
1898
2936
  platform: controllerPlatform,
1899
2937
  });
1900
2938
  const shouldEnsureController = options.controller !== false
2939
+ && localControl.enabled
1901
2940
  && controllerSupportedPlatform(controllerPlatform)
1902
2941
  && Boolean(options.apiKey)
1903
2942
  && selfTestPassed
@@ -1948,11 +2987,13 @@ async function install(options) {
1948
2987
  adapterProvenance: ADAPTER_PROVENANCE,
1949
2988
  mode: plan.mode,
1950
2989
  writeMode,
2990
+ toolProfile: selfTest.mcp_tool_profile || initialToolProfileReport(options.toolProfile),
1951
2991
  detected: {
1952
2992
  node: detection.node,
1953
2993
  python: detection.python,
1954
2994
  claudeCode: detection.claudeCode,
1955
2995
  cursor: detection.cursor,
2996
+ grok: detection.grok,
1956
2997
  codex: detection.codex,
1957
2998
  openclaw: detection.openclaw,
1958
2999
  mcpConfig: detection.mcpConfig,
@@ -1969,7 +3010,7 @@ async function install(options) {
1969
3010
  receipt: selfTest.activation_receipt || null,
1970
3011
  profile,
1971
3012
  },
1972
- harnessReload: harnessReloadPlan(detection, changes),
3013
+ harnessReload,
1973
3014
  firstCapture: firstCapturePath(detection, options.agentId),
1974
3015
  changes,
1975
3016
  doctor: {
@@ -1991,6 +3032,7 @@ async function install(options) {
1991
3032
  configRepairs,
1992
3033
  sdkDependency,
1993
3034
  controller,
3035
+ local_control: localControl,
1994
3036
  selfTest,
1995
3037
  warnings: [
1996
3038
  ...(options.keyFromArg
@@ -2038,7 +3080,21 @@ module.exports = {
2038
3080
  stableAgentId,
2039
3081
  activationProfile,
2040
3082
  claudeNativeHookFingerprint,
3083
+ codexNativeHookFingerprint,
3084
+ cursorNativeHookFingerprint,
3085
+ clineNativeHookFingerprint,
3086
+ windsurfNativeHookFingerprint,
3087
+ geminiNativeHookFingerprint,
3088
+ grokNativeHookFingerprint,
3089
+ GROK_CONTEXT_HOOK_COMMAND,
3090
+ GROK_PRE_ACTION_HOOK_COMMAND,
3091
+ GROK_ACTION_RESULT_HOOK_COMMAND,
3092
+ GROK_SESSION_END_HOOK_COMMAND,
3093
+ GROK_NATIVE_HOOK_MATCHER,
2041
3094
  printReport,
3095
+ buildMcpToolProfileReport,
3096
+ resolveToolProfile,
3097
+ PRIMARY_TOOL_NAMES,
2042
3098
  ADAPTER_PROVENANCE,
2043
3099
  HARNESS_CAPABILITY_REGISTRY,
2044
3100
  defaultHarnessInstallMatrix,