fullcourtdefense-cli 1.21.7 → 1.21.8

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.
@@ -0,0 +1,2 @@
1
+ import { DeterministicFinding, LocalSafetyScanOptions } from './deterministicGuard';
2
+ export declare function scanDeterministicClipboard(text: string, options?: LocalSafetyScanOptions): DeterministicFinding | undefined;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scanDeterministicClipboard = scanDeterministicClipboard;
4
+ const deterministicGuard_1 = require("./deterministicGuard");
5
+ /**
6
+ * Clipboard scan for advisory paste protection (e.g. Claude Desktop, where the
7
+ * native chat has no hook and the MCP gateway can't see typed prompts). A
8
+ * recognizable credential sitting on the clipboard is a finding on its own —
9
+ * the very next action is a paste the guard cannot observe.
10
+ *
11
+ * This composes the SAME deterministic engine used everywhere else via its
12
+ * public API, so detection patterns and per-rule Local Safety toggles are
13
+ * identical. No new detection logic and no keyword list lives here.
14
+ *
15
+ * - Prefixed secrets (sk-, ghp_, AKIA…) and keyword-adjacent high-entropy
16
+ * strings are caught directly by scanDeterministicTextResponse.
17
+ * - A clipboard that is ONE bare credential-shaped token has, by definition,
18
+ * no surrounding keyword. We re-run the same contextual detector with a
19
+ * synthetic keyword so its entropy + shape gate still decides — a lone hex
20
+ * app secret (the exact WhatsApp-secret scenario) is caught, while a hash
21
+ * embedded in a sentence or diff stays silent because it is never a lone
22
+ * token.
23
+ */
24
+ const LONE_TOKEN = /^[A-Za-z0-9_\-+/=]{24,256}$/;
25
+ function scanDeterministicClipboard(text, options) {
26
+ const trimmed = (text || '').trim();
27
+ if (!trimmed)
28
+ return undefined;
29
+ const direct = (0, deterministicGuard_1.scanDeterministicTextResponse)(trimmed, options);
30
+ if (direct)
31
+ return direct;
32
+ if (LONE_TOKEN.test(trimmed)) {
33
+ // Inject a canonical keyword so the contextual detector's proximity check
34
+ // passes; its high-entropy gate still does the real work. The synthetic
35
+ // prefix is never surfaced — the finding's evidence comes from the token.
36
+ return (0, deterministicGuard_1.scanDeterministicTextResponse)(`api key ${trimmed}`, options);
37
+ }
38
+ return undefined;
39
+ }
@@ -12,14 +12,24 @@ export declare function desktopChatGuardSupported(): boolean;
12
12
  export declare function desktopChatGuardHealthy(withinMs?: number): boolean;
13
13
  /**
14
14
  * The PowerShell watcher. Compiles a tiny MSAA reader (C# via Add-Type using
15
- * the .NET Framework compiler built into Windows), then — only while Claude is
16
- * the foreground process — reads the composer's editable text and emits
17
- * `FCD:<base64 utf8>` on stdout when it changes. Everything is wrapped in
18
- * try/catch it must never crash the host.
15
+ * the .NET Framework compiler built into Windows), then — while the "Claude"
16
+ * process is running — reads the composer's editable text (emitting
17
+ * `FCD:<base64 utf8>`) and, only while Claude Desktop is the FOREGROUND window,
18
+ * the clipboard contents (emitting `FCDCLIP:<base64 utf8>`) when either
19
+ * changes. Everything is wrapped in try/catch — it must never crash the host,
20
+ * and it never writes to the clipboard or synthesizes input.
19
21
  */
20
22
  export declare function desktopChatWatcherScript(): string;
21
- /** Decode one `FCD:<base64>` watcher line into UTF-8 text (undefined if not ours). */
22
- export declare function decodeWatcherLine(line: string): string | undefined;
23
+ export type WatcherSource = 'composer' | 'clipboard';
24
+ /**
25
+ * Decode one watcher line into { source, text } (undefined if not ours).
26
+ * `FCD:` lines are composer reads; `FCDCLIP:` lines are clipboard reads while
27
+ * Claude Desktop is focused.
28
+ */
29
+ export declare function decodeWatcherLine(line: string): {
30
+ source: WatcherSource;
31
+ text: string;
32
+ } | undefined;
23
33
  export interface DesktopChatGuardHandle {
24
34
  stop(): void;
25
35
  }
@@ -47,6 +47,7 @@ const readline = __importStar(require("readline"));
47
47
  const config_1 = require("../config");
48
48
  const localSafetySnapshot_1 = require("../localSafetySnapshot");
49
49
  const deterministicGuard_1 = require("./deterministicGuard");
50
+ const clipboardScan_1 = require("./clipboardScan");
50
51
  const telemetry_1 = require("../telemetry");
51
52
  const notify_1 = require("../notify");
52
53
  const machineIdentity_1 = require("../machineIdentity");
@@ -78,21 +79,27 @@ const discoverPaths_1 = require("./discoverPaths");
78
79
  * The SUPPORTED protection surface for Claude Desktop is its MCP tool calls
79
80
  * through the FCD gateway — that is where enforcement happens.
80
81
  *
81
- * Advisory ONLY: no keyboard hook, no input mutation, no keystroke capture. It
82
- * reads only the composer text of Claude Desktop's own window (scoped to the
83
- * "Claude" process never any other application), scans it in-process, and —
84
- * on a finding shows a native toast and reports a monitor event to the fleet.
85
- * Text is never uploaded; only finding metadata (item id + masked value) is
86
- * spooled. It cannot break typing and presents no keylogger surface.
82
+ * Paste protection (clipboard) the reliable surface:
83
+ * Because reading the composer is best-effort, the guard ALSO watches the
84
+ * Windows clipboard, but ONLY while Claude Desktop is the foreground window.
85
+ * When a developer pastes a secret into Claude (the exact scenario we keep
86
+ * testing), the value is on the clipboard at that moment the guard runs it
87
+ * through the SAME deterministic engine and warns before it reaches the model.
88
+ * Scoping to the Claude foreground window means a secret copied for any other
89
+ * app is never inspected. This is app-agnostic detection reused only here.
87
90
  *
88
- * Deliberately NOT gated on the foreground window: a foreground check goes blind
89
- * the instant the user alt-tabs after typing, and MSAA reads Claude's composer
90
- * whether or not Claude is focused. Scoping to the Claude process (not the
91
- * active window) is what keeps this from ever reading other apps.
91
+ * Advisory ONLY: no keyboard hook, no input mutation, no keystroke capture, and
92
+ * it never writes to the clipboard. It reads the composer text and (only when
93
+ * Claude is focused) the clipboard, scans in-process, and — on a finding —
94
+ * shows a native warning window and reports a monitor event to the fleet. Text
95
+ * is never uploaded; only finding metadata (item id + masked value) is spooled.
96
+ * It cannot break typing and presents no keylogger surface.
92
97
  */
93
98
  const WATCHER_PS1_PATH = path.join(os.homedir(), '.fullcourtdefense-claude-desktop-watcher.ps1');
94
99
  const STATUS_PATH = path.join(os.homedir(), '.fullcourtdefense', 'claude-desktop-guard.json');
95
100
  const STDOUT_PREFIX = 'FCD:';
101
+ /** Prefix for clipboard payloads (distinct from composer reads). */
102
+ const CLIP_PREFIX = 'FCDCLIP:';
96
103
  /** Poll cadence for the composer read (ms). 800ms lost the race against a
97
104
  * fast paste+Enter (the composer clears before the next read); 250ms is still
98
105
  * negligible CPU for one MSAA tree walk. */
@@ -135,10 +142,12 @@ function desktopChatGuardHealthy(withinMs = 15 * 60_000) {
135
142
  }
136
143
  /**
137
144
  * The PowerShell watcher. Compiles a tiny MSAA reader (C# via Add-Type using
138
- * the .NET Framework compiler built into Windows), then — only while Claude is
139
- * the foreground process — reads the composer's editable text and emits
140
- * `FCD:<base64 utf8>` on stdout when it changes. Everything is wrapped in
141
- * try/catch it must never crash the host.
145
+ * the .NET Framework compiler built into Windows), then — while the "Claude"
146
+ * process is running — reads the composer's editable text (emitting
147
+ * `FCD:<base64 utf8>`) and, only while Claude Desktop is the FOREGROUND window,
148
+ * the clipboard contents (emitting `FCDCLIP:<base64 utf8>`) when either
149
+ * changes. Everything is wrapped in try/catch — it must never crash the host,
150
+ * and it never writes to the clipboard or synthesizes input.
142
151
  */
143
152
  function desktopChatWatcherScript() {
144
153
  return [
@@ -163,8 +172,15 @@ function desktopChatWatcherScript() {
163
172
  ' [DllImport("user32.dll")] static extern int GetWindowText(IntPtr h, StringBuilder sb, int max);',
164
173
  ' [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);',
165
174
  ' [DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr h, uint msg, IntPtr wParam, IntPtr lParam);',
175
+ ' [DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();',
166
176
  ' const uint WM_GETOBJECT = 0x003D;',
167
177
  ' public delegate bool EnumProc(IntPtr h, IntPtr p);',
178
+ ' public static bool ClaudeIsForeground(HashSet<uint> pids) {',
179
+ ' IntPtr fg = GetForegroundWindow();',
180
+ ' if (fg == IntPtr.Zero) return false;',
181
+ ' uint pid; GetWindowThreadProcessId(fg, out pid);',
182
+ ' return pids.Contains(pid);',
183
+ ' }',
168
184
  ' public static IntPtr FindMain(HashSet<uint> pids) {',
169
185
  ' IntPtr result = IntPtr.Zero;',
170
186
  ' EnumWindows((h, p) => {',
@@ -227,6 +243,7 @@ function desktopChatWatcherScript() {
227
243
  '} catch { }',
228
244
  '',
229
245
  '$last = ""',
246
+ '$lastClip = ""',
230
247
  '$lastWake = [DateTime]::MinValue',
231
248
  'while ($true) {',
232
249
  ' Start-Sleep -Milliseconds ' + POLL_MS,
@@ -234,6 +251,19 @@ function desktopChatWatcherScript() {
234
251
  ' $pids = New-Object \'System.Collections.Generic.HashSet[uint32]\'',
235
252
  " Get-Process -Name 'Claude' -ErrorAction SilentlyContinue | ForEach-Object { [void]$pids.Add([uint32]$_.Id) }",
236
253
  ' if ($pids.Count -eq 0) { continue }',
254
+ // Clipboard paste guard: only while Claude Desktop is the foreground app, so
255
+ // a copy staged for any OTHER application is never inspected or reported.
256
+ // The deterministic engine (parent process) decides if the text is a secret.
257
+ ' if ([FcdMsaa]::ClaudeIsForeground($pids)) {',
258
+ ' $clip = ""',
259
+ ' try { $clip = Get-Clipboard -Raw -ErrorAction SilentlyContinue } catch { }',
260
+ ' if (-not [string]::IsNullOrEmpty($clip) -and $clip.Length -le 8000 -and $clip -ne $lastClip) {',
261
+ ' $lastClip = $clip',
262
+ ' $cb = [System.Text.Encoding]::UTF8.GetBytes($clip)',
263
+ " [Console]::Out.WriteLine('" + CLIP_PREFIX + "' + [Convert]::ToBase64String($cb))",
264
+ ' [Console]::Out.Flush()',
265
+ ' }',
266
+ ' }',
237
267
  ' $main = [FcdMsaa]::FindMain($pids)',
238
268
  ' if ($main -eq [IntPtr]::Zero) { continue }',
239
269
  ' if (([DateTime]::UtcNow - $lastWake).TotalSeconds -ge 30) { [FcdMsaa]::Wake($main); $lastWake = [DateTime]::UtcNow }',
@@ -260,12 +290,21 @@ function desktopChatWatcherScript() {
260
290
  '',
261
291
  ].join('\n');
262
292
  }
263
- /** Decode one `FCD:<base64>` watcher line into UTF-8 text (undefined if not ours). */
293
+ /**
294
+ * Decode one watcher line into { source, text } (undefined if not ours).
295
+ * `FCD:` lines are composer reads; `FCDCLIP:` lines are clipboard reads while
296
+ * Claude Desktop is focused.
297
+ */
264
298
  function decodeWatcherLine(line) {
265
- if (!line.startsWith(STDOUT_PREFIX))
299
+ const prefix = line.startsWith(CLIP_PREFIX)
300
+ ? { p: CLIP_PREFIX, source: 'clipboard' }
301
+ : line.startsWith(STDOUT_PREFIX)
302
+ ? { p: STDOUT_PREFIX, source: 'composer' }
303
+ : undefined;
304
+ if (!prefix)
266
305
  return undefined;
267
306
  try {
268
- return Buffer.from(line.slice(STDOUT_PREFIX.length), 'base64').toString('utf8');
307
+ return { source: prefix.source, text: Buffer.from(line.slice(prefix.p.length), 'base64').toString('utf8') };
269
308
  }
270
309
  catch {
271
310
  return undefined;
@@ -312,10 +351,12 @@ function startDesktopChatGuard(runtime) {
312
351
  startedAt,
313
352
  heartbeatAt: new Date().toISOString(),
314
353
  });
315
- const handleText = (text) => {
354
+ const handleText = (text, source) => {
316
355
  if (!text.trim())
317
356
  return;
318
- const finding = (0, deterministicGuard_1.scanDeterministicPrompt)(text, scanOptions);
357
+ const finding = source === 'clipboard'
358
+ ? (0, clipboardScan_1.scanDeterministicClipboard)(text, scanOptions)
359
+ : (0, deterministicGuard_1.scanDeterministicPrompt)(text, scanOptions);
319
360
  if (!finding)
320
361
  return;
321
362
  findings += 1;
@@ -327,7 +368,7 @@ function startDesktopChatGuard(runtime) {
327
368
  (0, telemetry_1.spoolEvent)({
328
369
  decision: 'allow',
329
370
  toolName: 'claude_desktop_chat',
330
- operation: 'prompt',
371
+ operation: source === 'clipboard' ? 'clipboard_paste' : 'prompt',
331
372
  reason: `would block: ${finding.reason}`,
332
373
  ruleId: finding.ruleId,
333
374
  category: finding.category,
@@ -339,21 +380,24 @@ function startDesktopChatGuard(runtime) {
339
380
  policyHash: finding.policyHash,
340
381
  });
341
382
  (0, telemetry_1.triggerFlush)();
342
- const key = `${finding.itemId}:${masked}`;
383
+ const key = `${source}:${finding.itemId}:${masked}`;
343
384
  const now = Date.now();
344
385
  const last = lastToastAt.get(key) || 0;
345
386
  if (!runtime.quiet && now - last > TOAST_DEBOUNCE_MS) {
346
387
  lastToastAt.set(key, now);
388
+ const message = source === 'clipboard'
389
+ ? `${finding.reason} You just copied this — do NOT paste it into Claude Desktop; its chat is not protected.`
390
+ : `${finding.reason}. Remove it before sending — this text has not been protected.`;
347
391
  (0, notify_1.notifyOs)({
348
392
  title: 'FullCourtDefense: secret in Claude Desktop',
349
- message: `${finding.reason}. Remove it before sending — this text has not been protected.`,
393
+ message,
350
394
  // Security warning: use a top-most window, not a toast. Toasts are
351
395
  // silently swallowed by Focus Assist / Do-Not-Disturb and per-app
352
396
  // banner settings — verified in the field — which would hide the alert.
353
397
  forceWindow: true,
354
398
  });
355
399
  }
356
- runtime.log(`Claude Desktop chat finding: ${finding.itemId} (${finding.reason}).`);
400
+ runtime.log(`Claude Desktop ${source} finding: ${finding.itemId} (${finding.reason}).`);
357
401
  };
358
402
  const spawnWatcher = () => {
359
403
  if (stopped)
@@ -376,11 +420,11 @@ function startDesktopChatGuard(runtime) {
376
420
  }
377
421
  const rl = readline.createInterface({ input: child.stdout });
378
422
  rl.on('line', (line) => {
379
- const text = decodeWatcherLine(line);
380
- if (text === undefined)
423
+ const decoded = decodeWatcherLine(line);
424
+ if (decoded === undefined)
381
425
  return;
382
426
  restartDelay = RESTART_MIN_MS; // healthy output resets backoff
383
- handleText(text);
427
+ handleText(decoded.text, decoded.source);
384
428
  });
385
429
  child.on('exit', () => { rl.close(); if (!stopped)
386
430
  scheduleRestart(); });
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.21.7"
2
+ "version": "1.21.8"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.21.7",
3
+ "version": "1.21.8",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "test:posture-overhaul": "npm run build && node scripts/test-posture-overhaul.js",
36
36
  "test:dpapi-config": "npm run build && node scripts/test-dpapi-config.js",
37
37
  "test:desktop-chat-guard": "npm run build && node scripts/test-desktop-chat-guard.js",
38
+ "test:clipboard-scan": "npm run build && node scripts/test-clipboard-scan.js",
38
39
  "build:msi": "powershell -NoProfile -ExecutionPolicy Bypass -File installer/windows/Build-Msi.ps1",
39
40
  "prepublishOnly": "npm run build"
40
41
  },