fullcourtdefense-cli 1.1.0 → 1.1.2

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.
@@ -206,11 +206,6 @@ function sessionId(p) {
206
206
  return fromPayload;
207
207
  return `cursor-${os.hostname()}`;
208
208
  }
209
- function emit(decision, blocked) {
210
- process.stdout.write(JSON.stringify(decision));
211
- // Exit 2 is the universal "block" signal across all hook events.
212
- process.exit(blocked ? 2 : 0);
213
- }
214
209
  async function hookCommand(args, config) {
215
210
  const shadow = args.shadow === 'true';
216
211
  const failClosed = args.failClosed === 'true';
@@ -236,15 +231,29 @@ async function hookCommand(args, config) {
236
231
  }
237
232
  const event = inferEvent(args.event, payload);
238
233
  const text = extractText(event, payload).trim();
234
+ // Emit the verdict in the shape Cursor expects for THIS event, then exit.
235
+ // beforeSubmitPrompt blocks via { continue: false }; the execution hooks
236
+ // (shell/mcp/read) block via { permission: "deny" }. Exit 2 reinforces it.
237
+ const respond = (blocked, userMsg, agentMsg) => {
238
+ const decision = event === 'prompt'
239
+ ? (blocked ? { continue: false } : { continue: true })
240
+ : { permission: blocked ? 'deny' : 'allow' };
241
+ if (userMsg)
242
+ decision.user_message = userMsg;
243
+ if (agentMsg)
244
+ decision.agent_message = agentMsg;
245
+ process.stdout.write(JSON.stringify(decision));
246
+ // ALWAYS exit 0 so Cursor uses our JSON verdict. Exit 2 is interpreted as
247
+ // `permission: "deny"`, which beforeSubmitPrompt ignores (it uses `continue`),
248
+ // so exiting 2 would silently let blocked prompts through.
249
+ process.exit(0);
250
+ };
239
251
  // Nothing to scan, or no Shield configured → allow (fail-open by design here:
240
252
  // a misconfigured machine must not brick the developer's IDE).
241
253
  if (!text)
242
- emit({ permission: 'allow' }, false);
254
+ respond(false);
243
255
  if (!shieldId) {
244
- emit({
245
- permission: 'allow',
246
- agent_message: 'FullCourtDefense hook installed but no Shield ID configured (run `fullcourtdefense configure`).',
247
- }, false);
256
+ respond(false, undefined, 'FullCourtDefense hook installed but no Shield ID configured (run `fullcourtdefense configure`).');
248
257
  }
249
258
  try {
250
259
  const controller = new AbortController();
@@ -269,12 +278,7 @@ async function hookCommand(args, config) {
269
278
  clearTimeout(timer);
270
279
  if (!resp.ok) {
271
280
  // Backend rejected (quota, bad shield, etc.) — fail open unless told otherwise.
272
- const blocked = failClosed;
273
- emit({
274
- permission: blocked ? 'deny' : 'allow',
275
- user_message: blocked ? `FullCourtDefense gate unavailable (HTTP ${resp.status}) — blocked by fail-closed policy.` : undefined,
276
- agent_message: `FullCourtDefense gate returned HTTP ${resp.status}.`,
277
- }, blocked);
281
+ respond(failClosed, failClosed ? `FullCourtDefense gate unavailable (HTTP ${resp.status}) — blocked by fail-closed policy.` : undefined, `FullCourtDefense gate returned HTTP ${resp.status}.`);
278
282
  }
279
283
  const data = await resp.json().catch(() => ({}));
280
284
  const sh = (data._shield || {});
@@ -285,28 +289,16 @@ async function hookCommand(args, config) {
285
289
  const reason = String(sh.reason || 'policy_violation');
286
290
  const explanation = String(sh.reason || 'Flagged by FullCourtDefense.');
287
291
  if (!isBlocked) {
288
- emit({ permission: 'allow' }, false);
292
+ respond(false);
289
293
  }
290
294
  // Blocked verdict. In shadow mode we annotate but let it through.
291
295
  if (shadow) {
292
- emit({
293
- permission: 'allow',
294
- agent_message: `[FullCourtDefense shadow] would block ${event} (${reason}): ${explanation}`,
295
- }, false);
296
+ respond(false, undefined, `[FullCourtDefense shadow] would block ${event} (${reason}): ${explanation}`);
296
297
  }
297
- emit({
298
- permission: 'deny',
299
- user_message: `Blocked by FullCourtDefense — ${reason}. ${explanation}`,
300
- agent_message: `FullCourtDefense blocked this ${event} as "${reason}". Do not retry; revise to remove the flagged content.`,
301
- }, true);
298
+ respond(true, `Blocked by FullCourtDefense — ${reason}.`, `FullCourtDefense blocked this ${event} as "${reason}". Do not retry; revise to remove the flagged content.`);
302
299
  }
303
300
  catch (err) {
304
301
  // Network error / timeout.
305
- const blocked = failClosed;
306
- emit({
307
- permission: blocked ? 'deny' : 'allow',
308
- user_message: blocked ? 'FullCourtDefense gate unreachable — blocked by fail-closed policy.' : undefined,
309
- agent_message: `FullCourtDefense hook error: ${err instanceof Error ? err.message : String(err)}.`,
310
- }, blocked);
302
+ respond(failClosed, failClosed ? 'FullCourtDefense gate unreachable — blocked by fail-closed policy.' : undefined, `FullCourtDefense hook error: ${err instanceof Error ? err.message : String(err)}.`);
311
303
  }
312
304
  }
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ const configure_1 = require("./commands/configure");
10
10
  const discover_1 = require("./commands/discover");
11
11
  const hook_1 = require("./commands/hook");
12
12
  const installCursorHook_1 = require("./commands/installCursorHook");
13
- const VERSION = '1.1.0';
13
+ const VERSION = '1.1.2';
14
14
  function parseArgs(argv) {
15
15
  const flags = {};
16
16
  let command = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {