fullcourtdefense-cli 1.18.11 → 1.19.0

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.
@@ -51,6 +51,7 @@ const integrity_1 = require("../integrity");
51
51
  const machineIdentity_1 = require("../machineIdentity");
52
52
  const discoveryMarker_1 = require("../discoveryMarker");
53
53
  const selfUpdate_1 = require("../selfUpdate");
54
+ const desktopChatGuard_1 = require("./desktopChatGuard");
54
55
  const COLOR = {
55
56
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
56
57
  red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
@@ -720,6 +721,7 @@ async function runDaemon(args, config) {
720
721
  integrityOk: integrity.ok,
721
722
  integrityReasons: integrity.reasons,
722
723
  integrityCheckedAt: integrity.checkedAt,
724
+ desktopChatGuard: (0, desktopChatGuard_1.desktopChatGuardSupported)() && (0, desktopChatGuard_1.desktopChatGuardHealthy)(),
723
725
  });
724
726
  if (result && result.accepted > 0)
725
727
  log(`Heartbeat: flushed ${result.accepted} spooled event(s).`);
@@ -746,6 +748,22 @@ async function runDaemon(args, config) {
746
748
  // for a condition the very next line repairs.
747
749
  await reprotect(['startup pass']);
748
750
  await heartbeat();
751
+ // Claude Desktop chat guard (Windows, advisory): Claude Desktop's regular
752
+ // chat has no hook and never hits an MCP server, so it is the one machine
753
+ // surface neither hooks nor the gateway can see. Supervise the advisory guard
754
+ // in-process here (auto-restart is built into the guard itself).
755
+ let desktopChatGuard;
756
+ if (creds.shieldId && (0, desktopChatGuard_1.desktopChatGuardSupported)()) {
757
+ desktopChatGuard = (0, desktopChatGuard_1.startDesktopChatGuard)({
758
+ apiUrl: creds.apiUrl,
759
+ shieldId: creds.shieldId,
760
+ shieldKey: creds.shieldKey,
761
+ quiet,
762
+ log,
763
+ });
764
+ if (desktopChatGuard)
765
+ log('Claude Desktop chat guard: supervising (advisory — warns on secrets typed into Claude Desktop).');
766
+ }
749
767
  // Fresh machines have never uploaded an inventory (MSI/onboard defers the
750
768
  // initial discovery to keep setup fast), so the dashboard shows "Never" for
751
769
  // discovery + posture until the daily scheduled job fires — up to 24h later.
@@ -833,6 +851,8 @@ async function runDaemon(args, config) {
833
851
  clearTimeout(initialDiscoverTimer);
834
852
  if (debounceTimer)
835
853
  clearTimeout(debounceTimer);
854
+ if (desktopChatGuard)
855
+ desktopChatGuard.stop();
836
856
  for (const watcher of watchers.values())
837
857
  watcher.close();
838
858
  for (const watcher of rootWatchers.values())
@@ -0,0 +1,43 @@
1
+ import { BotGuardConfig } from '../config';
2
+ export interface DesktopChatGuardArgs {
3
+ apiUrl?: string;
4
+ shieldId?: string;
5
+ shieldKey?: string;
6
+ /** Suppress OS toasts (still spools findings). */
7
+ quiet?: string;
8
+ }
9
+ /** Only meaningful where Claude Desktop runs and UI Automation is available. */
10
+ export declare function desktopChatGuardSupported(): boolean;
11
+ /** True when the guard reported itself healthy within the last `withinMs`. */
12
+ export declare function desktopChatGuardHealthy(withinMs?: number): boolean;
13
+ /**
14
+ * The PowerShell watcher. Reads ONLY the focused element of the foreground
15
+ * Claude process (never the transcript), so it is low-noise and resilient to
16
+ * Claude UI changes. Emits `FCD:<base64 utf8>` lines on stdout when the text
17
+ * changes. Everything is wrapped in try/catch — it must never crash the host.
18
+ */
19
+ export declare function desktopChatWatcherScript(): string;
20
+ /** Decode one `FCD:<base64>` watcher line into UTF-8 text (undefined if not ours). */
21
+ export declare function decodeWatcherLine(line: string): string | undefined;
22
+ export interface DesktopChatGuardHandle {
23
+ stop(): void;
24
+ }
25
+ interface GuardRuntime {
26
+ apiUrl: string;
27
+ shieldId?: string;
28
+ shieldKey?: string;
29
+ quiet: boolean;
30
+ log: (msg: string) => void;
31
+ }
32
+ /**
33
+ * Start the advisory guard. Returns a handle whose stop() tears down the
34
+ * watcher and timers. Safe no-op (returns undefined) on unsupported platforms.
35
+ */
36
+ export declare function startDesktopChatGuard(runtime: GuardRuntime): DesktopChatGuardHandle | undefined;
37
+ /**
38
+ * Foreground `desktop-chat-guard` command — runs the advisory guard in this
39
+ * process until interrupted. Mostly for manual testing; in production the
40
+ * daemon supervises the guard in-process (startDesktopChatGuard).
41
+ */
42
+ export declare function desktopChatGuardCommand(args: DesktopChatGuardArgs, config: BotGuardConfig): Promise<void>;
43
+ export {};
@@ -0,0 +1,376 @@
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.desktopChatGuardSupported = desktopChatGuardSupported;
37
+ exports.desktopChatGuardHealthy = desktopChatGuardHealthy;
38
+ exports.desktopChatWatcherScript = desktopChatWatcherScript;
39
+ exports.decodeWatcherLine = decodeWatcherLine;
40
+ exports.startDesktopChatGuard = startDesktopChatGuard;
41
+ exports.desktopChatGuardCommand = desktopChatGuardCommand;
42
+ const child_process_1 = require("child_process");
43
+ const fs = __importStar(require("fs"));
44
+ const os = __importStar(require("os"));
45
+ const path = __importStar(require("path"));
46
+ const readline = __importStar(require("readline"));
47
+ const config_1 = require("../config");
48
+ const localSafetySnapshot_1 = require("../localSafetySnapshot");
49
+ const deterministicGuard_1 = require("./deterministicGuard");
50
+ const telemetry_1 = require("../telemetry");
51
+ const notify_1 = require("../notify");
52
+ const machineIdentity_1 = require("../machineIdentity");
53
+ const discoverPaths_1 = require("./discoverPaths");
54
+ /**
55
+ * Claude Desktop chat guard (Windows, phase 1 — advisory).
56
+ *
57
+ * Claude Desktop's regular chat sends prompts straight to Anthropic's API — it
58
+ * exposes no hook and never touches an MCP server, so neither the Claude-format
59
+ * hooks nor the MCP gateway can see what a developer types there. This guard
60
+ * closes that blind spot with the SAME on-device deterministic engine used
61
+ * everywhere else (scanDeterministicPrompt): a lightweight PowerShell UI
62
+ * Automation watcher reads the text of the focused Claude Desktop input element
63
+ * and streams it (base64) to this process, which scans it locally and — on a
64
+ * finding — shows a native toast and reports a monitor event to the fleet.
65
+ *
66
+ * Phase 1 is advisory ONLY: no keyboard hook, no input mutation. It cannot
67
+ * break typing and presents no keylogger surface (it reads only the focused
68
+ * element of the foreground Claude process). Text is scanned in-process and is
69
+ * never uploaded — only finding metadata (item id + masked value) is spooled.
70
+ * Hard blocking is a later, org-policy-gated phase.
71
+ */
72
+ const WATCHER_PS1_PATH = path.join(os.homedir(), '.fullcourtdefense-claude-desktop-watcher.ps1');
73
+ const STATUS_PATH = path.join(os.homedir(), '.fullcourtdefense', 'claude-desktop-guard.json');
74
+ const STDOUT_PREFIX = 'FCD:';
75
+ /** Poll cadence for the foreground/focused-element read (ms). */
76
+ const POLL_MS = 800;
77
+ /** Don't re-toast the same finding value more often than this. */
78
+ const TOAST_DEBOUNCE_MS = 60_000;
79
+ /** Refresh the cached Local Safety snapshot on this cadence. */
80
+ const SNAPSHOT_REFRESH_MS = 5 * 60_000;
81
+ /** Watcher restart backoff bounds. */
82
+ const RESTART_MIN_MS = 2_000;
83
+ const RESTART_MAX_MS = 30_000;
84
+ /** Only meaningful where Claude Desktop runs and UI Automation is available. */
85
+ function desktopChatGuardSupported() {
86
+ return process.platform === 'win32' && (0, discoverPaths_1.claudeDesktopLikelyInstalled)();
87
+ }
88
+ function mask(value) {
89
+ const v = String(value || '');
90
+ if (v.length <= 8)
91
+ return '*'.repeat(v.length);
92
+ return `${v.slice(0, 4)}${'*'.repeat(Math.max(4, v.length - 8))}${v.slice(-4)}`;
93
+ }
94
+ function writeStatus(status) {
95
+ try {
96
+ fs.mkdirSync(path.dirname(STATUS_PATH), { recursive: true });
97
+ fs.writeFileSync(STATUS_PATH, JSON.stringify(status, null, 2), { encoding: 'utf8', mode: 0o600 });
98
+ }
99
+ catch { /* best-effort */ }
100
+ }
101
+ /** True when the guard reported itself healthy within the last `withinMs`. */
102
+ function desktopChatGuardHealthy(withinMs = 15 * 60_000) {
103
+ try {
104
+ const parsed = JSON.parse(fs.readFileSync(STATUS_PATH, 'utf8'));
105
+ if (!parsed?.running || !parsed.heartbeatAt)
106
+ return false;
107
+ return Date.now() - new Date(parsed.heartbeatAt).getTime() < withinMs;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ /**
114
+ * The PowerShell watcher. Reads ONLY the focused element of the foreground
115
+ * Claude process (never the transcript), so it is low-noise and resilient to
116
+ * Claude UI changes. Emits `FCD:<base64 utf8>` lines on stdout when the text
117
+ * changes. Everything is wrapped in try/catch — it must never crash the host.
118
+ */
119
+ function desktopChatWatcherScript() {
120
+ return [
121
+ "$ErrorActionPreference = 'SilentlyContinue'",
122
+ 'try {',
123
+ " Add-Type -AssemblyName UIAutomationClient,UIAutomationTypes,WindowsBase | Out-Null",
124
+ '} catch { }',
125
+ 'try {',
126
+ ' Add-Type -TypeDefinition @"',
127
+ 'using System;',
128
+ 'using System.Runtime.InteropServices;',
129
+ 'public static class FcdWin {',
130
+ ' [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
131
+ ' [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);',
132
+ '}',
133
+ '"@',
134
+ '} catch { }',
135
+ '',
136
+ 'function Get-FcdFocusedText {',
137
+ ' param([int]$OwnerPid)',
138
+ ' try {',
139
+ ' $focused = [System.Windows.Automation.AutomationElement]::FocusedElement',
140
+ ' if ($null -eq $focused) { return $null }',
141
+ ' if ($focused.Current.ProcessId -ne $OwnerPid) { return $null }',
142
+ ' $text = $null',
143
+ ' $vp = $null',
144
+ ' if ($focused.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) {',
145
+ ' $text = $vp.Current.Value',
146
+ ' }',
147
+ ' if ([string]::IsNullOrEmpty($text)) {',
148
+ ' $tp = $null',
149
+ ' if ($focused.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref]$tp)) {',
150
+ ' $text = $tp.DocumentRange.GetText(20000)',
151
+ ' }',
152
+ ' }',
153
+ ' if ([string]::IsNullOrEmpty($text)) { $text = $focused.Current.Name }',
154
+ ' return $text',
155
+ ' } catch { return $null }',
156
+ '}',
157
+ '',
158
+ '$last = ""',
159
+ 'while ($true) {',
160
+ " Start-Sleep -Milliseconds " + POLL_MS,
161
+ ' try {',
162
+ ' $h = [FcdWin]::GetForegroundWindow()',
163
+ ' if ($h -eq [IntPtr]::Zero) { continue }',
164
+ ' $procId = 0',
165
+ ' [void][FcdWin]::GetWindowThreadProcessId($h, [ref]$procId)',
166
+ ' if ($procId -eq 0) { continue }',
167
+ ' $proc = Get-Process -Id $procId -ErrorAction SilentlyContinue',
168
+ " if ($null -eq $proc -or $proc.ProcessName -ne 'Claude') { continue }",
169
+ ' $text = Get-FcdFocusedText -OwnerPid $procId',
170
+ ' if ([string]::IsNullOrEmpty($text)) { continue }',
171
+ ' if ($text.Length -gt 8000) { continue }',
172
+ ' if ($text -eq $last) { continue }',
173
+ ' $last = $text',
174
+ ' $bytes = [System.Text.Encoding]::UTF8.GetBytes($text)',
175
+ ' $b64 = [Convert]::ToBase64String($bytes)',
176
+ " [Console]::Out.WriteLine('" + STDOUT_PREFIX + "' + $b64)",
177
+ ' [Console]::Out.Flush()',
178
+ ' } catch { }',
179
+ '}',
180
+ '',
181
+ ].join('\n');
182
+ }
183
+ /** Decode one `FCD:<base64>` watcher line into UTF-8 text (undefined if not ours). */
184
+ function decodeWatcherLine(line) {
185
+ if (!line.startsWith(STDOUT_PREFIX))
186
+ return undefined;
187
+ try {
188
+ return Buffer.from(line.slice(STDOUT_PREFIX.length), 'base64').toString('utf8');
189
+ }
190
+ catch {
191
+ return undefined;
192
+ }
193
+ }
194
+ function ensureWatcherScript() {
195
+ fs.writeFileSync(WATCHER_PS1_PATH, desktopChatWatcherScript(), { encoding: 'utf8' });
196
+ return WATCHER_PS1_PATH;
197
+ }
198
+ /**
199
+ * Start the advisory guard. Returns a handle whose stop() tears down the
200
+ * watcher and timers. Safe no-op (returns undefined) on unsupported platforms.
201
+ */
202
+ function startDesktopChatGuard(runtime) {
203
+ if (!desktopChatGuardSupported())
204
+ return undefined;
205
+ const startedAt = new Date().toISOString();
206
+ let stopped = false;
207
+ let child;
208
+ let restartTimer;
209
+ let restartDelay = RESTART_MIN_MS;
210
+ let findings = 0;
211
+ const lastToastAt = new Map();
212
+ let scanOptions = (0, localSafetySnapshot_1.snapshotToScanOptions)(undefined);
213
+ const identity = (0, machineIdentity_1.getMachineIdentity)();
214
+ const refreshSnapshot = async () => {
215
+ if (!runtime.shieldId)
216
+ return;
217
+ try {
218
+ const snapshot = await (0, localSafetySnapshot_1.loadLocalSafetySnapshot)({
219
+ apiUrl: runtime.apiUrl,
220
+ shieldId: runtime.shieldId,
221
+ shieldKey: runtime.shieldKey,
222
+ developerName: identity.developerName,
223
+ machineName: identity.hostname,
224
+ });
225
+ scanOptions = (0, localSafetySnapshot_1.snapshotToScanOptions)(snapshot);
226
+ }
227
+ catch { /* offline — keep last options */ }
228
+ };
229
+ // heartbeatAt lets the daemon/console tell "guard alive" from "stale".
230
+ const heartbeat = () => writeStatus({
231
+ running: !stopped,
232
+ findings,
233
+ startedAt,
234
+ heartbeatAt: new Date().toISOString(),
235
+ });
236
+ const handleText = (text) => {
237
+ if (!text.trim())
238
+ return;
239
+ const finding = (0, deterministicGuard_1.scanDeterministicPrompt)(text, scanOptions);
240
+ if (!finding)
241
+ return;
242
+ findings += 1;
243
+ const masked = mask(finding.evidence || '');
244
+ heartbeat();
245
+ // Monitor semantics: advisory phase never blocks, so the decision is
246
+ // 'allow' with a "would block" evidence note (same shape shell-guard uses
247
+ // in monitor mode), which the Activity log renders as a monitor finding.
248
+ (0, telemetry_1.spoolEvent)({
249
+ decision: 'allow',
250
+ toolName: 'claude_desktop_chat',
251
+ operation: 'prompt',
252
+ reason: `would block: ${finding.reason}`,
253
+ ruleId: finding.ruleId,
254
+ category: finding.category,
255
+ categoryId: finding.categoryId,
256
+ itemId: finding.itemId,
257
+ source: finding.source,
258
+ evidence: masked,
259
+ explanation: finding.explanation,
260
+ policyHash: finding.policyHash,
261
+ });
262
+ (0, telemetry_1.triggerFlush)();
263
+ const key = `${finding.itemId}:${masked}`;
264
+ const now = Date.now();
265
+ const last = lastToastAt.get(key) || 0;
266
+ if (!runtime.quiet && now - last > TOAST_DEBOUNCE_MS) {
267
+ lastToastAt.set(key, now);
268
+ (0, notify_1.notifyOs)({
269
+ title: 'FullCourtDefense: secret in Claude Desktop',
270
+ message: `${finding.reason}. Remove it before sending — this text has not been protected.`,
271
+ });
272
+ }
273
+ runtime.log(`Claude Desktop chat finding: ${finding.itemId} (${finding.reason}).`);
274
+ };
275
+ const spawnWatcher = () => {
276
+ if (stopped)
277
+ return;
278
+ let scriptPath;
279
+ try {
280
+ scriptPath = ensureWatcherScript();
281
+ }
282
+ catch (error) {
283
+ runtime.log(`Claude Desktop guard: cannot write watcher script: ${error.message}`);
284
+ scheduleRestart();
285
+ return;
286
+ }
287
+ child = (0, child_process_1.spawn)('powershell', [
288
+ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', scriptPath,
289
+ ], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
290
+ if (!child.stdout) {
291
+ scheduleRestart();
292
+ return;
293
+ }
294
+ const rl = readline.createInterface({ input: child.stdout });
295
+ rl.on('line', (line) => {
296
+ const text = decodeWatcherLine(line);
297
+ if (text === undefined)
298
+ return;
299
+ restartDelay = RESTART_MIN_MS; // healthy output resets backoff
300
+ handleText(text);
301
+ });
302
+ child.on('exit', () => { rl.close(); if (!stopped)
303
+ scheduleRestart(); });
304
+ child.on('error', () => { if (!stopped)
305
+ scheduleRestart(); });
306
+ runtime.log('Claude Desktop chat guard: watcher started (advisory).');
307
+ heartbeat();
308
+ };
309
+ const scheduleRestart = () => {
310
+ if (stopped || restartTimer)
311
+ return;
312
+ restartTimer = setTimeout(() => {
313
+ restartTimer = undefined;
314
+ spawnWatcher();
315
+ }, restartDelay);
316
+ restartDelay = Math.min(RESTART_MAX_MS, restartDelay * 2);
317
+ };
318
+ void refreshSnapshot();
319
+ const snapshotTimer = setInterval(() => { void refreshSnapshot(); }, SNAPSHOT_REFRESH_MS);
320
+ const heartbeatTimer = setInterval(heartbeat, 60_000);
321
+ spawnWatcher();
322
+ return {
323
+ stop() {
324
+ if (stopped)
325
+ return;
326
+ stopped = true;
327
+ clearInterval(snapshotTimer);
328
+ clearInterval(heartbeatTimer);
329
+ if (restartTimer) {
330
+ clearTimeout(restartTimer);
331
+ restartTimer = undefined;
332
+ }
333
+ try {
334
+ child?.kill();
335
+ }
336
+ catch { /* ignore */ }
337
+ writeStatus({ running: false, findings, startedAt });
338
+ },
339
+ };
340
+ }
341
+ /**
342
+ * Foreground `desktop-chat-guard` command — runs the advisory guard in this
343
+ * process until interrupted. Mostly for manual testing; in production the
344
+ * daemon supervises the guard in-process (startDesktopChatGuard).
345
+ */
346
+ async function desktopChatGuardCommand(args, config) {
347
+ if (process.platform !== 'win32') {
348
+ console.error('The Claude Desktop chat guard is Windows-only.');
349
+ process.exit(1);
350
+ }
351
+ if (!(0, discoverPaths_1.claudeDesktopLikelyInstalled)()) {
352
+ console.error('Claude Desktop was not detected on this machine — nothing to guard.');
353
+ process.exit(1);
354
+ }
355
+ const creds = (0, config_1.resolveCliCredentials)(config, {
356
+ shieldId: args.shieldId,
357
+ shieldKey: args.shieldKey,
358
+ apiUrl: args.apiUrl,
359
+ });
360
+ const handle = startDesktopChatGuard({
361
+ apiUrl: creds.apiUrl,
362
+ shieldId: creds.shieldId,
363
+ shieldKey: creds.shieldKey,
364
+ quiet: args.quiet === 'true',
365
+ log: (msg) => console.log(msg),
366
+ });
367
+ if (!handle) {
368
+ console.error('Claude Desktop chat guard is not supported on this machine.');
369
+ process.exit(1);
370
+ }
371
+ console.log('FullCourtDefense Claude Desktop chat guard running (advisory). Stop with Ctrl+C.');
372
+ const shutdown = () => { handle.stop(); process.exit(0); };
373
+ process.on('SIGINT', shutdown);
374
+ process.on('SIGTERM', shutdown);
375
+ await new Promise(() => { });
376
+ }
@@ -33,4 +33,19 @@ export interface HookArgs {
33
33
  approvalTimeoutMs?: string;
34
34
  approvalPollMs?: string;
35
35
  }
36
+ type HookEvent = 'prompt' | 'shell' | 'mcp' | 'file' | 'read' | 'unknown';
37
+ /**
38
+ * Claude-format hooks (Claude Code, VS Code Copilot agent mode, GitHub Copilot
39
+ * CLI — all share the same schema) send `hook_event_name` + `tool_name` +
40
+ * `tool_input` on stdin. Normalize that payload into the field shapes the rest
41
+ * of this file already understands (command/file_path/tool_name/tool_input),
42
+ * and map the tool onto our event taxonomy.
43
+ *
44
+ * Returns null when the payload is not Claude-format.
45
+ */
46
+ export declare function normalizeClaudePayload(payload: Record<string, unknown>): {
47
+ event: HookEvent | 'ignore';
48
+ payload: Record<string, unknown>;
49
+ } | null;
36
50
  export declare function hookCommand(args: HookArgs, config: BotGuardConfig): Promise<void>;
51
+ export {};
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.normalizeClaudePayload = normalizeClaudePayload;
36
37
  exports.hookCommand = hookCommand;
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
@@ -122,6 +123,12 @@ function normalizeClaudePayload(payload) {
122
123
  const hookEventName = str(payload.hook_event_name);
123
124
  if (!hookEventName)
124
125
  return null;
126
+ // Cursor now also includes `hook_event_name`, using lower-camel names such
127
+ // as beforeSubmitPrompt/beforeShellExecution. Treating presence alone as a
128
+ // Claude discriminator turned every Cursor event into "unknown" and allowed
129
+ // it. Claude-family lifecycle names are PascalCase.
130
+ if (/^[a-z]/.test(hookEventName))
131
+ return null;
125
132
  if (hookEventName === 'UserPromptSubmit') {
126
133
  return { event: 'prompt', payload };
127
134
  }
@@ -253,6 +253,7 @@ async function installClaudeHookCommand(args, config) {
253
253
  console.log(`${COLOR.gray}Scope:${COLOR.reset} ${projectScope ? 'project (.claude/settings.json)' : 'machine-wide (~/.claude/settings.json)'}`);
254
254
  console.log(`${COLOR.gray}File:${COLOR.reset} ${file}`);
255
255
  console.log(`${COLOR.gray}Covers:${COLOR.reset} Claude Code, VS Code Copilot agent mode, GitHub Copilot CLI (all read this file)`);
256
+ console.log(`${COLOR.gray}Note:${COLOR.reset} Claude Desktop's regular chat has no hook — it is protected separately by the Windows chat guard the daemon runs (advisory).`);
256
257
  const parts = [];
257
258
  if (wantTools)
258
259
  parts.push('tool calls (shell / MCP / file writes / reads) checked against org Action Policies + Local Safety rules');
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ const installAll_1 = require("./commands/installAll");
52
52
  const onboard_1 = require("./commands/onboard");
53
53
  const autoProtect_1 = require("./commands/autoProtect");
54
54
  const daemon_1 = require("./commands/daemon");
55
+ const desktopChatGuard_1 = require("./commands/desktopChatGuard");
55
56
  const windowsAudit_1 = require("./commands/windowsAudit");
56
57
  const shellGuard_1 = require("./commands/shellGuard");
57
58
  const cmdGuard_1 = require("./commands/cmdGuard");
@@ -845,6 +846,16 @@ async function main() {
845
846
  await (0, daemon_1.daemonCommand)(args, config);
846
847
  break;
847
848
  }
849
+ case 'desktop-chat-guard': {
850
+ const args = {
851
+ apiUrl: flags['api-url'],
852
+ shieldId: flags['shield-id'],
853
+ shieldKey: flags['shield-key'],
854
+ quiet: flags.quiet,
855
+ };
856
+ await (0, desktopChatGuard_1.desktopChatGuardCommand)(args, config);
857
+ break;
858
+ }
848
859
  case 'install-cursor-mcp-gateway': {
849
860
  const args = {
850
861
  ...buildGatewayArgs(),
@@ -34,6 +34,8 @@ export interface FlushInput {
34
34
  integrityCheckedAt?: string;
35
35
  /** Set only by the resident process; distinguishes daemon liveness from hook flushes. */
36
36
  daemon?: boolean;
37
+ /** Windows Claude Desktop chat guard liveness (advisory prompt protection). */
38
+ desktopChatGuard?: boolean;
37
39
  timeoutMs?: number;
38
40
  }
39
41
  /** Drain the spool to the backend in one batch (+ optional heartbeat). Returns accepted count. */
package/dist/telemetry.js CHANGED
@@ -164,6 +164,7 @@ async function flushSpool(input) {
164
164
  integrityCheckedAt: input.integrityCheckedAt,
165
165
  daemon: input.daemon === true,
166
166
  coverage: 'hooks',
167
+ desktopChatGuard: input.desktopChatGuard === true,
167
168
  hostname: identity.hostname,
168
169
  // Windows-only: current PowerShell audit coverage (ScriptBlock
169
170
  // Logging + Transcription). Keeps the fleet dashboard's coverage
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.18.11"
2
+ "version": "1.19.0"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.18.11",
3
+ "version": "1.19.0",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "scripts": {
16
16
  "build": "tsc && node scripts/copy-attack-corpus.js",
17
17
  "test:deterministic-guard": "npm run build && node scripts/test-deterministic-guard.js",
18
+ "test:desktop-chat-guard": "npm run build && node scripts/test-desktop-chat-guard.js",
18
19
  "test:taint-ledger": "npm run build && node scripts/test-taint-ledger.js",
19
20
  "test:shell-audit": "npm run build && node scripts/test-shell-audit.js",
20
21
  "test:shell-guard": "npm run build && node scripts/test-shell-guard.js",