@privacyscrubber/mcp-server 2.4.5 → 2.4.7

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.
@@ -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.5';
30
+ let version = '2.4.7';
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,8 @@ 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");
39
+ const isWrapMode = args.includes("--wrap");
38
40
  const isJsonMode = args.includes("--json");
39
41
  const isRestoreMode = args.includes("--restore") || args.includes("--unscrub");
40
42
  let tokenMapInput = null;
@@ -42,18 +44,49 @@ const tokenMapIdx = args.indexOf("--token-map");
42
44
  if (tokenMapIdx !== -1 && args[tokenMapIdx + 1]) {
43
45
  tokenMapInput = args[tokenMapIdx + 1];
44
46
  }
45
- const hasCommand = cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0;
47
+ let hookPayloadArg = null;
48
+ const payloadIdx = args.indexOf("--payload");
49
+ if (payloadIdx !== -1 && args[payloadIdx + 1]) {
50
+ hookPayloadArg = args[payloadIdx + 1];
51
+ }
52
+
53
+ let wrapCommandArgs = [];
54
+ if (isWrapMode) {
55
+ if (cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0) {
56
+ wrapCommandArgs = args.slice(cmdIndex + 1);
57
+ } else {
58
+ const wrapIdx = args.indexOf("--wrap");
59
+ const afterWrap = args.slice(wrapIdx + 1);
60
+ for (let i = 0; i < afterWrap.length; i++) {
61
+ const a = afterWrap[i];
62
+ if ((a === "--profile" || a === "--key" || a === "--token-map") && afterWrap[i + 1]) {
63
+ i++;
64
+ continue;
65
+ }
66
+ if (a === "--quiet" || a === "-q" || a === "--verbose" || a === "--json") {
67
+ continue;
68
+ }
69
+ wrapCommandArgs = afterWrap.slice(i);
70
+ break;
71
+ }
72
+ }
73
+ }
74
+
75
+ const hasCommand = (cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0) || wrapCommandArgs.length > 0;
46
76
  const hasStdin = !process.stdin.isTTY;
47
77
 
48
- if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isRestoreMode && !hasStdin)) {
78
+ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isHookMode && !isWrapMode && !isRestoreMode && !hasStdin)) {
49
79
  console.log("PrivacyScrubber CLI & Agentic Guard: ps-guard / pii-masking-run");
50
80
  console.log("\nUsage:");
51
81
  console.log(" npx ps-guard [options] -- <command> [args...]");
82
+ console.log(" npx ps-guard --wrap [options] [--] <mcp-server-cmd> [args...]");
52
83
  console.log(" cat <file> | npx ps-guard [options]");
53
84
  console.log(" npx ps-guard --diff [--staged] [options]");
54
85
  console.log(" npx ps-guard --restore --token-map <file|json> [options]");
55
86
  console.log(" npx ps-guard --rules [targets]");
87
+ console.log(" npx ps-guard --hook (Claude Code & Cursor IDE PreToolUse / beforeReadFile hook)");
56
88
  console.log("\nOptions:");
89
+ console.log(" --wrap Transparent JSON-RPC 2.0 stdio proxy for any 3rd-party MCP server");
57
90
  console.log(" --profile <name> Specify detection profile (e.g. General, Dev, Medical, Legal, HR)");
58
91
  console.log(" --key <licKey> PrivacyScrubber PRO License Key");
59
92
  console.log(" --json Output structured JSON (scrubbedText, tokenMap, count, telemetry)");
@@ -62,11 +95,13 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
62
95
  console.log(" --diff Sanitize git diff output before committing or sending to AI");
63
96
  console.log(" --staged Use staged git changes with --diff");
64
97
  console.log(" --rules [targets] Generate AI agent rules (.cursorrules, .windsurfrules, CLAUDE.md, copilot, cline)");
98
+ console.log(" --hook Evaluate IDE lifecycle hook JSON from stdin (Claude Code & Cursor)");
65
99
  console.log(" -q, --quiet Suppress stderr proof telemetry");
66
100
  console.log(" -v, --version Print version");
67
101
  console.log(" -h, --help Show this help screen");
68
102
  console.log("\nExamples:");
69
103
  console.log(" npx ps-guard -- cat database-dump.sql");
104
+ console.log(" npx ps-guard --wrap -- npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb");
70
105
  console.log(" npx ps-guard --profile dev -- npm test");
71
106
  console.log(" cat .env | npx ps-guard --profile dev --json");
72
107
  console.log(" cat ai_reply.txt | npx ps-guard --restore --token-map map.json");
@@ -75,6 +110,98 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
75
110
  process.exit(0);
76
111
  }
77
112
 
113
+ // Fast-path Mode 0: Deterministic IDE Lifecycle Hook (--hook) for Claude Code & Cursor v1.7+
114
+ if (isHookMode) {
115
+ 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;
116
+ const SAFE_TEMPLATE_RX = /\.(?:example|sample|template|dist|schema)$/i;
117
+ 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;
118
+
119
+ const evaluateHookPayload = (rawStr) => {
120
+ let payload = {};
121
+ try {
122
+ payload = rawStr && rawStr.trim() ? JSON.parse(rawStr) : {};
123
+ } catch (_) {
124
+ process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
125
+ process.exit(0);
126
+ }
127
+
128
+ const eventName = String(payload.hook_event_name || payload.hookEventName || payload.event || "");
129
+ const toolName = String(payload.tool_name || payload.toolName || "");
130
+ const toolInput = (payload.tool_input || payload.toolInput || payload) || {};
131
+
132
+ const candidatePath = String(
133
+ toolInput.file_path || toolInput.path || toolInput.target_file || payload.file_path || payload.path || ""
134
+ ).trim();
135
+ const candidateCmd = String(
136
+ toolInput.command || toolInput.cmd || payload.command || ""
137
+ ).trim();
138
+
139
+ const isCursorEvent = /^(?:beforeReadFile|beforeShellExecution|beforeSubmitPrompt|beforeMCPExecution)$/i.test(eventName) ||
140
+ (!toolName && (payload.file_path !== undefined || payload.command !== undefined || payload.attachments !== undefined));
141
+
142
+ const isReadTool = /^(?:Read|View|read_file|view_file|ReadFile)$/i.test(toolName) || /^beforeReadFile$/i.test(eventName) || (!toolName && Boolean(candidatePath));
143
+ const isShellTool = /^(?:Bash|Execute|run_command| run_terminal_cmd)$/i.test(toolName) || /^beforeShellExecution$/i.test(eventName) || (!toolName && Boolean(candidateCmd));
144
+
145
+ if (isReadTool && candidatePath && SENSITIVE_FILE_RX.test(candidatePath) && !SAFE_TEMPLATE_RX.test(candidatePath)) {
146
+ 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.`;
147
+ if (isCursorEvent) {
148
+ process.stdout.write(JSON.stringify({
149
+ permission: "deny",
150
+ userMessage: `[PrivacyScrubber ZTDS] Direct read of ${path.basename(candidatePath)} blocked to prevent cloud egress.`,
151
+ agentMessage: reason
152
+ }) + "\n");
153
+ process.exit(0);
154
+ }
155
+ process.stdout.write(JSON.stringify({
156
+ permission: "deny",
157
+ hookSpecificOutput: {
158
+ hookEventName: eventName || "PreToolUse",
159
+ permissionDecision: "deny",
160
+ permissionDecisionReason: reason
161
+ }
162
+ }) + "\n");
163
+ process.stderr.write(reason + "\n");
164
+ process.exit(2);
165
+ }
166
+
167
+ if (isShellTool && candidateCmd && SENSITIVE_CMD_RX.test(candidateCmd)) {
168
+ 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.`;
169
+ if (isCursorEvent) {
170
+ process.stdout.write(JSON.stringify({
171
+ permission: "deny",
172
+ userMessage: `[PrivacyScrubber ZTDS] Shell command exposing raw credentials blocked.`,
173
+ agentMessage: reason
174
+ }) + "\n");
175
+ process.exit(0);
176
+ }
177
+ process.stdout.write(JSON.stringify({
178
+ permission: "deny",
179
+ hookSpecificOutput: {
180
+ hookEventName: eventName || "PreToolUse",
181
+ permissionDecision: "deny",
182
+ permissionDecisionReason: reason
183
+ }
184
+ }) + "\n");
185
+ process.stderr.write(reason + "\n");
186
+ process.exit(2);
187
+ }
188
+
189
+ process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
190
+ process.exit(0);
191
+ };
192
+
193
+ if (hookPayloadArg !== null) {
194
+ evaluateHookPayload(hookPayloadArg);
195
+ } else if (!process.stdin.isTTY) {
196
+ let buf = "";
197
+ process.stdin.setEncoding("utf8");
198
+ process.stdin.on("data", (c) => { buf += c; });
199
+ process.stdin.on("end", () => evaluateHookPayload(buf));
200
+ } else {
201
+ evaluateHookPayload("{}");
202
+ }
203
+ }
204
+
78
205
  // Import the production core engine
79
206
  const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
80
207
  const PrivacyScrubberCore = require(scrubberCorePath);
@@ -154,6 +281,13 @@ function resolveConfigPath() {
154
281
  return configPath;
155
282
  }
156
283
 
284
+ function isCatastrophicRegex(pattern) {
285
+ if (!pattern || typeof pattern !== 'string' || pattern.length > 500) return true;
286
+ if (/(\([^)]*[+*][^)]*\))[+*?{]/.test(pattern)) return true;
287
+ if (/(\.\*){3,}/.test(pattern)) return true;
288
+ return false;
289
+ }
290
+
157
291
  function loadCustomRules() {
158
292
  const license = checkLicenseStatus();
159
293
  const configPath = resolveConfigPath();
@@ -164,12 +298,20 @@ function loadCustomRules() {
164
298
  const rules = config.customRules || [];
165
299
  if (rules.length > 0) {
166
300
  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);
301
+ return rules.map(r => {
302
+ const pat = r.pattern || r.regex || "";
303
+ if (!pat || isCatastrophicRegex(pat)) return null;
304
+ try {
305
+ return {
306
+ pattern: pat,
307
+ label: (r.label || r.category || "CUSTOM").toUpperCase(),
308
+ regex: new RegExp(pat, "gi"),
309
+ category: (r.label || r.category || "CUSTOM").toUpperCase()
310
+ };
311
+ } catch (_) {
312
+ return null;
313
+ }
314
+ }).filter(Boolean);
173
315
  } else {
174
316
  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
317
  }
@@ -246,34 +388,6 @@ function performSanitization(text) {
246
388
  if (result && result.tokenMap) {
247
389
  Object.assign(localSessionMap, result.tokenMap);
248
390
  }
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
391
 
278
392
  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
393
  }
@@ -338,11 +452,27 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
338
452
  trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
339
453
  };
340
454
 
455
+ const agenticProtocol = {
456
+ protocolsSupported: ['MCP-1.0 (stdio/SSE)', 'A2A-1.0 (Agent2Agent)', 'ZTDS-RFC-v1.0'],
457
+ agentCardUrl: 'https://privacyscrubber.com/.well-known/agent-card.json',
458
+ ztdsStandardCardUrl: 'https://ztds.ai/.well-known/agent-card.json',
459
+ regulatorySafeHarbor: [
460
+ 'GDPR-Art-4(5)-Pseudonymization',
461
+ 'GDPR-Art-25-Privacy-by-Design',
462
+ 'GDPR-Art-32-TOMs',
463
+ 'EU-AI-Act-Art-50',
464
+ 'EU-AI-Act-Art-17-EN-18286-QMS',
465
+ 'CNIL-ICO-Agentic-Memory-Compartmentalization',
466
+ 'IL-PPL-Amendment-13-Sec-17'
467
+ ],
468
+ zeroCleartextEgress: true
469
+ };
470
+
341
471
  const statutoryCitations = [
342
472
  {
343
- statute: 'EU AI Act (Art. 50)',
344
- scope: 'AI Transparency & Data Minimization',
345
- rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
473
+ statute: 'EU AI Act (Art. 50 & Art. 17 / EN 18286:2026 QMS)',
474
+ scope: 'AI Transparency, Data Minimization & Harmonized QMS Governance',
475
+ rule: 'Sanitization runs locally by design; reduces the content sent to external AI models and supports Article 17 Quality Management System (EN 18286:2026) data governance controls. Informational mapping, not a compliance determination.'
346
476
  },
347
477
  {
348
478
  statute: 'GDPR (Art. 4, 25, 28, 32)',
@@ -374,6 +504,7 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
374
504
  frameworksList: Array.from(frameworksSet),
375
505
  statutoryCitations,
376
506
  ipHoldings,
507
+ agenticProtocol,
377
508
  multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
378
509
  };
379
510
  }
@@ -453,10 +584,26 @@ if (isRestoreMode) {
453
584
  let tokenMap = {};
454
585
  if (tokenMapInput) {
455
586
  try {
587
+ const safeReviver = (k, v) => (k === '__proto__' || k === 'constructor' || k === 'prototype') ? undefined : v;
456
588
  if (tokenMapInput.trim().startsWith('{')) {
457
- tokenMap = JSON.parse(tokenMapInput);
458
- } else if (fs.existsSync(tokenMapInput)) {
459
- tokenMap = JSON.parse(fs.readFileSync(tokenMapInput, 'utf8'));
589
+ tokenMap = JSON.parse(tokenMapInput, safeReviver);
590
+ } else {
591
+ const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
592
+ const resolvedMapPath = path.resolve(workspaceRoot, tokenMapInput);
593
+ const relToRoot = path.relative(workspaceRoot, resolvedMapPath);
594
+ if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
595
+ throw new Error('Security policy violation: --token-map path resolves outside active workspace.');
596
+ }
597
+ if (fs.existsSync(resolvedMapPath)) {
598
+ const lstat = fs.lstatSync(resolvedMapPath);
599
+ if (lstat.isSymbolicLink() || !lstat.isFile()) {
600
+ throw new Error('Security policy violation: --token-map must be a regular non-symlink file.');
601
+ }
602
+ if (lstat.size > 10 * 1024 * 1024) {
603
+ throw new Error('Token map file exceeds 10MB limit.');
604
+ }
605
+ tokenMap = JSON.parse(fs.readFileSync(resolvedMapPath, 'utf8'), safeReviver);
606
+ }
460
607
  }
461
608
  } catch (e) {
462
609
  console.error(`Error parsing token map: ${e.message}`);
@@ -526,6 +673,11 @@ Before reading sensitive files, running terminal commands that may print credent
526
673
  if (targets.includes("cursor")) {
527
674
  fs.writeFileSync(path.join(targetDir, '.cursorrules'), RULE_TEXT, 'utf8');
528
675
  generatedFiles.push('.cursorrules');
676
+ const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
677
+ if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
678
+ const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) — Mandatory PII & Secret Guard\nglobs: "**/*"\nalwaysApply: true\n---\n\n${RULE_TEXT}`;
679
+ fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
680
+ generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
529
681
  }
530
682
  if (targets.includes("windsurf")) {
531
683
  fs.writeFileSync(path.join(targetDir, '.windsurfrules'), RULE_TEXT, 'utf8');
@@ -546,6 +698,21 @@ Before reading sensitive files, running terminal commands that may print credent
546
698
  generatedFiles.push('CLAUDE.md');
547
699
  }
548
700
  }
701
+ if (targets.includes("agents_md") || targets.includes("codex") || rulesTargets.includes("all")) {
702
+ const agentsPath = path.join(targetDir, 'AGENTS.md');
703
+ if (fs.existsSync(agentsPath)) {
704
+ const existing = fs.readFileSync(agentsPath, 'utf8');
705
+ if (!existing.includes("Zero-Trust Agentic Guard")) {
706
+ fs.writeFileSync(agentsPath, existing.trimEnd() + "\n\n" + RULE_TEXT, 'utf8');
707
+ generatedFiles.push('AGENTS.md (appended)');
708
+ } else {
709
+ generatedFiles.push('AGENTS.md (already present)');
710
+ }
711
+ } else {
712
+ fs.writeFileSync(agentsPath, RULE_TEXT, 'utf8');
713
+ generatedFiles.push('AGENTS.md');
714
+ }
715
+ }
549
716
  if (targets.includes("copilot")) {
550
717
  const copilotDir = path.join(targetDir, '.github');
551
718
  if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
@@ -556,6 +723,46 @@ Before reading sensitive files, running terminal commands that may print credent
556
723
  fs.writeFileSync(path.join(targetDir, '.clinerules'), RULE_TEXT, 'utf8');
557
724
  generatedFiles.push('.clinerules');
558
725
  }
726
+ if (args.includes("--hooks") || targets.includes("hooks") || rulesTargets.includes("all")) {
727
+ try {
728
+ const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
729
+ fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
730
+ let cursorHooks = { version: 1, hooks: {} };
731
+ if (fs.existsSync(cursorHooksPath)) {
732
+ try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
733
+ }
734
+ if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
735
+ const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
736
+ for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
737
+ if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
738
+ if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
739
+ cursorHooks.hooks[ev].push({ command: hookCmd });
740
+ }
741
+ }
742
+ fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
743
+ generatedFiles.push('.cursor/hooks.json');
744
+
745
+ const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
746
+ fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
747
+ let claudeSettings = {};
748
+ if (fs.existsSync(claudeSettingsPath)) {
749
+ try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
750
+ }
751
+ if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
752
+ if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
753
+ const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
754
+ Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
755
+ );
756
+ if (!hasClaudeHook) {
757
+ claudeSettings.hooks.PreToolUse.push({
758
+ matcher: "Read|Bash",
759
+ hooks: [{ type: "command", command: hookCmd }]
760
+ });
761
+ }
762
+ fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
763
+ generatedFiles.push('.claude/settings.json');
764
+ } catch (_) {}
765
+ }
559
766
 
560
767
  console.log(`[Zero-Trust Agentic Guard: Agent Rules Generated]`);
561
768
  console.log(`Workspace: ${targetDir}`);
@@ -612,8 +819,183 @@ if (isDiffMode) {
612
819
  }
613
820
  }
614
821
 
615
- // Mode 3: Stdin Stream Mode (pipe)
616
- if (hasStdin && !hasCommand && !isRestoreMode && !isDiffMode && !isRulesMode) {
822
+ // Mode 2.5: Transparent MCP JSON-RPC 2.0 Stdio Proxy Wrapper (--wrap)
823
+ if (isWrapMode) {
824
+ if (wrapCommandArgs.length === 0) {
825
+ console.error('Error: --wrap requires an upstream MCP server command (e.g. npx ps-guard --wrap -- npx -y @modelcontextprotocol/server-postgres ...)');
826
+ process.exit(1);
827
+ }
828
+
829
+ const SKIP_SANITIZE_KEYS = new Set([
830
+ 'jsonrpc', 'id', 'method', 'type', 'mimeType', 'uri', 'name', 'role',
831
+ 'protocolVersion', 'cursor', 'nextCursor', 'isError', 'version'
832
+ ]);
833
+
834
+ const deepDetokenizeParams = (node, depth = 0) => {
835
+ if (depth > 20 || node === null || node === undefined) return node;
836
+ if (typeof node === 'string') {
837
+ if (Object.keys(localSessionMap).length === 0) return node;
838
+ const res = PrivacyScrubberCore.unscrubText(node, localSessionMap);
839
+ return (res && typeof res.restoredText === 'string') ? res.restoredText : node;
840
+ }
841
+ if (Array.isArray(node)) {
842
+ return node.map(item => deepDetokenizeParams(item, depth + 1));
843
+ }
844
+ if (typeof node === 'object') {
845
+ const out = {};
846
+ for (const [k, v] of Object.entries(node)) {
847
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
848
+ out[k] = deepDetokenizeParams(v, depth + 1);
849
+ }
850
+ return out;
851
+ }
852
+ return node;
853
+ };
854
+
855
+ const deepSanitizeResult = (node, keyName = '', depth = 0) => {
856
+ if (depth > 20 || node === null || node === undefined) return node;
857
+ if (typeof node === 'string') {
858
+ if (SKIP_SANITIZE_KEYS.has(keyName) || node.length < 4) return node;
859
+ return performSanitization(node);
860
+ }
861
+ if (Array.isArray(node)) {
862
+ return node.map(item => deepSanitizeResult(item, keyName, depth + 1));
863
+ }
864
+ if (typeof node === 'object') {
865
+ const out = {};
866
+ for (const [k, v] of Object.entries(node)) {
867
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
868
+ if (k === 'inputSchema' || k === 'outputSchema' || k === 'capabilities' || k === 'serverInfo' || k === '_meta') {
869
+ out[k] = v;
870
+ } else {
871
+ out[k] = deepSanitizeResult(v, k, depth + 1);
872
+ }
873
+ }
874
+ return out;
875
+ }
876
+ return node;
877
+ };
878
+
879
+ const zeroizeSessionMap = () => {
880
+ for (const k of Object.keys(localSessionMap)) {
881
+ localSessionMap[k] = '';
882
+ delete localSessionMap[k];
883
+ }
884
+ };
885
+
886
+ const [cmdBin, ...cmdRest] = wrapCommandArgs;
887
+ const child = spawn(cmdBin, cmdRest, {
888
+ stdio: ['pipe', 'pipe', 'pipe'],
889
+ shell: process.platform === 'win32'
890
+ });
891
+
892
+ // Intercept client -> upstream MCP server (JSON-RPC requests on stdin)
893
+ const rlClientIn = readline.createInterface({
894
+ input: process.stdin,
895
+ terminal: false
896
+ });
897
+
898
+ rlClientIn.on('line', (line) => {
899
+ if (!child.stdin || child.stdin.destroyed) return;
900
+ const trimmed = line.trim();
901
+ if (!trimmed.startsWith('{')) {
902
+ child.stdin.write(line + '\n');
903
+ return;
904
+ }
905
+ try {
906
+ const msg = JSON.parse(trimmed);
907
+ if (msg && typeof msg === 'object' && msg.params && (msg.method === 'tools/call' || msg.method === 'prompts/get' || msg.method === 'resources/read' || msg.method === 'sampling/createMessage')) {
908
+ msg.params = deepDetokenizeParams(msg.params);
909
+ child.stdin.write(JSON.stringify(msg) + '\n');
910
+ return;
911
+ }
912
+ } catch (_) {}
913
+ child.stdin.write(line + '\n');
914
+ });
915
+
916
+ rlClientIn.on('close', () => {
917
+ if (child.stdin && !child.stdin.destroyed) {
918
+ child.stdin.end();
919
+ }
920
+ });
921
+
922
+ // Intercept upstream MCP server -> client (JSON-RPC responses/notifications on stdout)
923
+ const rlUpstreamOut = readline.createInterface({
924
+ input: child.stdout,
925
+ terminal: false
926
+ });
927
+
928
+ rlUpstreamOut.on('line', (line) => {
929
+ const trimmed = line.trim();
930
+ if (!trimmed.startsWith('{')) {
931
+ process.stdout.write(performSanitization(line) + '\n');
932
+ return;
933
+ }
934
+ try {
935
+ const msg = JSON.parse(trimmed);
936
+ if (msg && typeof msg === 'object') {
937
+ if (msg.result && typeof msg.result === 'object') {
938
+ const isSchemaListing = Array.isArray(msg.result.tools) || Array.isArray(msg.result.prompts) || Array.isArray(msg.result.resourceTemplates) || Boolean(msg.result.protocolVersion);
939
+ if (!isSchemaListing) {
940
+ msg.result = deepSanitizeResult(msg.result);
941
+ const receipt = buildPsr2Receipt(localSessionMap);
942
+ const telemetry = buildCisoAuditTelemetry(localSessionMap);
943
+ msg.result._meta = Object.assign({}, msg.result._meta || {}, {
944
+ ztds_receipt: {
945
+ version: receipt.version,
946
+ timestamp: receipt.timestamp,
947
+ manifestSha256: receipt.manifestSha256,
948
+ receiptSha256: receipt.receiptSha256,
949
+ entityCounts: receipt.entityCounts,
950
+ tokensTotal: receipt.tokensTotal,
951
+ riskLevel: telemetry.riskLevel,
952
+ regulatorySafeHarbor: telemetry.agenticProtocol ? telemetry.agenticProtocol.regulatorySafeHarbor : [],
953
+ zero_cleartext_egress: true
954
+ }
955
+ });
956
+ }
957
+ } else if (msg.params && typeof msg.params === 'object') {
958
+ msg.params = deepSanitizeResult(msg.params);
959
+ }
960
+ process.stdout.write(JSON.stringify(msg) + '\n');
961
+ return;
962
+ }
963
+ } catch (_) {}
964
+ process.stdout.write(performSanitization(line) + '\n');
965
+ });
966
+
967
+ const rlUpstreamErr = readline.createInterface({
968
+ input: child.stderr,
969
+ terminal: false
970
+ });
971
+
972
+ rlUpstreamErr.on('line', (line) => {
973
+ process.stderr.write(performSanitization(line) + '\n');
974
+ });
975
+
976
+ const handleSignal = (sig) => {
977
+ zeroizeSessionMap();
978
+ if (!child.killed) {
979
+ try { child.kill(sig); } catch (_) {}
980
+ }
981
+ };
982
+ process.on('SIGINT', () => handleSignal('SIGINT'));
983
+ process.on('SIGTERM', () => handleSignal('SIGTERM'));
984
+ process.on('exit', zeroizeSessionMap);
985
+
986
+ child.on('close', (code) => {
987
+ emitProofFeedback();
988
+ zeroizeSessionMap();
989
+ process.exit(code || 0);
990
+ });
991
+
992
+ child.on('error', (err) => {
993
+ zeroizeSessionMap();
994
+ console.error(`Error spawning wrapped MCP server: ${err.message}`);
995
+ process.exit(1);
996
+ });
997
+ } else if (hasStdin && !hasCommand && !isRestoreMode && !isDiffMode && !isRulesMode) {
998
+ // Mode 3: Stdin Stream Mode (pipe)
617
999
  if (isJsonMode) {
618
1000
  let rawInput = '';
619
1001
  process.stdin.setEncoding('utf8');