fullcourtdefense-cli 1.18.8 → 1.18.10

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.
@@ -489,7 +489,7 @@ function bareOrActionIntent(value, action) {
489
489
  }
490
490
  function scanDeterministicPrompt(text, options) {
491
491
  const trimmed = text.trim();
492
- if (!trimmed || looksLikeQuestion(trimmed))
492
+ if (!trimmed)
493
493
  return undefined;
494
494
  const custom = customBlock(trimmed, options);
495
495
  if (custom)
@@ -506,6 +506,11 @@ function scanDeterministicPrompt(text, options) {
506
506
  if (contextualSecret) {
507
507
  return builtIn(contextualSecret.itemId, 'secret_exfiltration', 'secret_exfiltration', 'local-prompt-contextual-secret', `Blocked prompt containing ${contextualSecret.label}.`, contextualSecret.value, contextualSecret.label, options);
508
508
  }
509
+ // Questions are benign for action-oriented rules below, but this check must
510
+ // happen AFTER secret detection: "is this my API secret: <value>?" still
511
+ // leaks a credential into agent context.
512
+ if (looksLikeQuestion(trimmed))
513
+ return undefined;
509
514
  const sensitivePath = containsSensitiveCredentialPath(trimmed);
510
515
  if (sensitivePath && bareOrActionIntent(trimmed, /\b(?:read|open|cat|show|print|copy|upload|send|exfiltrate)\b/i)) {
511
516
  return builtIn(sensitivePath.itemId, 'sensitive_files', 'sensitive_file', 'local-prompt-sensitive-credential-path', `Blocked prompt targeting ${sensitivePath.label}.`, trimmed, sensitivePath.label, options);
@@ -27,6 +27,7 @@ export interface HookArgs {
27
27
  shadow?: string;
28
28
  enforce?: string;
29
29
  failClosed?: string;
30
+ localOnly?: string;
30
31
  timeout?: string;
31
32
  approvalMode?: string;
32
33
  approvalTimeoutMs?: string;
@@ -459,6 +459,7 @@ async function hookCommand(args, config) {
459
459
  // (enforcing machines fail closed, monitor/shadow fail open) — resolved below.
460
460
  const failClosedExplicit = args.failClosed !== undefined;
461
461
  let failClosed = args.failClosed === 'true';
462
+ const localOnly = args.localOnly === 'true';
462
463
  const timeoutMs = Number(args.timeout) > 0 ? Number(args.timeout) : 8000;
463
464
  const approvalMode = args.approvalMode === 'block' ? 'block' : 'wait';
464
465
  const approvalTimeoutMs = Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 900000;
@@ -654,6 +655,11 @@ async function hookCommand(args, config) {
654
655
  respond(true, `Blocked by FullCourtDefense local guard — ${localBlock.reason}`, `FullCourtDefense blocked this prompt locally (${localBlock.ruleId}). Do not retry.`);
655
656
  return;
656
657
  }
658
+ if (localOnly) {
659
+ dbg({ phase: 'local_deterministic_prompt_allow', event });
660
+ respond(false);
661
+ return;
662
+ }
657
663
  await enforceShieldText({
658
664
  event, text, payload, apiUrl: apiUrl, shieldId: shieldId, shieldKey,
659
665
  shadow, failClosed, timeoutMs, respond, client,
@@ -41,10 +41,10 @@ async function installAllCommand(args, config) {
41
41
  shieldKey: creds.shieldKey,
42
42
  apiUrl: creds.apiUrl,
43
43
  project: args.cursorProject,
44
- // Scope is MCP/tool actions only prompt text never leaves the machine.
44
+ // Prompt secrets are checked locally; prompt text never leaves the machine.
45
45
  // No failClosed flag: the offline stance is server-authoritative (from
46
46
  // the runtime bundle), unified with the Claude/VS Code hook below.
47
- events: 'shell,mcp',
47
+ events: 'prompt,shell,mcp',
48
48
  };
49
49
  try {
50
50
  await (0, installCursorHook_1.installCursorHookCommand)(hookArgs, config);
@@ -59,9 +59,9 @@ async function installAllCommand(args, config) {
59
59
  shieldId: creds.shieldId,
60
60
  shieldKey: creds.shieldKey,
61
61
  apiUrl: creds.apiUrl,
62
- // Tool actions only by default same scope as the Cursor hooks. Prompt
63
- // scanning stays opt-in (`install-claude-hook --events tools,prompt`).
64
- events: 'tools',
62
+ // Prompt secrets are checked locally; tool actions retain their existing
63
+ // Action Policy + approval behavior.
64
+ events: 'tools,prompt',
65
65
  };
66
66
  try {
67
67
  await (0, installClaudeHook_1.installClaudeHookCommand)(claudeHookArgs, config);
@@ -119,12 +119,15 @@ function repairClaudeManagedHooks() {
119
119
  settings.hooks = hooks;
120
120
  let changed = false;
121
121
  if (before.managedEntries === 0) {
122
- // Match install-all's privacy-preserving baseline: tool actions only.
123
- // Prompt scanning remains an explicit opt-in and is preserved when present.
122
+ // Match install-all's privacy-preserving baseline: tool policies plus
123
+ // deterministic on-device prompt secret protection.
124
124
  const command = buildHookCommand({ shadow: false, failClosed: false });
125
125
  const tools = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
126
126
  tools.push({ matcher: '*', hooks: [{ type: 'command', command, timeout: 300 }] });
127
127
  hooks.PreToolUse = tools;
128
+ const prompts = Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
129
+ prompts.push({ hooks: [{ type: 'command', command, timeout: 30 }] });
130
+ hooks.UserPromptSubmit = prompts;
128
131
  changed = true;
129
132
  }
130
133
  else {
@@ -135,6 +138,19 @@ function repairClaudeManagedHooks() {
135
138
  changed = true;
136
139
  }
137
140
  }
141
+ const prompts = Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
142
+ const hasManagedPrompt = prompts.some(entry => Array.isArray(entry.hooks) && entry.hooks.some(isManagedCommand));
143
+ if (!hasManagedPrompt) {
144
+ prompts.push({
145
+ hooks: [{
146
+ type: 'command',
147
+ command: buildHookCommand({ shadow: false, failClosed: false }),
148
+ timeout: 30,
149
+ }],
150
+ });
151
+ hooks.UserPromptSubmit = prompts;
152
+ changed = true;
153
+ }
138
154
  }
139
155
  if (changed) {
140
156
  fs.mkdirSync(path.dirname(before.file), { recursive: true });
@@ -169,7 +185,9 @@ function buildHookCommand(opts) {
169
185
  const scriptPath = process.argv[1] || path.join(__dirname, '..', 'index.js');
170
186
  const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
171
187
  // No --event flag: the bridge detects the Claude payload (hook_event_name) itself.
172
- let cmd = `${q(nodeExe)} ${q(scriptPath)} hook --approval-mode wait`;
188
+ // local-only affects prompt events only; tool events still use the complete
189
+ // Action Policy/approval path. This keeps prompt text on the endpoint.
190
+ let cmd = `${q(nodeExe)} ${q(scriptPath)} hook --approval-mode wait --local-only true`;
173
191
  if (opts.shadow)
174
192
  cmd += ' --shadow true';
175
193
  if (opts.failClosed)
@@ -239,7 +257,7 @@ async function installClaudeHookCommand(args, config) {
239
257
  if (wantTools)
240
258
  parts.push('tool calls (shell / MCP / file writes / reads) checked against org Action Policies + Local Safety rules');
241
259
  if (wantPrompt)
242
- parts.push('prompts scanned by your Shield');
260
+ parts.push('prompts checked by deterministic Local Safety on-device (text is not uploaded)');
243
261
  console.log(`${COLOR.gray}Enforcement:${COLOR.reset} ${parts.join('; ')}`);
244
262
  if (shadow)
245
263
  console.log(`${COLOR.yellow}Mode: SHADOW (monitor only — nothing is blocked).${COLOR.reset}`);
@@ -80,6 +80,8 @@ function buildHookCommand(flag, opts) {
80
80
  cmd += ' --shadow true';
81
81
  if (opts.failClosed)
82
82
  cmd += ' --fail-closed true';
83
+ if (opts.localOnly)
84
+ cmd += ' --local-only true';
83
85
  if (opts.waitForApproval)
84
86
  cmd += ' --approval-mode wait';
85
87
  cmd += ` ${MANAGED_MARKER}`;
@@ -145,12 +147,17 @@ function repairCursorManagedHooks() {
145
147
  const json = readHooksJson(before.file);
146
148
  let changed = false;
147
149
  if (before.managedEntries === 0) {
148
- for (const event of ['shell', 'mcp']) {
150
+ for (const event of ['prompt', 'shell', 'mcp']) {
149
151
  const { hookKey, flag, timeoutSec } = EVENT_MAP[event];
150
152
  const list = Array.isArray(json.hooks[hookKey]) ? json.hooks[hookKey] : [];
151
153
  list.push({
152
154
  // Offline stance comes from the runtime bundle at hook time.
153
- command: buildHookCommand(flag, { shadow: false, failClosed: false, waitForApproval: true }),
155
+ command: buildHookCommand(flag, {
156
+ shadow: false,
157
+ failClosed: false,
158
+ waitForApproval: event !== 'prompt',
159
+ localOnly: event === 'prompt',
160
+ }),
154
161
  timeout: timeoutSec,
155
162
  });
156
163
  json.hooks[hookKey] = list;
@@ -171,6 +178,25 @@ function repairCursorManagedHooks() {
171
178
  }
172
179
  }
173
180
  }
181
+ // Existing managed installations predate prompt protection. Add only the
182
+ // missing local prompt entry; preserve every shell/MCP entry and unrelated
183
+ // customer hook exactly as-is.
184
+ const promptEntries = Array.isArray(json.hooks.beforeSubmitPrompt)
185
+ ? json.hooks.beforeSubmitPrompt
186
+ : [];
187
+ if (!promptEntries.some(isManaged)) {
188
+ promptEntries.push({
189
+ command: buildHookCommand('prompt', {
190
+ shadow: false,
191
+ failClosed: false,
192
+ waitForApproval: false,
193
+ localOnly: true,
194
+ }),
195
+ timeout: EVENT_MAP.prompt.timeoutSec,
196
+ });
197
+ json.hooks.beforeSubmitPrompt = promptEntries;
198
+ changed = true;
199
+ }
174
200
  }
175
201
  if (changed) {
176
202
  fs.mkdirSync(path.dirname(before.file), { recursive: true });
@@ -192,7 +218,7 @@ async function installCursorHookCommand(args, config) {
192
218
  const projectScope = args.project === 'true';
193
219
  const shadow = args.shadow === 'true';
194
220
  const file = hooksJsonPath(projectScope);
195
- const requested = (args.events || 'shell,mcp')
221
+ const requested = (args.events || 'prompt,shell,mcp')
196
222
  .split(/[\s,]+/).map((e) => e.trim().toLowerCase()).filter(Boolean);
197
223
  const events = requested.filter((e) => EVENT_MAP[e]);
198
224
  if (events.length === 0) {
@@ -222,7 +248,14 @@ async function installCursorHookCommand(args, config) {
222
248
  // stance from the runtime bundle (same behavior as the Claude hook).
223
249
  const failClosed = e === 'prompt' ? false : args.failClosed === 'true';
224
250
  const entry = {
225
- command: buildHookCommand(flag, { shadow, failClosed, waitForApproval: e !== 'prompt' }),
251
+ command: buildHookCommand(flag, {
252
+ shadow,
253
+ failClosed,
254
+ waitForApproval: e !== 'prompt',
255
+ // Prompt secrets are blocked entirely on-device. Benign prompt text is
256
+ // never sent to the Shield service by the managed default.
257
+ localOnly: e === 'prompt',
258
+ }),
226
259
  timeout: timeoutSec,
227
260
  };
228
261
  if (failClosed)
@@ -242,7 +275,7 @@ async function installCursorHookCommand(args, config) {
242
275
  console.log(`${COLOR.gray}Enforcement:${COLOR.reset} ${actionEvents.join(', ')} checked against your org Action Policies (allow / block / wait-for-human-approval) in every repo.`);
243
276
  }
244
277
  if (events.includes('prompt')) {
245
- console.log(`${COLOR.gray}Prompts:${COLOR.reset} scanned by your Shield (developer-chat).`);
278
+ console.log(`${COLOR.gray}Prompts:${COLOR.reset} deterministic Local Safety scan on-device (prompt text is not uploaded).`);
246
279
  }
247
280
  if (shadow)
248
281
  console.log(`${COLOR.yellow}Mode: SHADOW (monitor only — nothing is blocked).${COLOR.reset}`);
package/dist/index.js CHANGED
@@ -613,6 +613,7 @@ async function main() {
613
613
  shadow: flags.shadow,
614
614
  enforce: flags.enforce,
615
615
  failClosed: flags['fail-closed'],
616
+ localOnly: flags['local-only'],
616
617
  timeout: flags.timeout,
617
618
  approvalMode: flags['approval-mode'],
618
619
  approvalTimeoutMs: flags['approval-timeout-ms'],
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.18.8"
2
+ "version": "1.18.10"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.18.8",
3
+ "version": "1.18.10",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {