fullcourtdefense-cli 1.7.23 → 1.7.24

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.
@@ -108,6 +108,90 @@ function inferEvent(explicit, payload) {
108
108
  return 'unknown';
109
109
  }
110
110
  const str = (v) => (typeof v === 'string' ? v : '');
111
+ /**
112
+ * Claude-format hooks (Claude Code, VS Code Copilot agent mode, GitHub Copilot
113
+ * CLI — all share the same schema) send `hook_event_name` + `tool_name` +
114
+ * `tool_input` on stdin. Normalize that payload into the field shapes the rest
115
+ * of this file already understands (command/file_path/tool_name/tool_input),
116
+ * and map the tool onto our event taxonomy.
117
+ *
118
+ * Returns null when the payload is not Claude-format.
119
+ */
120
+ function normalizeClaudePayload(payload) {
121
+ const hookEventName = str(payload.hook_event_name);
122
+ if (!hookEventName)
123
+ return null;
124
+ if (hookEventName === 'UserPromptSubmit') {
125
+ return { event: 'prompt', payload };
126
+ }
127
+ if (hookEventName !== 'PreToolUse') {
128
+ // PostToolUse / Stop / SessionStart etc. — nothing to enforce pre-execution.
129
+ return { event: 'ignore', payload };
130
+ }
131
+ const toolName = str(payload.tool_name);
132
+ let toolInput = payload.tool_input;
133
+ if (typeof toolInput === 'string') {
134
+ const trimmed = toolInput.trim();
135
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
136
+ try {
137
+ toolInput = JSON.parse(trimmed);
138
+ }
139
+ catch { /* keep raw */ }
140
+ }
141
+ }
142
+ const input = (toolInput && typeof toolInput === 'object' ? toolInput : {});
143
+ // MCP tools arrive as `mcp__<server>__<tool>` (same in Claude Code and VS Code).
144
+ if (toolName.startsWith('mcp__')) {
145
+ const parts = toolName.split('__');
146
+ const server = parts[1] || 'mcp';
147
+ const shortTool = parts.slice(2).join('__') || toolName;
148
+ return {
149
+ event: 'mcp',
150
+ payload: { ...payload, tool_name: shortTool, mcp_server_name: server, tool_input: input },
151
+ };
152
+ }
153
+ // Claude Code, VS Code Copilot, and Copilot CLI each use their own tool names
154
+ // for the same operations — compare case/underscore-insensitively.
155
+ const canon = toolName.toLowerCase().replace(/[_-]/g, '');
156
+ const SHELL_TOOLS = new Set(['bash', 'shell', 'runterminalcommand', 'runinterminal', 'powershell', 'terminal']);
157
+ const READ_TOOLS = new Set(['read', 'readfile', 'view']);
158
+ const WRITE_TOOLS = new Set([
159
+ 'write', 'edit', 'multiedit', 'notebookedit', 'createfile', 'editfiles',
160
+ 'replacestringinfile', 'editfile', 'applypatch', 'strreplaceeditor', 'insertedittofile',
161
+ ]);
162
+ if (SHELL_TOOLS.has(canon)) {
163
+ return {
164
+ event: 'shell',
165
+ payload: { ...payload, command: str(input.command), cwd: str(input.cwd) || str(payload.cwd) || undefined },
166
+ };
167
+ }
168
+ if (READ_TOOLS.has(canon)) {
169
+ return { event: 'read', payload: { ...payload, file_path: str(input.file_path) || str(input.path) } };
170
+ }
171
+ if (WRITE_TOOLS.has(canon)) {
172
+ const content = str(input.content) || str(input.new_string) || str(input.new_source) || str(input.code)
173
+ || (Array.isArray(input.edits)
174
+ ? input.edits.map((e) => str(e?.new_string) || str(e?.content)).filter(Boolean).join('\n')
175
+ : '');
176
+ const filePath = str(input.file_path) || str(input.notebook_path) || str(input.path) || str(input.filePath)
177
+ || (Array.isArray(input.files) ? input.files.map((f) => str(f)).filter(Boolean).join(', ') : '');
178
+ return { event: 'file', payload: { ...payload, file_path: filePath, content } };
179
+ }
180
+ // Glob/Grep/Task/WebFetch/etc. — no enforcement surface; let the client's own
181
+ // permission flow handle them.
182
+ return { event: 'ignore', payload };
183
+ }
184
+ /**
185
+ * Which AI client invoked a Claude-format hook. Claude Code sets CLAUDECODE=1;
186
+ * VS Code Copilot runs hooks inside the VS Code process environment.
187
+ */
188
+ function claudeFormatClient() {
189
+ if (process.env.CLAUDECODE || process.env.CLAUDE_PROJECT_DIR)
190
+ return 'claude-code';
191
+ if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === 'vscode')
192
+ return 'vscode';
193
+ return 'claude-code';
194
+ }
111
195
  function collectPromptStrings(value, out = [], depth = 0) {
112
196
  if (depth > 5 || value === null || value === undefined)
113
197
  return out;
@@ -240,6 +324,22 @@ function safeJson(v) {
240
324
  function degradedAllowMessage(event, detail) {
241
325
  return `FullCourtDefense policy gate unavailable; allowed ${event} in degraded mode. ${detail}`.trim();
242
326
  }
327
+ /**
328
+ * The policy gate could not be reached (or errored). Apply the machine's
329
+ * configured offline stance: fail-closed blocks the action, fail-open allows it
330
+ * in degraded mode. Either way the decision is spooled (`offlineEnforced`) and
331
+ * replayed to the backend when connectivity returns, so nothing is invisible.
332
+ */
333
+ function respondDegraded(ctx, detail, toolName) {
334
+ if (ctx.failClosed && !ctx.shadow) {
335
+ (0, telemetry_1.spoolEvent)({ decision: 'block', toolName, reason: `fail-closed: ${detail}`, offlineEnforced: true });
336
+ (0, telemetry_1.triggerFlush)(true);
337
+ ctx.respond(true, `Blocked by FullCourtDefense — policy service unreachable and this machine is set to fail-closed. ${detail}`, `FullCourtDefense could not verify this ${ctx.event} against your org policies (${detail}) and fail-closed mode is on. Do not retry until the connection is restored.`);
338
+ }
339
+ (0, telemetry_1.spoolEvent)({ decision: 'allow', toolName, reason: `degraded: ${detail}`, offlineEnforced: true });
340
+ (0, telemetry_1.triggerFlush)(false);
341
+ ctx.respond(false, undefined, degradedAllowMessage(ctx.event, detail));
342
+ }
243
343
  /** Per-event attribution that maps onto the existing Shield runtime headers (prompt path). */
244
344
  function attributionFor(event, p) {
245
345
  const h = {};
@@ -294,13 +394,13 @@ function mcpServerName(p) {
294
394
  * The client is still reported separately as `agentClient` metadata for optional per-runtime
295
395
  * constraints. Non-MCP events keep the collapsed per-developer IDE-runtime identity.
296
396
  */
297
- function agentNameForCall(event, p) {
397
+ function agentNameForCall(event, p, client = 'cursor') {
298
398
  if (event === 'mcp') {
299
399
  const server = mcpServerName(p);
300
400
  if (server)
301
401
  return `${safeIdentityPart(developerId())}-${safeIdentityPart(server)}`;
302
402
  }
303
- return `cursor-${developerId()}`;
403
+ return `${client}-${developerId()}`;
304
404
  }
305
405
  /** Cursor conversation/session id when present, else a stable per-machine id. */
306
406
  function sessionId(p) {
@@ -309,7 +409,7 @@ function sessionId(p) {
309
409
  return fromPayload;
310
410
  return `cursor-${os.hostname()}`;
311
411
  }
312
- function machineMetadata() {
412
+ function machineMetadata(client = 'cursor') {
313
413
  let username = 'unknown';
314
414
  try {
315
415
  username = os.userInfo().username;
@@ -322,7 +422,7 @@ function machineMetadata() {
322
422
  username,
323
423
  cwd: process.cwd(),
324
424
  workspacePath: process.env.WORKSPACE_PATH || process.env.CLAUDE_PROJECT_DIR || process.cwd(),
325
- agentClient: 'cursor',
425
+ agentClient: client,
326
426
  };
327
427
  }
328
428
  function spoolLocalFinding(input) {
@@ -405,28 +505,79 @@ async function hookCommand(args, config) {
405
505
  payload = {};
406
506
  }
407
507
  }
408
- const event = inferEvent(args.event, payload);
409
- dbg({ phase: 'invoke', event, argEvent: args.event, pid: process.pid,
508
+ // Claude-format hooks (Claude Code / VS Code Copilot / Copilot CLI) are
509
+ // detected from the payload itself, so ONE installed command serves every
510
+ // client that reads ~/.claude/settings.json.
511
+ const claudeNormalized = normalizeClaudePayload(payload);
512
+ const hookFormat = claudeNormalized ? 'claude' : 'cursor';
513
+ const client = claudeNormalized ? claudeFormatClient() : 'cursor';
514
+ let event;
515
+ let ignoreEvent = false;
516
+ if (claudeNormalized) {
517
+ payload = claudeNormalized.payload;
518
+ if (claudeNormalized.event === 'ignore') {
519
+ event = 'unknown';
520
+ ignoreEvent = true;
521
+ }
522
+ else {
523
+ event = claudeNormalized.event;
524
+ }
525
+ }
526
+ else {
527
+ event = inferEvent(args.event, payload);
528
+ }
529
+ dbg({ phase: 'invoke', event, argEvent: args.event, format: hookFormat, client, pid: process.pid,
410
530
  isTTY, rawLen: raw.length, rawPreview: raw.slice(0, 300), stdinErr, parseErr,
411
531
  argv: process.argv.slice(2), payloadKeys: Object.keys(payload) });
412
- // Emit the verdict in the shape Cursor expects for THIS event, then exit.
413
- // beforeSubmitPrompt blocks via { continue: false }; the execution hooks
414
- // (shell/mcp/read) block via { permission: "deny" }.
532
+ // Emit the verdict in the shape THIS client expects, then exit.
533
+ // Cursor: beforeSubmitPrompt blocks via { continue: false }; execution hooks
534
+ // block via { permission: "deny" }.
535
+ // Claude format (Claude Code / VS Code / Copilot CLI):
536
+ // UserPromptSubmit blocks via { decision: "block" }; PreToolUse blocks
537
+ // via hookSpecificOutput.permissionDecision "deny". On allow we emit
538
+ // NO permission decision so the client's own approval flow still runs
539
+ // (an explicit "allow" would silently bypass its permission prompts).
415
540
  const respond = (blocked, userMsg, agentMsg) => {
416
- const decision = event === 'prompt'
417
- ? (blocked ? { continue: false } : { continue: true })
418
- : { permission: blocked ? 'deny' : 'allow' };
419
- if (userMsg)
420
- decision.user_message = userMsg;
421
- if (agentMsg)
422
- decision.agent_message = agentMsg;
423
- dbg({ phase: 'verdict', event, blocked, decision });
541
+ let decision;
542
+ if (hookFormat === 'claude') {
543
+ if (event === 'prompt') {
544
+ decision = blocked ? { decision: 'block', reason: userMsg || agentMsg || 'Blocked by FullCourtDefense.' } : {};
545
+ }
546
+ else if (blocked) {
547
+ decision = {
548
+ hookSpecificOutput: {
549
+ hookEventName: 'PreToolUse',
550
+ permissionDecision: 'deny',
551
+ permissionDecisionReason: userMsg || agentMsg || 'Blocked by FullCourtDefense.',
552
+ },
553
+ };
554
+ }
555
+ else {
556
+ decision = {};
557
+ }
558
+ if (!blocked && agentMsg)
559
+ decision.systemMessage = agentMsg;
560
+ }
561
+ else {
562
+ decision = event === 'prompt'
563
+ ? (blocked ? { continue: false } : { continue: true })
564
+ : { permission: blocked ? 'deny' : 'allow' };
565
+ if (userMsg)
566
+ decision.user_message = userMsg;
567
+ if (agentMsg)
568
+ decision.agent_message = agentMsg;
569
+ }
570
+ dbg({ phase: 'verdict', event, format: hookFormat, blocked, decision });
424
571
  process.stdout.write(JSON.stringify(decision));
425
- // ALWAYS exit 0 so Cursor uses our JSON verdict. Exit 2 is interpreted as
572
+ // ALWAYS exit 0 so the client uses our JSON verdict. Exit 2 is interpreted as
426
573
  // `permission: "deny"`, which beforeSubmitPrompt ignores (it uses `continue`),
427
574
  // so exiting 2 would silently let blocked prompts through.
428
575
  process.exit(0);
429
576
  };
577
+ // Claude-format lifecycle events with no pre-execution surface (PostToolUse,
578
+ // SessionStart, ...) and tools we don't police (Glob/Grep/Task/...) — no opinion.
579
+ if (ignoreEvent)
580
+ respond(false);
430
581
  // No Shield configured → allow (fail-open by design: a misconfigured machine
431
582
  // must not brick the developer's IDE).
432
583
  if (!shieldId) {
@@ -437,7 +588,7 @@ async function hookCommand(args, config) {
437
588
  await enforceActionPolicy({
438
589
  event, payload, apiUrl: apiUrl, shieldId: shieldId, shieldKey,
439
590
  shadow, failClosed, timeoutMs, approvalMode, approvalTimeoutMs, approvalPollMs, respond,
440
- effectivePolicyHash,
591
+ effectivePolicyHash, client,
441
592
  });
442
593
  return;
443
594
  }
@@ -472,7 +623,7 @@ async function hookCommand(args, config) {
472
623
  }
473
624
  await enforceShieldText({
474
625
  event, text, payload, apiUrl: apiUrl, shieldId: shieldId, shieldKey,
475
- shadow, failClosed, timeoutMs, respond,
626
+ shadow, failClosed, timeoutMs, respond, client,
476
627
  });
477
628
  return;
478
629
  }
@@ -544,37 +695,34 @@ async function enforceActionPolicy(ctx) {
544
695
  // For MCP calls this mirrors the gateway's runtime-agnostic per-server identity
545
696
  // (`<developer>-<server>`), so ONE per-agent policy matches the same server on the
546
697
  // gateway path AND this in-IDE hook path, across every client/runtime.
547
- agentName: agentNameForCall(event, payload),
698
+ agentName: agentNameForCall(event, payload, ctx.client),
548
699
  toolName: call.toolName,
549
700
  toolArgs: call.toolArgs,
550
701
  argsSummary: call.toolArgs,
551
702
  sessionId: sessionId(payload),
552
703
  source: 'cursor_hook',
553
704
  ...(event === 'mcp' && mcpServerName(payload) ? { mcpServer: mcpServerName(payload) } : {}),
554
- ...machineMetadata(),
705
+ ...machineMetadata(ctx.client),
555
706
  }),
556
707
  signal: controller.signal,
557
708
  });
558
709
  clearTimeout(timer);
559
710
  if (!resp.ok) {
560
- dbg({ phase: 'policy_http_error', event, status: resp.status });
561
- (0, telemetry_1.spoolEvent)({ decision: 'allow', toolName: call.toolName, reason: `degraded: backend HTTP ${resp.status}`, offlineEnforced: true });
562
- respond(false, undefined, degradedAllowMessage(event, `Backend returned HTTP ${resp.status}.`));
711
+ dbg({ phase: 'policy_http_error', event, status: resp.status, failClosed: ctx.failClosed });
712
+ respondDegraded(ctx, `Backend returned HTTP ${resp.status}.`, call.toolName);
563
713
  return;
564
714
  }
565
715
  const body = await resp.json().catch(() => ({}));
566
716
  if (!body.success || !body.data) {
567
- dbg({ phase: 'policy_no_data', event, error: body.error });
568
- respond(false, undefined, degradedAllowMessage(event, body.error ? `Backend error: ${body.error}` : 'Backend returned no policy data.'));
717
+ dbg({ phase: 'policy_no_data', event, error: body.error, failClosed: ctx.failClosed });
718
+ respondDegraded(ctx, body.error ? `Backend error: ${body.error}` : 'Backend returned no policy data.', call.toolName);
569
719
  return;
570
720
  }
571
721
  result = body.data;
572
722
  }
573
723
  catch (err) {
574
- dbg({ phase: 'policy_exception', event, error: err instanceof Error ? err.message : String(err) });
575
- (0, telemetry_1.spoolEvent)({ decision: 'allow', toolName: call.toolName, reason: `degraded: ${err instanceof Error ? err.message : String(err)}`, offlineEnforced: true });
576
- (0, telemetry_1.triggerFlush)(false);
577
- respond(false, undefined, degradedAllowMessage(event, `Hook error: ${err instanceof Error ? err.message : String(err)}.`));
724
+ dbg({ phase: 'policy_exception', event, error: err instanceof Error ? err.message : String(err), failClosed: ctx.failClosed });
725
+ respondDegraded(ctx, `Hook error: ${err instanceof Error ? err.message : String(err)}.`, call.toolName);
578
726
  return;
579
727
  }
580
728
  const reason = result.actionPolicy?.reason
@@ -646,7 +794,7 @@ async function waitForApproval(input) {
646
794
  return { status: 'timeout', message: 'approval timed out — still waiting for human review.' };
647
795
  }
648
796
  async function enforceShieldText(ctx) {
649
- const { event, text, payload, apiUrl, shieldId, shieldKey, shadow, failClosed, timeoutMs, respond } = ctx;
797
+ const { event, text, payload, apiUrl, shieldId, shieldKey, shadow, timeoutMs, respond } = ctx;
650
798
  try {
651
799
  const controller = new AbortController();
652
800
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -667,7 +815,7 @@ async function enforceShieldText(ctx) {
667
815
  });
668
816
  clearTimeout(timer);
669
817
  if (!resp.ok) {
670
- respond(false, undefined, degradedAllowMessage(event, `Backend returned HTTP ${resp.status}.`));
818
+ respondDegraded(ctx, `Backend returned HTTP ${resp.status}.`, 'prompt');
671
819
  return;
672
820
  }
673
821
  const data = await resp.json().catch(() => ({}));
@@ -683,6 +831,6 @@ async function enforceShieldText(ctx) {
683
831
  respond(true, `Blocked by FullCourtDefense — ${reason}.`, `FullCourtDefense blocked this ${event} as "${reason}". Do not retry; revise to remove the flagged content.`);
684
832
  }
685
833
  catch (err) {
686
- respond(false, undefined, degradedAllowMessage(event, `Hook error: ${err instanceof Error ? err.message : String(err)}.`));
834
+ respondDegraded(ctx, `Hook error: ${err instanceof Error ? err.message : String(err)}.`, 'prompt');
687
835
  }
688
836
  }
@@ -6,6 +6,8 @@ export interface InstallAllArgs extends ProtectAllArgs {
6
6
  upload?: string;
7
7
  hooks?: string;
8
8
  discover?: string;
9
+ schedule?: string;
10
+ scheduleHour?: string;
9
11
  autoProtect?: string;
10
12
  intervalMinutes?: string;
11
13
  }
@@ -4,6 +4,8 @@ exports.installAllCommand = installAllCommand;
4
4
  const config_1 = require("../config");
5
5
  const discover_1 = require("./discover");
6
6
  const installCursorHook_1 = require("./installCursorHook");
7
+ const installClaudeHook_1 = require("./installClaudeHook");
8
+ const discoverSchedule_1 = require("./discoverSchedule");
7
9
  const mcpGateway_1 = require("./mcpGateway");
8
10
  const autoProtect_1 = require("./autoProtect");
9
11
  const restartNotice_1 = require("./restartNotice");
@@ -45,6 +47,23 @@ async function installAllCommand(args, config) {
45
47
  catch (error) {
46
48
  console.log(`Cursor hooks skipped: ${error instanceof Error ? error.message : String(error)}`);
47
49
  }
50
+ // ONE Claude-format hook file (~/.claude/settings.json) covers Claude Code,
51
+ // VS Code Copilot agent mode, and GitHub Copilot CLI.
52
+ console.log('\n\x1b[1mInstalling Claude Code / VS Code / Copilot CLI runtime hooks…\x1b[0m');
53
+ const claudeHookArgs = {
54
+ shieldId: creds.shieldId,
55
+ shieldKey: creds.shieldKey,
56
+ apiUrl: creds.apiUrl,
57
+ // Tool actions only by default — same scope as the Cursor hooks. Prompt
58
+ // scanning stays opt-in (`install-claude-hook --events tools,prompt`).
59
+ events: 'tools',
60
+ };
61
+ try {
62
+ await (0, installClaudeHook_1.installClaudeHookCommand)(claudeHookArgs, config);
63
+ }
64
+ catch (error) {
65
+ console.log(`Claude-format hooks skipped: ${error instanceof Error ? error.message : String(error)}`);
66
+ }
48
67
  }
49
68
  if (args.autoProtect === 'true') {
50
69
  console.log('\n\x1b[1mScheduling self-healing MCP protection…\x1b[0m');
@@ -70,6 +89,26 @@ async function installAllCommand(args, config) {
70
89
  };
71
90
  await (0, discover_1.discoverCommand)(discoverArgs, config);
72
91
  }
92
+ // Keep the fleet view fresh without any manual step: register a recurring
93
+ // discovery run (posture + inventory upload) once a day. Opt out with
94
+ // `--schedule false`.
95
+ if (args.schedule !== 'false') {
96
+ console.log('\n\x1b[1mScheduling daily desktop discovery…\x1b[0m');
97
+ try {
98
+ const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
99
+ (0, discoverSchedule_1.installDailyDiscoverSchedule)({
100
+ userEmail: args.developerName,
101
+ hour: Number.isFinite(hour) ? hour : 9,
102
+ surface: 'all',
103
+ trigger: 'daily',
104
+ });
105
+ console.log(`Desktop discovery scheduled daily at ${String(Number.isFinite(hour) ? hour : 9).padStart(2, '0')}:00 (posture + inventory upload).`);
106
+ }
107
+ catch (error) {
108
+ console.log(`Daily discovery schedule skipped: ${error instanceof Error ? error.message : String(error)}`);
109
+ console.log('You can retry with: fullcourtdefense discover --schedule daily');
110
+ }
111
+ }
73
112
  console.log('\n\x1b[32mDone.\x1b[0m Run \x1b[1mfullcourtdefense protect-all --dry-run true\x1b[0m to verify gateways.');
74
113
  await (0, restartNotice_1.promptRestartNotice)();
75
114
  }
@@ -0,0 +1,27 @@
1
+ import { BotGuardConfig } from '../config';
2
+ /**
3
+ * `fullcourtdefense install-claude-hook` — Claude-format hook installer.
4
+ *
5
+ * Writes PreToolUse + UserPromptSubmit hooks into ~/.claude/settings.json.
6
+ * That ONE file is read by THREE clients:
7
+ * - Claude Code (CLI + desktop)
8
+ * - VS Code Copilot agent mode (user-scope Claude-format hooks)
9
+ * - GitHub Copilot CLI
10
+ *
11
+ * The hook command is the same `fullcourtdefense hook` bridge used for Cursor —
12
+ * it detects the Claude payload format from stdin (`hook_event_name`), so no
13
+ * per-client configuration is needed. VS Code ignores `matcher` values (hooks
14
+ * run on every tool), so the bridge self-filters by tool name and returns an
15
+ * empty verdict for tools we don't police.
16
+ */
17
+ export interface InstallClaudeHookArgs {
18
+ project?: string;
19
+ shieldId?: string;
20
+ shieldKey?: string;
21
+ apiUrl?: string;
22
+ shadow?: string;
23
+ failClosed?: string;
24
+ events?: string;
25
+ }
26
+ export declare function installClaudeHookCommand(args: InstallClaudeHookArgs, config: BotGuardConfig): Promise<void>;
27
+ export declare function uninstallClaudeHookCommand(args: InstallClaudeHookArgs): Promise<void>;
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installClaudeHookCommand = installClaudeHookCommand;
37
+ exports.uninstallClaudeHookCommand = uninstallClaudeHookCommand;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const config_1 = require("../config");
42
+ const restartNotice_1 = require("./restartNotice");
43
+ const COLOR = {
44
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
45
+ red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
46
+ };
47
+ // Stable marker embedded in every command we write, so uninstall/re-install can
48
+ // find our entries regardless of the install path (npm global vs local dev).
49
+ const MANAGED_MARKER = '--fcd-managed true';
50
+ const MANAGED_TAG = 'fullcourtdefense';
51
+ function settingsPath(projectScope) {
52
+ return projectScope
53
+ ? path.join(process.cwd(), '.claude', 'settings.json')
54
+ : path.join(os.homedir(), '.claude', 'settings.json');
55
+ }
56
+ function readSettings(file) {
57
+ if (!fs.existsSync(file))
58
+ return {};
59
+ try {
60
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
61
+ return parsed && typeof parsed === 'object' ? parsed : {};
62
+ }
63
+ catch {
64
+ return {};
65
+ }
66
+ }
67
+ function isManagedCommand(cmd) {
68
+ return typeof cmd?.command === 'string'
69
+ && (cmd.command.includes(MANAGED_MARKER) || cmd.command.includes(MANAGED_TAG));
70
+ }
71
+ /** Remove FullCourtDefense-managed commands from every event's matcher entries. */
72
+ function stripManaged(hooks) {
73
+ let removed = 0;
74
+ for (const eventName of Object.keys(hooks)) {
75
+ const entries = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
76
+ const kept = [];
77
+ for (const entry of entries) {
78
+ const cmds = Array.isArray(entry?.hooks) ? entry.hooks : [];
79
+ const keptCmds = cmds.filter((c) => !isManagedCommand(c));
80
+ removed += cmds.length - keptCmds.length;
81
+ if (keptCmds.length > 0 || cmds.length === 0) {
82
+ kept.push({ ...entry, hooks: keptCmds });
83
+ }
84
+ }
85
+ if (kept.length)
86
+ hooks[eventName] = kept;
87
+ else
88
+ delete hooks[eventName];
89
+ }
90
+ return removed;
91
+ }
92
+ /** Build the absolute, shell-agnostic command that invokes this CLI's `hook`. */
93
+ function buildHookCommand(opts) {
94
+ const nodeExe = process.execPath;
95
+ const scriptPath = process.argv[1] || path.join(__dirname, '..', 'index.js');
96
+ const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
97
+ // No --event flag: the bridge detects the Claude payload (hook_event_name) itself.
98
+ let cmd = `${q(nodeExe)} ${q(scriptPath)} hook --approval-mode wait`;
99
+ if (opts.shadow)
100
+ cmd += ' --shadow true';
101
+ if (opts.failClosed)
102
+ cmd += ' --fail-closed true';
103
+ cmd += ` ${MANAGED_MARKER}`;
104
+ return cmd;
105
+ }
106
+ function ensureHomeShield(input) {
107
+ if (!input.shieldId && !input.shieldKey && !input.apiUrl)
108
+ return undefined;
109
+ return (0, config_1.saveSetupConfig)({
110
+ shieldId: input.shieldId,
111
+ shieldKey: input.shieldKey,
112
+ apiUrl: input.apiUrl,
113
+ });
114
+ }
115
+ async function installClaudeHookCommand(args, config) {
116
+ const projectScope = args.project === 'true';
117
+ const shadow = args.shadow === 'true';
118
+ // Default fail-OPEN: when the backend is unreachable the developer keeps working;
119
+ // the local deterministic guard (cached Local Safety policies) still blocks
120
+ // offline, and every degraded allow is spooled + replayed. `--fail-closed true`
121
+ // opts into blocking all agent actions while offline.
122
+ const failClosed = args.failClosed === 'true';
123
+ const file = settingsPath(projectScope);
124
+ const requested = (args.events || 'tools,prompt')
125
+ .split(/[\s,]+/).map((e) => e.trim().toLowerCase()).filter(Boolean);
126
+ const wantTools = requested.includes('tools') || requested.includes('shell') || requested.includes('mcp');
127
+ const wantPrompt = requested.includes('prompt');
128
+ if (!wantTools && !wantPrompt) {
129
+ console.error(`${COLOR.red}No valid events. Choose from: tools, prompt${COLOR.reset}`);
130
+ process.exit(1);
131
+ }
132
+ const creds = (0, config_1.resolveCliCredentials)(config, {
133
+ shieldId: args.shieldId,
134
+ shieldKey: args.shieldKey,
135
+ apiUrl: args.apiUrl,
136
+ });
137
+ const credFile = ensureHomeShield(creds);
138
+ const settings = readSettings(file);
139
+ const hooks = (settings.hooks && typeof settings.hooks === 'object'
140
+ ? settings.hooks : {});
141
+ settings.hooks = hooks;
142
+ stripManaged(hooks);
143
+ const command = buildHookCommand({ shadow, failClosed });
144
+ if (wantTools) {
145
+ const list = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
146
+ // Long timeout: allow/block verdicts return in <1s; the timeout only matters
147
+ // while an action is paused waiting for a human approval in the console.
148
+ // (Claude hook timeouts are in seconds.)
149
+ list.push({ matcher: '*', hooks: [{ type: 'command', command, timeout: 300 }] });
150
+ hooks.PreToolUse = list;
151
+ }
152
+ if (wantPrompt) {
153
+ const list = Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
154
+ list.push({ hooks: [{ type: 'command', command, timeout: 30 }] });
155
+ hooks.UserPromptSubmit = list;
156
+ }
157
+ fs.mkdirSync(path.dirname(file), { recursive: true });
158
+ fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf8');
159
+ console.log('');
160
+ console.log(`${COLOR.green}${COLOR.bold}FullCourtDefense Claude-format hook installed.${COLOR.reset}`);
161
+ console.log(`${COLOR.gray}Scope:${COLOR.reset} ${projectScope ? 'project (.claude/settings.json)' : 'machine-wide (~/.claude/settings.json)'}`);
162
+ console.log(`${COLOR.gray}File:${COLOR.reset} ${file}`);
163
+ console.log(`${COLOR.gray}Covers:${COLOR.reset} Claude Code, VS Code Copilot agent mode, GitHub Copilot CLI (all read this file)`);
164
+ const parts = [];
165
+ if (wantTools)
166
+ parts.push('tool calls (shell / MCP / file writes / reads) checked against org Action Policies + Local Safety rules');
167
+ if (wantPrompt)
168
+ parts.push('prompts scanned by your Shield');
169
+ console.log(`${COLOR.gray}Enforcement:${COLOR.reset} ${parts.join('; ')}`);
170
+ if (shadow)
171
+ console.log(`${COLOR.yellow}Mode: SHADOW (monitor only — nothing is blocked).${COLOR.reset}`);
172
+ if (failClosed)
173
+ console.log(`${COLOR.yellow}Fail-closed: agent actions are BLOCKED while the policy service is unreachable.${COLOR.reset}`);
174
+ if (credFile)
175
+ console.log(`${COLOR.gray}Creds:${COLOR.reset} ${credFile}`);
176
+ if (!creds.shieldId) {
177
+ console.log('');
178
+ console.log(`${COLOR.yellow}No Shield ID set.${COLOR.reset} Run ${COLOR.bold}fullcourtdefense login --token <fleet-enrollment-token>${COLOR.reset} first, then re-run install.`);
179
+ }
180
+ console.log('');
181
+ (0, restartNotice_1.printRestartNotice)('Claude Code, VS Code, Copilot CLI');
182
+ console.log(`${COLOR.gray}Remove with:${COLOR.reset} fullcourtdefense uninstall-claude-hook${projectScope ? ' --project true' : ''}`);
183
+ }
184
+ async function uninstallClaudeHookCommand(args) {
185
+ const projectScope = args.project === 'true';
186
+ const file = settingsPath(projectScope);
187
+ if (!fs.existsSync(file)) {
188
+ console.log(`${COLOR.yellow}No settings.json found at ${file}. Nothing to remove.${COLOR.reset}`);
189
+ return;
190
+ }
191
+ const settings = readSettings(file);
192
+ const hooks = (settings.hooks && typeof settings.hooks === 'object'
193
+ ? settings.hooks : {});
194
+ const removed = stripManaged(hooks);
195
+ if (Object.keys(hooks).length === 0)
196
+ delete settings.hooks;
197
+ else
198
+ settings.hooks = hooks;
199
+ fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf8');
200
+ console.log(`${COLOR.green}Removed ${removed} FullCourtDefense hook entr${removed === 1 ? 'y' : 'ies'} from ${file}.${COLOR.reset}`);
201
+ }