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.
- package/dist/commands/deterministicGuard.js +6 -1
- package/dist/commands/hook.d.ts +1 -0
- package/dist/commands/hook.js +6 -0
- package/dist/commands/installAll.js +5 -5
- package/dist/commands/installClaudeHook.js +22 -4
- package/dist/commands/installCursorHook.js +38 -5
- package/dist/index.js +1 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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);
|
package/dist/commands/hook.d.ts
CHANGED
package/dist/commands/hook.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
63
|
-
//
|
|
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
|
|
123
|
-
//
|
|
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
|
-
|
|
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
|
|
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, {
|
|
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, {
|
|
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}
|
|
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