@rigour-labs/cli 5.5.0 → 5.5.1

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/README.md CHANGED
@@ -29,7 +29,7 @@ brew install rigour
29
29
 
30
30
  AI agents are powerful but ungoverned. They claim success based on narrative, not execution. Credentials get cached in agent memory. Imports get hallucinated. Code quality drifts across sessions.
31
31
 
32
- **Rigour breaks this cycle** with deterministic PASS/FAIL gates, credential interception, and memory governance — all local-first.
32
+ **Rigour breaks this cycle** with deterministic PASS/FAIL gates, credential warnings, and memory governance — all local-first.
33
33
 
34
34
  ## 🔄 How It Works
35
35
 
@@ -80,7 +80,7 @@ Every failure carries a provenance tag (`ai-drift`, `traditional`, `security`, `
80
80
 
81
81
  ## 🔒 AI Agent DLP (Data Loss Prevention)
82
82
 
83
- Real-time credential interception via PreToolUse hooks — blocks credentials before agents see them.
83
+ Real-time credential detection via PreToolUse hooks — warns by default before agent actions.
84
84
 
85
85
  - **29 credential patterns**: AWS, GCP, Azure, OpenAI, Anthropic, GitHub, Stripe, private keys, database URLs, JWTs, CI/CD tokens
86
86
  - **Anti-evasion**: Unicode normalization, zero-width char removal, bidi control stripping, Shannon entropy detection (>4.5 bits)
package/dist/cli.js CHANGED
@@ -267,7 +267,7 @@ const hooksCmd = program
267
267
  .description('Manage AI coding tool hook integrations (file checks + DLP credential scanning)')
268
268
  .addHelpText('after', `
269
269
  DLP false-positive learning:
270
- When a hook blocks your prompt incorrectly, teach Rigour once:
270
+ When a hook warns about your prompt incorrectly, teach Rigour once:
271
271
  $ rigour hooks check --dlp-allow-last
272
272
 
273
273
  Learned patterns are stored per-project in .rigour/dlp-feedback.json.
@@ -305,7 +305,7 @@ hooksCmd
305
305
  .option('--timeout <ms>', 'Timeout in milliseconds (default: 5000)')
306
306
  .option('--mode <mode>', 'Check mode: "check" (default) or "dlp" (credential scanning)')
307
307
  .option('--agent <name>', 'Agent name for DLP audit trail (e.g., cursor, claude)')
308
- .option('--dlp-allow-last', 'Record last DLP block as learned false positives (hook feedback)')
308
+ .option('--dlp-allow-last', 'Record the last DLP warning as learned false positives (hook feedback)')
309
309
  .addHelpText('after', `
310
310
  Examples:
311
311
  $ rigour hooks check --files src/app.ts
@@ -315,8 +315,8 @@ Examples:
315
315
  $ rigour hooks check --dlp-allow-last
316
316
 
317
317
  DLP learning:
318
- After a false-positive block, run --dlp-allow-last to store the detection shape
319
- in .rigour/dlp-feedback.json. Future scans allow matching patterns on the same line.
318
+ After a false-positive warning, run --dlp-allow-last to store a safe fingerprint
319
+ in .rigour/dlp-feedback.json. Future scans allow matching generic patterns.
320
320
  Provider keys and high-confidence secrets are never learned away.
321
321
  `)
322
322
  .action(async (options) => {
@@ -135,6 +135,8 @@ function buildAuditPackage(cwd, report, config) {
135
135
  score_trend: trend ? {
136
136
  direction: trend.direction,
137
137
  delta: trend.delta,
138
+ visible_delta: trend.visibleDelta
139
+ ?? trend.recentScores[trend.recentScores.length - 1] - trend.recentScores[0],
138
140
  recent_average: trend.recentAvg,
139
141
  previous_average: trend.previousAvg,
140
142
  last_scores: trend.recentScores,
@@ -234,7 +236,8 @@ function renderMarkdown(audit) {
234
236
  lines.push(`**Direction:** ${audit.score_trend.direction} ${arrow}`);
235
237
  lines.push(`**Recent Average:** ${audit.score_trend.recent_average}/100`);
236
238
  lines.push(`**Previous Average:** ${audit.score_trend.previous_average}/100`);
237
- lines.push(`**Delta:** ${audit.score_trend.delta > 0 ? '+' : ''}${audit.score_trend.delta}`);
239
+ lines.push(`**Visible Delta:** ${audit.score_trend.visible_delta > 0 ? '+' : ''}${audit.score_trend.visible_delta}`);
240
+ lines.push(`**Window Average Delta:** ${audit.score_trend.delta > 0 ? '+' : ''}${audit.score_trend.delta}`);
238
241
  lines.push(`**Recent Scores:** ${audit.score_trend.last_scores.join(' → ')}`);
239
242
  lines.push('');
240
243
  }
@@ -13,9 +13,9 @@ export function guideCommand() {
13
13
  console.log(chalk.yellow(' • Strategic Guardians') + chalk.dim(': Dependency and Architectural boundary enforcement.\n'));
14
14
  console.log(chalk.bold('Hooks & DLP Learning:'));
15
15
  console.log(chalk.dim(' 1. Run ') + chalk.cyan('rigour hooks init') + chalk.dim(' to wire Cursor/Claude/Cline/Windsurf hooks.'));
16
- console.log(chalk.dim(' 2. When DLP blocks a prompt falsely, run ') + chalk.cyan('rigour hooks check --dlp-allow-last'));
16
+ console.log(chalk.dim(' 2. When DLP warns about a prompt falsely, run ') + chalk.cyan('rigour hooks check --dlp-allow-last'));
17
17
  console.log(chalk.dim(' to teach Rigour that pattern is safe (stored in ') + chalk.cyan('.rigour/dlp-feedback.json') + chalk.dim(').'));
18
- console.log(chalk.dim(' 3. Real provider keys (OpenAI, AWS, etc.) still block learning only relaxes generic shapes.\n'));
18
+ console.log(chalk.dim(' 3. Real provider keys (OpenAI, AWS, etc.) cannot be learned away.\n'));
19
19
  console.log(chalk.bold('Workflow Integration:'));
20
20
  console.log(chalk.green(' • Cursor') + chalk.dim(': Add the MCP server or use the ') + chalk.cyan('.cursor/rules/rigour.mdc') + chalk.dim(' handshake.'));
21
21
  console.log(chalk.green(' • CI/CD') + chalk.dim(': Use ') + chalk.cyan('rigour check --ci') + chalk.dim(' to fail PRs that violate quality gates.'));
@@ -18,7 +18,7 @@ export interface HooksOptions {
18
18
  dryRun?: boolean;
19
19
  force?: boolean;
20
20
  block?: boolean;
21
- /** Also generate DLP (pre-input credential interception) hooks */
21
+ /** Also generate DLP pre-input warning hooks */
22
22
  dlp?: boolean;
23
23
  }
24
24
  export interface HooksCheckOptions {
@@ -30,7 +30,7 @@ export interface HooksCheckOptions {
30
30
  mode?: 'check' | 'dlp';
31
31
  /** Agent name for audit trail (DLP mode) */
32
32
  agent?: string;
33
- /** Record last DLP block detections as learned false positives (hook feedback) */
33
+ /** Record last DLP warning detections as learned false positives (hook feedback) */
34
34
  dlpAllowLast?: boolean;
35
35
  }
36
36
  export declare function hooksInitCommand(cwd: string, options?: HooksOptions): Promise<void>;
@@ -17,7 +17,18 @@ import fs from 'fs-extra';
17
17
  import path from 'path';
18
18
  import chalk from 'chalk';
19
19
  import { randomUUID } from 'crypto';
20
+ import { fileURLToPath } from 'url';
20
21
  import { runHookChecker, scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, writeDLPBlockManifest, allowLastDLPBlock } from '@rigour-labs/core';
22
+ function getHookCliVersion() {
23
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
24
+ const packagePath = path.resolve(thisDir, '../../package.json');
25
+ const pkg = fs.readJsonSync(packagePath);
26
+ const version = pkg.version?.trim();
27
+ if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
28
+ throw new Error('Unable to resolve the installed Rigour CLI version');
29
+ }
30
+ return version;
31
+ }
21
32
  // ── Studio event logging ─────────────────────────────────────────────
22
33
  const MAX_EVENT_LOG_LINES = 2000;
23
34
  async function logStudioEvent(cwd, event) {
@@ -74,20 +85,11 @@ function detectTools(cwd) {
74
85
  }
75
86
  return detected;
76
87
  }
77
- function resolveCheckerCommand(cwd) {
78
- // 1. Try project-local node_modules (installed as dependency)
79
- const localPath = path.join(cwd, 'node_modules', '@rigour-labs', 'core', 'dist', 'hooks', 'standalone-checker.js');
80
- if (fs.existsSync(localPath)) {
81
- return { command: 'node', args: [localPath] };
82
- }
83
- // 2. Try dev checkout: ESM has no __dirname, derive from import.meta.url
84
- const thisDir = path.dirname(new URL(import.meta.url).pathname);
85
- const localCli = path.resolve(thisDir, '../cli.js');
86
- if (fs.existsSync(localCli)) {
87
- return { command: 'node', args: [localCli, 'hooks', 'check'] };
88
- }
89
- // 3. Fallback: assume globally installed or aliased
90
- return { command: 'npx', args: ['@rigour-labs/cli', 'hooks', 'check'] };
88
+ function resolveCheckerCommand() {
89
+ return {
90
+ command: 'npx',
91
+ args: ['--yes', `@rigour-labs/cli@${getHookCliVersion()}`, 'hooks', 'check'],
92
+ };
91
93
  }
92
94
  function shellEscape(arg) {
93
95
  if (/^[A-Za-z0-9_/@%+=:,.-]+$/.test(arg)) {
@@ -136,7 +138,7 @@ function generateClaudeHooks(checker, block, dlp = true) {
136
138
  }]
137
139
  }],
138
140
  };
139
- // DLP: Add PreToolUse hook for credential interception
141
+ // DLP: Add PreToolUse hook for credential warnings
140
142
  if (dlp) {
141
143
  hooks.PreToolUse = [{
142
144
  matcher: ".*",
@@ -151,7 +153,7 @@ function generateClaudeHooks(checker, block, dlp = true) {
151
153
  path: '.claude/settings.json',
152
154
  content: JSON.stringify(settings, null, 4),
153
155
  description: dlp
154
- ? 'Claude Code hooks — PostToolUse quality checks + PreToolUse DLP credential interception'
156
+ ? 'Claude Code hooks — PostToolUse quality checks + PreToolUse DLP credential warnings'
155
157
  : 'Claude Code PostToolUse hook',
156
158
  }];
157
159
  }
@@ -169,7 +171,7 @@ function generateCursorHooks(checker, block, dlp = true) {
169
171
  path: '.cursor/hooks.json',
170
172
  content: JSON.stringify(hooks, null, 4),
171
173
  description: dlp
172
- ? 'Cursor hooks — afterFileEdit quality checks + beforeSubmitPrompt DLP credential interception'
174
+ ? 'Cursor hooks — afterFileEdit quality checks + beforeSubmitPrompt DLP warnings'
173
175
  : 'Cursor afterFileEdit hook config',
174
176
  }];
175
177
  }
@@ -185,7 +187,7 @@ function generateClineHooks(checker, block, dlp = true) {
185
187
  path: '.clinerules/hooks/PreToolUse',
186
188
  content: buildClineDLPScript(checker),
187
189
  executable: true,
188
- description: 'Cline PreToolUse DLP hook — credential interception before agent execution',
190
+ description: 'Cline PreToolUse DLP hook — credential warnings before agent execution',
189
191
  });
190
192
  }
191
193
  return files;
@@ -251,7 +253,7 @@ function buildClineDLPScript(checker) {
251
253
  return `#!/usr/bin/env node
252
254
  /**
253
255
  * Cline PreToolUse DLP hook for Rigour.
254
- * Scans tool input for credentials BEFORE agent execution.
256
+ * Warns about possible credentials before agent execution.
255
257
  */
256
258
  let data = '';
257
259
  process.stdin.on('data', chunk => { data += chunk; });
@@ -288,14 +290,15 @@ process.stdin.on('end', async () => {
288
290
  return;
289
291
  }
290
292
  const result = JSON.parse(raw);
291
- if (result.status === 'blocked') {
293
+ if (result.status !== 'clean') {
292
294
  const msgs = result.detections
293
295
  .map(d => \`[rigour/dlp/\${d.type}] \${d.description} → \${d.recommendation}\`)
294
296
  .join('\\n');
297
+ const label = result.status === 'blocked' ? 'BLOCKED' : 'warning';
295
298
  process.stdout.write(JSON.stringify({
296
- contextModification: \`\\n🛑 [Rigour DLP] \${result.detections.length} credential(s) BLOCKED:\\n\${msgs}\\nReplace with environment variable references.\`,
299
+ contextModification: \`\\n⚠️ [Rigour DLP] \${result.detections.length} possible credential(s) \${label}:\\n\${msgs}\`,
297
300
  }));
298
- process.exit(2);
301
+ if (result.status === 'blocked') process.exit(2);
299
302
  } else {
300
303
  process.stdout.write(JSON.stringify({}));
301
304
  }
@@ -320,7 +323,7 @@ function generateWindsurfHooks(checker, block, dlp = true) {
320
323
  path: '.windsurf/hooks.json',
321
324
  content: JSON.stringify(hooks, null, 4),
322
325
  description: dlp
323
- ? 'Windsurf hooks — post_write_code quality checks + pre_write_code DLP credential interception'
326
+ ? 'Windsurf hooks — post_write_code quality checks + pre_write_code DLP warnings'
324
327
  : 'Windsurf post_write_code hook config',
325
328
  }];
326
329
  }
@@ -345,6 +348,7 @@ function printDryRun(files) {
345
348
  async function writeHookFiles(cwd, files, force) {
346
349
  let written = 0;
347
350
  let skipped = 0;
351
+ const failedPaths = new Set();
348
352
  for (const file of files) {
349
353
  const fullPath = path.join(cwd, file.path);
350
354
  const exists = await fs.pathExists(fullPath);
@@ -353,16 +357,28 @@ async function writeHookFiles(cwd, files, force) {
353
357
  skipped++;
354
358
  continue;
355
359
  }
356
- await fs.ensureDir(path.dirname(fullPath));
357
- await fs.writeFile(fullPath, file.content, 'utf-8');
358
- if (file.executable) {
359
- await fs.chmod(fullPath, 0o755);
360
- }
361
- console.log(chalk.green(` CREATE ${file.path}`));
362
- console.log(chalk.dim(` ${file.description}`));
363
- written++;
364
- }
365
- return { written, skipped };
360
+ try {
361
+ await fs.ensureDir(path.dirname(fullPath));
362
+ await fs.writeFile(fullPath, file.content, 'utf-8');
363
+ if (file.executable) {
364
+ await fs.chmod(fullPath, 0o755);
365
+ }
366
+ console.log(chalk.green(` CREATE ${file.path}`));
367
+ console.log(chalk.dim(` ${file.description}`));
368
+ written++;
369
+ }
370
+ catch (error) {
371
+ const code = error instanceof Error && 'code' in error
372
+ ? String(error.code)
373
+ : 'UNKNOWN';
374
+ const reason = code === 'ENOTDIR'
375
+ ? 'a parent path is a file; keep the existing config and configure this tool manually'
376
+ : error instanceof Error ? error.message : String(error);
377
+ console.error(chalk.yellow(` SKIP ${file.path} (${reason})`));
378
+ failedPaths.add(file.path);
379
+ }
380
+ }
381
+ return { written, skipped, failedPaths };
366
382
  }
367
383
  // ── Next-steps guidance ──────────────────────────────────────────────
368
384
  const NEXT_STEPS = {
@@ -371,10 +387,15 @@ const NEXT_STEPS = {
371
387
  cline: 'Cline: Hook is active. Quality feedback appears in agent context on violations.',
372
388
  windsurf: 'Windsurf: Reload editor. Check terminal for Rigour output after Cascade writes.',
373
389
  };
374
- function printNextSteps(tools) {
390
+ function printNextSteps(tools, unavailableTools) {
375
391
  console.log(chalk.cyan('\nNext steps:'));
376
392
  for (const tool of tools) {
377
- console.log(chalk.dim(` ${NEXT_STEPS[tool]}`));
393
+ if (unavailableTools.has(tool)) {
394
+ console.log(chalk.yellow(` ${tool[0].toUpperCase() + tool.slice(1)}: Not configured; resolve the path conflict or configure manually.`));
395
+ }
396
+ else {
397
+ console.log(chalk.dim(` ${NEXT_STEPS[tool]}`));
398
+ }
378
399
  }
379
400
  console.log('');
380
401
  }
@@ -387,7 +408,7 @@ export async function hooksInitCommand(cwd, options = {}) {
387
408
  arguments: { tool: options.tool, dryRun: options.dryRun, dlp: options.dlp },
388
409
  });
389
410
  const tools = resolveTools(cwd, options.tool);
390
- const checker = resolveCheckerCommand(cwd);
411
+ const checker = resolveCheckerCommand();
391
412
  const block = !!options.block;
392
413
  // DLP is ON by default — user must explicitly pass --no-dlp to disable
393
414
  const dlp = options.dlp !== false;
@@ -400,7 +421,12 @@ export async function hooksInitCommand(cwd, options = {}) {
400
421
  printDryRun(allFiles);
401
422
  return;
402
423
  }
403
- const { written, skipped } = await writeHookFiles(cwd, allFiles, !!options.force);
424
+ const { written, skipped, failedPaths } = await writeHookFiles(cwd, allFiles, !!options.force);
425
+ const failed = failedPaths.size;
426
+ const unavailableTools = new Set(tools.filter(tool => {
427
+ const generatedPaths = GENERATORS[tool](checker, block, dlp).map(file => file.path);
428
+ return generatedPaths.every(filePath => failedPaths.has(filePath));
429
+ }));
404
430
  console.log('');
405
431
  if (written > 0) {
406
432
  console.log(chalk.green.bold(`Created ${written} hook file(s).`));
@@ -408,17 +434,24 @@ export async function hooksInitCommand(cwd, options = {}) {
408
434
  if (skipped > 0) {
409
435
  console.log(chalk.yellow(`Skipped ${skipped} existing file(s).`));
410
436
  }
411
- printNextSteps(tools);
437
+ if (failed > 0) {
438
+ console.log(chalk.yellow(`Skipped ${failed} incompatible hook file(s); other tools were configured.`));
439
+ }
440
+ printNextSteps(tools, unavailableTools);
412
441
  if (dlp) {
413
- console.log(chalk.red.bold(' 🛑 DLP Protection ACTIVE'));
414
- console.log(chalk.dim(' Credentials will be intercepted BEFORE reaching AI agents.'));
442
+ console.log(chalk.yellow.bold(' DLP warnings ACTIVE'));
443
+ console.log(chalk.dim(' Possible credentials will be reported before agent actions.'));
444
+ console.log(chalk.dim(' Use --block only when every input path is covered by the same policy.'));
415
445
  console.log(chalk.dim(' Coverage: AWS keys, API tokens, database URLs, private keys, JWTs, passwords.\n'));
416
446
  }
417
447
  await logStudioEvent(cwd, {
418
448
  type: 'tool_response',
419
449
  tool: 'rigour_hooks_init',
420
- status: 'success',
421
- content: [{ type: 'text', text: `Generated hooks for: ${tools.join(', ')}${options.dlp ? ' (+ DLP)' : ''}` }],
450
+ status: failed > 0 ? 'partial' : 'success',
451
+ content: [{
452
+ type: 'text',
453
+ text: `Generated hooks for: ${tools.join(', ')}; ${written} written, ${skipped} existing, ${failed} incompatible`,
454
+ }],
422
455
  });
423
456
  }
424
457
  async function readStdin() {
@@ -533,19 +566,26 @@ export async function hooksCheckCommand(cwd, options = {}) {
533
566
  }
534
567
  const result = scanInputForCredentials(textToScan, {
535
568
  enabled: true,
536
- block_on_detection: options.block ?? true,
569
+ block_on_detection: options.block ?? false,
537
570
  cwd,
538
571
  use_learned_feedback: true,
539
572
  });
573
+ const messages = result.detections
574
+ .map((d) => `[${d.type}] ${d.description} → ${d.recommendation}`)
575
+ .join('\n');
576
+ const allowCommand = `npx --yes @rigour-labs/cli@${getHookCliVersion()} hooks check --dlp-allow-last`;
540
577
  // Return Cursor-compatible format if detected as Cursor hook
541
578
  if (cursorMode) {
542
579
  if (result.status === 'blocked') {
543
- const messages = result.detections
544
- .map((d) => `[${d.type}] ${d.description} → ${d.recommendation}`)
545
- .join('\n');
546
580
  process.stdout.write(JSON.stringify({
547
581
  continue: false,
548
- user_message: `🛑 Rigour DLP: ${result.detections.length} credential(s) detected in your prompt:\n${messages}\n\nReplace with environment variable references before submitting.\n\nIf this is a false positive, run: rigour hooks check --dlp-allow-last`,
582
+ user_message: `🛑 Rigour DLP: ${result.detections.length} credential(s) detected in your prompt:\n${messages}\n\nReplace with environment variable references before submitting.\n\nIf this is a false positive, run: ${allowCommand}`,
583
+ }));
584
+ }
585
+ else if (result.status === 'warning') {
586
+ process.stdout.write(JSON.stringify({
587
+ continue: true,
588
+ user_message: `⚠️ Rigour DLP warning: ${result.detections.length} possible credential(s) detected:\n${messages}`,
549
589
  }));
550
590
  }
551
591
  else {
@@ -567,13 +607,13 @@ export async function hooksCheckCommand(cwd, options = {}) {
567
607
  catch {
568
608
  // Silent
569
609
  }
610
+ try {
611
+ await writeDLPBlockManifest(cwd, result.detections, textToScan);
612
+ }
613
+ catch {
614
+ // best-effort
615
+ }
570
616
  if (result.status === 'blocked') {
571
- try {
572
- await writeDLPBlockManifest(cwd, result.detections, textToScan);
573
- }
574
- catch {
575
- // best-effort
576
- }
577
617
  process.exitCode = 2;
578
618
  }
579
619
  }
@@ -44,6 +44,17 @@ describe('hooksInitCommand', () => {
44
44
  const hookPath = path.join(testDir, '.clinerules', 'hooks', 'PostToolUse');
45
45
  expect(fs.existsSync(hookPath)).toBe(true);
46
46
  });
47
+ it('should skip a legacy .clinerules file and continue other tools', async () => {
48
+ fs.writeFileSync(path.join(testDir, '.clinerules'), 'legacy Cline rules');
49
+ await expect(hooksInitCommand(testDir, {
50
+ tool: 'cline,windsurf',
51
+ })).resolves.toBeUndefined();
52
+ expect(fs.readFileSync(path.join(testDir, '.clinerules'), 'utf-8'))
53
+ .toBe('legacy Cline rules');
54
+ expect(fs.existsSync(path.join(testDir, '.windsurf', 'hooks.json'))).toBe(true);
55
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('SKIP .clinerules/hooks/PostToolUse'));
56
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Cline: Not configured'));
57
+ });
47
58
  it('should generate Windsurf hooks', async () => {
48
59
  await hooksInitCommand(testDir, { tool: 'windsurf' });
49
60
  const hooksPath = path.join(testDir, '.windsurf', 'hooks.json');
@@ -109,6 +120,8 @@ describe('hooksInitCommand — DLP integration', () => {
109
120
  expect(settings.hooks.PostToolUse).toBeDefined();
110
121
  expect(settings.hooks.PreToolUse).toBeDefined();
111
122
  expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain('--mode dlp');
123
+ expect(settings.hooks.PreToolUse[0].hooks[0].command)
124
+ .toMatch(/npx --yes @rigour-labs\/cli@\d+\.\d+\.\d+/);
112
125
  });
113
126
  it('should generate Cursor hooks with DLP (beforeFileEdit) by default', async () => {
114
127
  await hooksInitCommand(testDir, { tool: 'cursor', force: true });
@@ -131,6 +144,22 @@ describe('hooksInitCommand — DLP integration', () => {
131
144
  expect(settings.hooks.PostToolUse).toBeDefined();
132
145
  expect(settings.hooks.PreToolUse).toBeUndefined();
133
146
  });
147
+ it('should generate Cline DLP warnings without default blocking', async () => {
148
+ await hooksInitCommand(testDir, { tool: 'cline', force: true });
149
+ const script = fs.readFileSync(path.join(testDir, '.clinerules', 'hooks', 'PreToolUse'), 'utf-8');
150
+ expect(script).toContain("result.status !== 'clean'");
151
+ expect(script).toContain("if (result.status === 'blocked') process.exit(2)");
152
+ });
153
+ it('should not route DLP hooks through the file-only core checker', async () => {
154
+ const coreChecker = path.join(testDir, 'node_modules/@rigour-labs/core/dist/hooks/standalone-checker.js');
155
+ fs.mkdirSync(path.dirname(coreChecker), { recursive: true });
156
+ fs.writeFileSync(coreChecker, '');
157
+ await hooksInitCommand(testDir, { tool: 'cursor', force: true });
158
+ const hooks = JSON.parse(fs.readFileSync(path.join(testDir, '.cursor', 'hooks.json'), 'utf-8'));
159
+ const command = hooks.hooks.beforeSubmitPrompt[0].command;
160
+ expect(command).toMatch(/npx --yes @rigour-labs\/cli@\d+\.\d+\.\d+/);
161
+ expect(command).not.toContain('standalone-checker');
162
+ });
134
163
  });
135
164
  describe('hooksCheckCommand', () => {
136
165
  let testDir;
@@ -164,4 +193,32 @@ describe('hooksCheckCommand', () => {
164
193
  expect(process.exitCode).toBe(2);
165
194
  process.exitCode = originalExitCode;
166
195
  });
196
+ it('should warn and continue for DLP detections by default', async () => {
197
+ const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
198
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
199
+ const originalExitCode = process.exitCode;
200
+ await hooksCheckCommand(testDir, {
201
+ mode: 'dlp',
202
+ files: 'AKIAZ9Y8X7W6V5U4T3Q2',
203
+ });
204
+ const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
205
+ expect(output).toContain('"status":"warning"');
206
+ expect(output).not.toContain('AKIAZ9Y8X7W6V5U4T3Q2');
207
+ expect(stderrSpy).toHaveBeenCalled();
208
+ expect(process.exitCode).toBe(originalExitCode);
209
+ });
210
+ it('should block DLP detections only with --block', async () => {
211
+ const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
212
+ vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
213
+ const originalExitCode = process.exitCode;
214
+ await hooksCheckCommand(testDir, {
215
+ mode: 'dlp',
216
+ files: 'AKIAZ9Y8X7W6V5U4T3Q2',
217
+ block: true,
218
+ });
219
+ const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
220
+ expect(output).toContain('"status":"blocked"');
221
+ expect(process.exitCode).toBe(2);
222
+ process.exitCode = originalExitCode;
223
+ });
167
224
  });
@@ -472,6 +472,12 @@ const IDE_TO_HOOK_TOOL = {
472
472
  cline: 'cline',
473
473
  windsurf: 'windsurf',
474
474
  };
475
+ const PRIMARY_HOOK_PATH = {
476
+ claude: '.claude/settings.json',
477
+ cursor: '.cursor/hooks.json',
478
+ cline: '.clinerules/hooks/PostToolUse',
479
+ windsurf: '.windsurf/hooks.json',
480
+ };
475
481
  /**
476
482
  * Build the pattern index so rigour_check_pattern can detect duplicates.
477
483
  * Uses semantic embeddings by default for fuzzy matching.
@@ -511,14 +517,16 @@ async function initHooksForAllDetectedTools(cwd, detectedIDEs) {
511
517
  try {
512
518
  console.log(chalk.dim(`\n Setting up real-time hooks for ${ide}...`));
513
519
  await hooksInitCommand(cwd, { tool: hookTool, dlp: true, force: true, block: true });
514
- enabledTools.push(hookTool);
520
+ if (await fs.pathExists(path.join(cwd, PRIMARY_HOOK_PATH[hookTool]))) {
521
+ enabledTools.push(hookTool);
522
+ }
515
523
  }
516
524
  catch (err) {
517
525
  console.log(chalk.dim(` (Hooks setup for ${ide} failed: ${err?.message || err})`));
518
526
  }
519
527
  }
520
528
  if (enabledTools.length > 0) {
521
- console.log(chalk.dim(` 🛑 DLP protection active for: ${enabledTools.join(', ')}`));
529
+ console.log(chalk.dim(` DLP warnings active for: ${enabledTools.join(', ')}`));
522
530
  }
523
531
  return enabledTools;
524
532
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "5.5.0",
3
+ "version": "5.5.1",
4
4
  "description": "AI-native quality gates with local LLM analysis. Forces AI agents (Claude, Cursor, Copilot, Cline, Windsurf) to meet engineering standards. Bayesian Brain learns your codebase. Zero config: npx rigour-scan.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -53,7 +53,7 @@
53
53
  "inquirer": "9.2.16",
54
54
  "ora": "^8.0.1",
55
55
  "yaml": "^2.8.2",
56
- "@rigour-labs/core": "5.5.0"
56
+ "@rigour-labs/core": "5.5.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/fs-extra": "^11.0.4",