@privacyscrubber/mcp-server 2.4.4 → 2.4.6

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/install.js CHANGED
@@ -234,6 +234,8 @@ export function parseArgs(rawArgs = process.argv.slice(2)) {
234
234
  dryRun: false,
235
235
  remove: false,
236
236
  all: false,
237
+ rules: false,
238
+ hooks: false,
237
239
  targets: [],
238
240
  help: false
239
241
  };
@@ -242,6 +244,8 @@ export function parseArgs(rawArgs = process.argv.slice(2)) {
242
244
  if (arg === '--dry-run') options.dryRun = true;
243
245
  else if (arg === '--remove' || arg === '--uninstall') options.remove = true;
244
246
  else if (arg === '--all') options.all = true;
247
+ else if (arg === '--rules') options.rules = true;
248
+ else if (arg === '--hooks') options.hooks = true;
245
249
  else if (arg === '--claude') options.targets.push('claude');
246
250
  else if (arg === '--claude-code') options.targets.push('claude-code', 'claude-code-project');
247
251
  else if (arg === '--cursor') options.targets.push('cursor', 'cursor-storage', 'cursor-workspace');
@@ -249,7 +253,11 @@ export function parseArgs(rawArgs = process.argv.slice(2)) {
249
253
  else if (arg === '--zed') options.targets.push('zed');
250
254
  else if (arg === '--windsurf') options.targets.push('windsurf');
251
255
  else if (arg === '--cline') options.targets.push('cline', 'roo');
252
- else if (arg === '--workspace') options.targets.push('cursor-workspace', 'claude-code-project', 'vscode');
256
+ else if (arg === '--workspace') {
257
+ options.targets.push('cursor-workspace', 'claude-code-project', 'vscode');
258
+ options.rules = true;
259
+ options.hooks = true;
260
+ }
253
261
  else if (arg === '--help' || arg === '-h') options.help = true;
254
262
  }
255
263
 
@@ -327,7 +335,8 @@ Options:
327
335
  --zed Configure Zed IDE (~/.config/zed/settings.json)
328
336
  --windsurf Configure Windsurf only
329
337
  --cline Configure Cline / Roo-Code only
330
- --workspace Configure workspace paths (.cursor, .mcp.json, .vscode)
338
+ --workspace Configure workspace paths (.cursor, .mcp.json, .vscode) & agent rules
339
+ --rules Scaffold native AI agent rules (.cursor/rules/privacyscrubber.mdc, AGENTS.md)
331
340
  --dry-run Preview configuration changes without writing files
332
341
  --remove Remove PrivacyScrubber from all target client configurations
333
342
  -h, --help Display this help guide
@@ -400,6 +409,88 @@ Options:
400
409
  console.log('');
401
410
  }
402
411
 
412
+ if (options.rules && !options.remove) {
413
+ const cwd = process.cwd();
414
+ const mdcPath = path.join(cwd, '.cursor', 'rules', 'privacyscrubber.mdc');
415
+ const agentsMdPath = path.join(cwd, 'AGENTS.md');
416
+ const ruleBody = [
417
+ '# PrivacyScrubber Zero-Trust Agentic Guard (ZTDS)',
418
+ '1. Never read `.env`, `*secret*`, `*credentials*`, `*.pem`, `*.key`, or database dumps with native `read_file` / `cat`. Call `guard_read_file` or `sanitize_file`.',
419
+ '2. Route shell commands that may print credentials or PII through `guard_exec`, and inspect git diffs via `guard_git_diff`.',
420
+ '3. Keep tokens like `[API_KEY_1]`, `[EMAIL_1]` intact during reasoning. Restore final answers via `reveal_text` or write to disk via `guard_apply_patch`.'
421
+ ].join('\n');
422
+ const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) — Mandatory PII & Secret Guard\nglobs: "**/*"\nalwaysApply: true\n---\n\n${ruleBody}\n`;
423
+
424
+ if (options.dryRun) {
425
+ console.log(`[DRY-RUN OK]: Would scaffold agent rules in ${mdcPath} and ${agentsMdPath}\n`);
426
+ } else {
427
+ try {
428
+ fs.mkdirSync(path.dirname(mdcPath), { recursive: true });
429
+ if (!fs.existsSync(mdcPath) || !fs.readFileSync(mdcPath, 'utf8').includes('PrivacyScrubber Zero-Trust Agentic Guard')) {
430
+ fs.writeFileSync(mdcPath, mdcContent, 'utf8');
431
+ console.log(`[RULES] Created ${mdcPath}`);
432
+ }
433
+ if (!fs.existsSync(agentsMdPath)) {
434
+ fs.writeFileSync(agentsMdPath, ruleBody + '\n', 'utf8');
435
+ console.log(`[RULES] Created ${agentsMdPath}`);
436
+ } else {
437
+ const existingAgents = fs.readFileSync(agentsMdPath, 'utf8');
438
+ if (!existingAgents.includes('PrivacyScrubber Zero-Trust Agentic Guard')) {
439
+ fs.writeFileSync(agentsMdPath, existingAgents.trimEnd() + '\n\n' + ruleBody + '\n', 'utf8');
440
+ console.log(`[RULES] Updated ${agentsMdPath}`);
441
+ }
442
+ }
443
+ } catch (_) {}
444
+ }
445
+ }
446
+
447
+ if (options.hooks && !options.remove) {
448
+ const cwd = process.cwd();
449
+ const cursorHooksPath = path.join(cwd, '.cursor', 'hooks.json');
450
+ const claudeSettingsPath = path.join(cwd, '.claude', 'settings.json');
451
+ const hookCmd = 'npx -y @privacyscrubber/mcp-server --hook';
452
+
453
+ if (options.dryRun) {
454
+ console.log(`[DRY-RUN OK]: Would scaffold IDE lifecycle hooks in ${cursorHooksPath} and ${claudeSettingsPath}\n`);
455
+ } else {
456
+ try {
457
+ fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
458
+ let cursorHooks = { version: 1, hooks: {} };
459
+ if (fs.existsSync(cursorHooksPath)) {
460
+ try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
461
+ }
462
+ if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
463
+ for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
464
+ if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
465
+ if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
466
+ cursorHooks.hooks[ev].push({ command: hookCmd });
467
+ }
468
+ }
469
+ fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
470
+ console.log(`[HOOKS] Configured ${cursorHooksPath}`);
471
+
472
+ fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
473
+ let claudeSettings = {};
474
+ if (fs.existsSync(claudeSettingsPath)) {
475
+ try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
476
+ }
477
+ if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
478
+ if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
479
+ const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
480
+ Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
481
+ );
482
+ if (!hasClaudeHook) {
483
+ claudeSettings.hooks.PreToolUse.push({
484
+ matcher: 'Read|Bash',
485
+ hooks: [{ type: 'command', command: hookCmd }]
486
+ });
487
+ }
488
+ fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
489
+ console.log(`[HOOKS] Configured ${claudeSettingsPath}`);
490
+ } catch (_) {}
491
+ }
492
+ }
493
+
403
494
  console.log('--------------------------------------------------');
404
495
  if (configuredCount > 0) {
405
496
  console.log(`[SUMMARY] ${options.remove ? 'Cleaned' : 'Configured'} ${configuredCount} AI client(s) successfully.`);
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "privacyscrubber/pii-masking-mcp",
4
- "version": "2.4.4",
4
+ "version": "2.4.6",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "author": {
7
7
  "name": "PrivacyScrubber",
@@ -30,21 +30,17 @@
30
30
  "description": "STEP 3: Call this last. Restores masked tokens back with original private data from the local volatile RAM-only session map."
31
31
  },
32
32
  {
33
- "name": "audit_context",
34
- "description": "STEP 0 (Pre-Flight): Non-destructive security inspection of raw prompts or document chunks before sending to LLMs. Evaluates PII, secrets, risk severity, and triggered regulatory frameworks."
33
+ "name": "stream_reveal_text",
34
+ "description": "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines."
35
35
  },
36
36
  {
37
- "name": "scrub_text",
38
- "description": "Alias for sanitize_text."
37
+ "name": "audit_context",
38
+ "description": "STEP 0 (Pre-Flight): Non-destructive security inspection of raw prompts or document chunks before sending to LLMs. Evaluates PII, secrets, risk severity, and triggered regulatory frameworks."
39
39
  },
40
40
  {
41
41
  "name": "sanitize_file",
42
42
  "description": "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory."
43
43
  },
44
- {
45
- "name": "scrub_file",
46
- "description": "Alias for sanitize_file."
47
- },
48
44
  {
49
45
  "name": "audit_directory_for_pii",
50
46
  "description": "Scans a local directory for leaks of secrets, keys, and PII. Returns a structured security summary report."
@@ -116,7 +112,9 @@
116
112
  {
117
113
  "name": "generate_procurement_memo",
118
114
  "description": "Zero-Trust Enterprise Procurement: Generates an executive-ready Procurement & ROI Memorandum for CISOs, DPOs, and VP of Engineering."
119
- },
115
+ }
116
+ ],
117
+ "prompts": [
120
118
  {
121
119
  "name": "secure_ai_workflow",
122
120
  "description": "Zero-Trust Data Sanitization workflow instruction to automatically scrub PII before processing data."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/mcp-server",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "mcpName": "io.github.moxno/privacyscrubber-mcp",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "main": "index.js",
@@ -13,10 +13,8 @@
13
13
  "install.js",
14
14
  "pii-masking-run.js",
15
15
  "scrubber-core.cjs",
16
- "ps-license-manager.js",
17
16
  "ps-license-manager.cjs",
18
17
  "ps-pii-engine.cjs",
19
- "ps-pii-engine.js",
20
18
  "config-schema.json",
21
19
  "smithery.yaml",
22
20
  "server.json",
@@ -27,7 +27,7 @@ const cmdIndex = args.indexOf("--");
27
27
 
28
28
  if (args.includes("--version") || args.includes("-v")) {
29
29
  const pkgPath = path.resolve(__dirname, './package.json');
30
- let version = '2.4.4';
30
+ let version = '2.4.6';
31
31
  try { version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch (_) {}
32
32
  console.log(`ps-guard / pii-masking-run v${version} (PrivacyScrubber MCP)`);
33
33
  process.exit(0);
@@ -35,6 +35,7 @@ if (args.includes("--version") || args.includes("-v")) {
35
35
 
36
36
  const isDiffMode = args.includes("--diff");
37
37
  const isRulesMode = args.includes("--rules");
38
+ const isHookMode = args.includes("--hook");
38
39
  const isJsonMode = args.includes("--json");
39
40
  const isRestoreMode = args.includes("--restore") || args.includes("--unscrub");
40
41
  let tokenMapInput = null;
@@ -42,10 +43,15 @@ const tokenMapIdx = args.indexOf("--token-map");
42
43
  if (tokenMapIdx !== -1 && args[tokenMapIdx + 1]) {
43
44
  tokenMapInput = args[tokenMapIdx + 1];
44
45
  }
46
+ let hookPayloadArg = null;
47
+ const payloadIdx = args.indexOf("--payload");
48
+ if (payloadIdx !== -1 && args[payloadIdx + 1]) {
49
+ hookPayloadArg = args[payloadIdx + 1];
50
+ }
45
51
  const hasCommand = cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0;
46
52
  const hasStdin = !process.stdin.isTTY;
47
53
 
48
- if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isRestoreMode && !hasStdin)) {
54
+ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isHookMode && !isRestoreMode && !hasStdin)) {
49
55
  console.log("PrivacyScrubber CLI & Agentic Guard: ps-guard / pii-masking-run");
50
56
  console.log("\nUsage:");
51
57
  console.log(" npx ps-guard [options] -- <command> [args...]");
@@ -53,6 +59,7 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
53
59
  console.log(" npx ps-guard --diff [--staged] [options]");
54
60
  console.log(" npx ps-guard --restore --token-map <file|json> [options]");
55
61
  console.log(" npx ps-guard --rules [targets]");
62
+ console.log(" npx ps-guard --hook (Claude Code & Cursor IDE PreToolUse / beforeReadFile hook)");
56
63
  console.log("\nOptions:");
57
64
  console.log(" --profile <name> Specify detection profile (e.g. General, Dev, Medical, Legal, HR)");
58
65
  console.log(" --key <licKey> PrivacyScrubber PRO License Key");
@@ -62,6 +69,7 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
62
69
  console.log(" --diff Sanitize git diff output before committing or sending to AI");
63
70
  console.log(" --staged Use staged git changes with --diff");
64
71
  console.log(" --rules [targets] Generate AI agent rules (.cursorrules, .windsurfrules, CLAUDE.md, copilot, cline)");
72
+ console.log(" --hook Evaluate IDE lifecycle hook JSON from stdin (Claude Code & Cursor)");
65
73
  console.log(" -q, --quiet Suppress stderr proof telemetry");
66
74
  console.log(" -v, --version Print version");
67
75
  console.log(" -h, --help Show this help screen");
@@ -75,6 +83,98 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
75
83
  process.exit(0);
76
84
  }
77
85
 
86
+ // Fast-path Mode 0: Deterministic IDE Lifecycle Hook (--hook) for Claude Code & Cursor v1.7+
87
+ if (isHookMode) {
88
+ const SENSITIVE_FILE_RX = /(?:^|[\\/])(?:\.env(?:\.[a-zA-Z0-9_-]+)?|id_rsa|id_ed25519|id_ecdsa|.*\.pem|.*\.key|.*\.p12|.*\.pfx|.*credentials.*\.json|.*secret.*\.(?:json|ya?ml|env|txt)|\.npmrc|\.pypirc|\.netrc)$/i;
89
+ const SAFE_TEMPLATE_RX = /\.(?:example|sample|template|dist|schema)$/i;
90
+ const SENSITIVE_CMD_RX = /(?:^|[;&|]\s*)(?:cat|head|tail|less|more|bat|sed|awk|grep)\s+[^\n|;&]*(?:\.env\b|id_rsa|\.pem\b|\.key\b|credentials\.json|\bsecrets?\.)|(?:^|[;&|]\s*)(?:printenv|env\s*$|export\s+-p|aws\s+configure\s+get|gcloud\s+auth\s+print-access-token)/i;
91
+
92
+ const evaluateHookPayload = (rawStr) => {
93
+ let payload = {};
94
+ try {
95
+ payload = rawStr && rawStr.trim() ? JSON.parse(rawStr) : {};
96
+ } catch (_) {
97
+ process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
98
+ process.exit(0);
99
+ }
100
+
101
+ const eventName = String(payload.hook_event_name || payload.hookEventName || payload.event || "");
102
+ const toolName = String(payload.tool_name || payload.toolName || "");
103
+ const toolInput = (payload.tool_input || payload.toolInput || payload) || {};
104
+
105
+ const candidatePath = String(
106
+ toolInput.file_path || toolInput.path || toolInput.target_file || payload.file_path || payload.path || ""
107
+ ).trim();
108
+ const candidateCmd = String(
109
+ toolInput.command || toolInput.cmd || payload.command || ""
110
+ ).trim();
111
+
112
+ const isCursorEvent = /^(?:beforeReadFile|beforeShellExecution|beforeSubmitPrompt|beforeMCPExecution)$/i.test(eventName) ||
113
+ (!toolName && (payload.file_path !== undefined || payload.command !== undefined || payload.attachments !== undefined));
114
+
115
+ const isReadTool = /^(?:Read|View|read_file|view_file|ReadFile)$/i.test(toolName) || /^beforeReadFile$/i.test(eventName) || (!toolName && Boolean(candidatePath));
116
+ const isShellTool = /^(?:Bash|Execute|run_command| run_terminal_cmd)$/i.test(toolName) || /^beforeShellExecution$/i.test(eventName) || (!toolName && Boolean(candidateCmd));
117
+
118
+ if (isReadTool && candidatePath && SENSITIVE_FILE_RX.test(candidatePath) && !SAFE_TEMPLATE_RX.test(candidatePath)) {
119
+ const reason = `Blocked by PrivacyScrubber ZTDS policy: Direct read of sensitive file '${path.basename(candidatePath)}' is prohibited. Call the PrivacyScrubber MCP tool 'guard_read_file' or 'sanitize_file' instead so secrets are tokenized in local RAM.`;
120
+ if (isCursorEvent) {
121
+ process.stdout.write(JSON.stringify({
122
+ permission: "deny",
123
+ userMessage: `[PrivacyScrubber ZTDS] Direct read of ${path.basename(candidatePath)} blocked to prevent cloud egress.`,
124
+ agentMessage: reason
125
+ }) + "\n");
126
+ process.exit(0);
127
+ }
128
+ process.stdout.write(JSON.stringify({
129
+ permission: "deny",
130
+ hookSpecificOutput: {
131
+ hookEventName: eventName || "PreToolUse",
132
+ permissionDecision: "deny",
133
+ permissionDecisionReason: reason
134
+ }
135
+ }) + "\n");
136
+ process.stderr.write(reason + "\n");
137
+ process.exit(2);
138
+ }
139
+
140
+ if (isShellTool && candidateCmd && SENSITIVE_CMD_RX.test(candidateCmd)) {
141
+ const reason = `Blocked by PrivacyScrubber ZTDS policy: Shell command exposing raw credentials ('${candidateCmd.slice(0, 60)}') is prohibited. Call the PrivacyScrubber MCP tool 'guard_exec' instead so stdout/stderr are sanitized in local RAM.`;
142
+ if (isCursorEvent) {
143
+ process.stdout.write(JSON.stringify({
144
+ permission: "deny",
145
+ userMessage: `[PrivacyScrubber ZTDS] Shell command exposing raw credentials blocked.`,
146
+ agentMessage: reason
147
+ }) + "\n");
148
+ process.exit(0);
149
+ }
150
+ process.stdout.write(JSON.stringify({
151
+ permission: "deny",
152
+ hookSpecificOutput: {
153
+ hookEventName: eventName || "PreToolUse",
154
+ permissionDecision: "deny",
155
+ permissionDecisionReason: reason
156
+ }
157
+ }) + "\n");
158
+ process.stderr.write(reason + "\n");
159
+ process.exit(2);
160
+ }
161
+
162
+ process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
163
+ process.exit(0);
164
+ };
165
+
166
+ if (hookPayloadArg !== null) {
167
+ evaluateHookPayload(hookPayloadArg);
168
+ } else if (!process.stdin.isTTY) {
169
+ let buf = "";
170
+ process.stdin.setEncoding("utf8");
171
+ process.stdin.on("data", (c) => { buf += c; });
172
+ process.stdin.on("end", () => evaluateHookPayload(buf));
173
+ } else {
174
+ evaluateHookPayload("{}");
175
+ }
176
+ }
177
+
78
178
  // Import the production core engine
79
179
  const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
80
180
  const PrivacyScrubberCore = require(scrubberCorePath);
@@ -154,6 +254,13 @@ function resolveConfigPath() {
154
254
  return configPath;
155
255
  }
156
256
 
257
+ function isCatastrophicRegex(pattern) {
258
+ if (!pattern || typeof pattern !== 'string' || pattern.length > 500) return true;
259
+ if (/(\([^)]*[+*][^)]*\))[+*?{]/.test(pattern)) return true;
260
+ if (/(\.\*){3,}/.test(pattern)) return true;
261
+ return false;
262
+ }
263
+
157
264
  function loadCustomRules() {
158
265
  const license = checkLicenseStatus();
159
266
  const configPath = resolveConfigPath();
@@ -164,12 +271,20 @@ function loadCustomRules() {
164
271
  const rules = config.customRules || [];
165
272
  if (rules.length > 0) {
166
273
  if (license.isPro) {
167
- return rules.map(r => ({
168
- pattern: r.pattern || r.regex || "",
169
- label: (r.label || r.category || "CUSTOM").toUpperCase(),
170
- regex: r.pattern || r.regex || "",
171
- category: (r.label || r.category || "CUSTOM").toUpperCase()
172
- })).filter(r => r.pattern);
274
+ return rules.map(r => {
275
+ const pat = r.pattern || r.regex || "";
276
+ if (!pat || isCatastrophicRegex(pat)) return null;
277
+ try {
278
+ return {
279
+ pattern: pat,
280
+ label: (r.label || r.category || "CUSTOM").toUpperCase(),
281
+ regex: new RegExp(pat, "gi"),
282
+ category: (r.label || r.category || "CUSTOM").toUpperCase()
283
+ };
284
+ } catch (_) {
285
+ return null;
286
+ }
287
+ }).filter(Boolean);
173
288
  } else {
174
289
  process.stderr.write(`⚠️ [PrivacyScrubber] Custom rules detected in privacyscrubber.json, but are ignored in the Free Tier.\n👉 Set PRIVACYSCRUBBER_KEY to your PRO license key.\n`);
175
290
  }
@@ -246,34 +361,6 @@ function performSanitization(text) {
246
361
  if (result && result.tokenMap) {
247
362
  Object.assign(localSessionMap, result.tokenMap);
248
363
  }
249
-
250
- if (!license.isPro && result.tokenMap) {
251
- // Free tier: check for secrets and warn
252
- let hasSecrets = false;
253
- const SECRET_TOKEN_RE = /^\[(?:SECRET|API_TOKEN|AWS_KEY|JWT_TOKEN|STRIPE_KEY|OPENAI_KEY|DB_URI)_\d+\]$/;
254
- Object.entries(result.tokenMap).forEach(([token, original]) => {
255
- let isSecret = SECRET_TOKEN_RE.test(token);
256
- if (!isSecret) {
257
- for (const detector of DEVOPS_SECRETS_DETECTOR) {
258
- detector.regex.lastIndex = 0;
259
- if (detector.regex.test(original)) {
260
- isSecret = true;
261
- break;
262
- }
263
- }
264
- }
265
- if (isSecret) {
266
- hasSecrets = true;
267
- result.scrubbedText = result.scrubbedText.replaceAll(token, '[SECRET_BLOCKED_UPGRADE_TO_PRO]');
268
- delete result.tokenMap[token];
269
- delete localSessionMap[token];
270
- }
271
- });
272
-
273
- if (hasSecrets) {
274
- process.stderr.write(`\x1b[1;31m🚫 [PrivacyScrubber] Secrets/Keys detected in output. Replaced with [SECRET_BLOCKED_UPGRADE_TO_PRO] to protect your data (Requires PRO tier).\x1b[0m\n`);
275
- }
276
- }
277
364
 
278
365
  return result.scrubbedText + (wasTruncated ? `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${limit.toLocaleString()} characters. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($299/mo) for unlimited payload processing: https://privacyscrubber.com/pricing]` : '');
279
366
  }
@@ -453,10 +540,26 @@ if (isRestoreMode) {
453
540
  let tokenMap = {};
454
541
  if (tokenMapInput) {
455
542
  try {
543
+ const safeReviver = (k, v) => (k === '__proto__' || k === 'constructor' || k === 'prototype') ? undefined : v;
456
544
  if (tokenMapInput.trim().startsWith('{')) {
457
- tokenMap = JSON.parse(tokenMapInput);
458
- } else if (fs.existsSync(tokenMapInput)) {
459
- tokenMap = JSON.parse(fs.readFileSync(tokenMapInput, 'utf8'));
545
+ tokenMap = JSON.parse(tokenMapInput, safeReviver);
546
+ } else {
547
+ const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
548
+ const resolvedMapPath = path.resolve(workspaceRoot, tokenMapInput);
549
+ const relToRoot = path.relative(workspaceRoot, resolvedMapPath);
550
+ if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
551
+ throw new Error('Security policy violation: --token-map path resolves outside active workspace.');
552
+ }
553
+ if (fs.existsSync(resolvedMapPath)) {
554
+ const lstat = fs.lstatSync(resolvedMapPath);
555
+ if (lstat.isSymbolicLink() || !lstat.isFile()) {
556
+ throw new Error('Security policy violation: --token-map must be a regular non-symlink file.');
557
+ }
558
+ if (lstat.size > 10 * 1024 * 1024) {
559
+ throw new Error('Token map file exceeds 10MB limit.');
560
+ }
561
+ tokenMap = JSON.parse(fs.readFileSync(resolvedMapPath, 'utf8'), safeReviver);
562
+ }
460
563
  }
461
564
  } catch (e) {
462
565
  console.error(`Error parsing token map: ${e.message}`);
@@ -526,6 +629,11 @@ Before reading sensitive files, running terminal commands that may print credent
526
629
  if (targets.includes("cursor")) {
527
630
  fs.writeFileSync(path.join(targetDir, '.cursorrules'), RULE_TEXT, 'utf8');
528
631
  generatedFiles.push('.cursorrules');
632
+ const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
633
+ if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
634
+ const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) — Mandatory PII & Secret Guard\nglobs: "**/*"\nalwaysApply: true\n---\n\n${RULE_TEXT}`;
635
+ fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
636
+ generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
529
637
  }
530
638
  if (targets.includes("windsurf")) {
531
639
  fs.writeFileSync(path.join(targetDir, '.windsurfrules'), RULE_TEXT, 'utf8');
@@ -546,6 +654,21 @@ Before reading sensitive files, running terminal commands that may print credent
546
654
  generatedFiles.push('CLAUDE.md');
547
655
  }
548
656
  }
657
+ if (targets.includes("agents_md") || targets.includes("codex") || rulesTargets.includes("all")) {
658
+ const agentsPath = path.join(targetDir, 'AGENTS.md');
659
+ if (fs.existsSync(agentsPath)) {
660
+ const existing = fs.readFileSync(agentsPath, 'utf8');
661
+ if (!existing.includes("Zero-Trust Agentic Guard")) {
662
+ fs.writeFileSync(agentsPath, existing.trimEnd() + "\n\n" + RULE_TEXT, 'utf8');
663
+ generatedFiles.push('AGENTS.md (appended)');
664
+ } else {
665
+ generatedFiles.push('AGENTS.md (already present)');
666
+ }
667
+ } else {
668
+ fs.writeFileSync(agentsPath, RULE_TEXT, 'utf8');
669
+ generatedFiles.push('AGENTS.md');
670
+ }
671
+ }
549
672
  if (targets.includes("copilot")) {
550
673
  const copilotDir = path.join(targetDir, '.github');
551
674
  if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
@@ -556,6 +679,46 @@ Before reading sensitive files, running terminal commands that may print credent
556
679
  fs.writeFileSync(path.join(targetDir, '.clinerules'), RULE_TEXT, 'utf8');
557
680
  generatedFiles.push('.clinerules');
558
681
  }
682
+ if (args.includes("--hooks") || targets.includes("hooks") || rulesTargets.includes("all")) {
683
+ try {
684
+ const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
685
+ fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
686
+ let cursorHooks = { version: 1, hooks: {} };
687
+ if (fs.existsSync(cursorHooksPath)) {
688
+ try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
689
+ }
690
+ if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
691
+ const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
692
+ for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
693
+ if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
694
+ if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
695
+ cursorHooks.hooks[ev].push({ command: hookCmd });
696
+ }
697
+ }
698
+ fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
699
+ generatedFiles.push('.cursor/hooks.json');
700
+
701
+ const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
702
+ fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
703
+ let claudeSettings = {};
704
+ if (fs.existsSync(claudeSettingsPath)) {
705
+ try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
706
+ }
707
+ if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
708
+ if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
709
+ const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
710
+ Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
711
+ );
712
+ if (!hasClaudeHook) {
713
+ claudeSettings.hooks.PreToolUse.push({
714
+ matcher: "Read|Bash",
715
+ hooks: [{ type: "command", command: hookCmd }]
716
+ });
717
+ }
718
+ fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
719
+ generatedFiles.push('.claude/settings.json');
720
+ } catch (_) {}
721
+ }
559
722
 
560
723
  console.log(`[Zero-Trust Agentic Guard: Agent Rules Generated]`);
561
724
  console.log(`Workspace: ${targetDir}`);