shroud-privacy 2.5.4 → 2.5.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/README.md +23 -5
- package/app-server.mjs +233 -16
- package/clients/claude-code/hooks.json +32 -0
- package/clients/claude-code/mcp.json +9 -0
- package/clients/claude-code/shroud-bridge.mjs +406 -0
- package/clients/claude-code/shroud-mcp.mjs +548 -0
- package/clients/claude-code/socket-client.mjs +119 -0
- package/clients/codex/bridge-env.mjs +21 -0
- package/clients/codex/shroud-bridge.mjs +220 -0
- package/clients/codex/shroud-mcp.mjs +65 -0
- package/clients/shared/agent-config.mjs +35 -0
- package/dist/codex-telemetry.d.ts +16 -0
- package/dist/codex-telemetry.js +69 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/hooks.js +216 -58
- package/dist/obfuscator.d.ts +5 -0
- package/dist/obfuscator.js +10 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
<a href="#install">Install</a> ·
|
|
14
14
|
<a href="#why-shroud">Why Shroud</a> ·
|
|
15
15
|
<a href="#configure">Configure</a> ·
|
|
16
|
+
<a href="docs/integrations.md">Integrations</a> ·
|
|
16
17
|
<a href="#agent-privacy-protocol-app">APP Protocol</a> ·
|
|
17
18
|
<a href="CHANGELOG.md">Changelog</a>
|
|
18
19
|
</p>
|
|
19
20
|
|
|
20
21
|
> Apache 2.0 · Zero runtime dependencies · Anthropic + OpenAI + Google supported · Prompt-caching friendly · Works with [OpenClaw](https://openclaw.ai), [Hermes Agent](https://github.com/nousresearch/hermes-agent), or any agent via [APP](#agent-privacy-protocol-app)
|
|
21
22
|
|
|
23
|
+
**Detailed integration reference:** [`docs/integrations.md`](docs/integrations.md)
|
|
24
|
+
|
|
22
25
|
---
|
|
23
26
|
|
|
24
27
|
## Why Shroud
|
|
@@ -78,14 +81,14 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
|
|
|
78
81
|
|
|
79
82
|
> **How it works:** Shroud intercepts ALL outbound LLM API calls (Anthropic, OpenAI, Google, any provider) at the `fetch` level and obfuscates detected entities in every message — including assistant history and Slack `<mailto:>` markup — before it leaves the process. On the response side, SSE streaming is deobfuscated per content block with buffered flushing. Every delivery path (Slack, WhatsApp, TUI, Telegram, Discord, Signal, web) gets real text automatically. Zero host patches required.
|
|
80
83
|
|
|
81
|
-
> **Requires OpenClaw 2026.3.
|
|
84
|
+
> **Requires OpenClaw 2026.3.24 or later.**
|
|
82
85
|
|
|
83
86
|
### OpenClaw support policy
|
|
84
87
|
|
|
85
88
|
- **Formal minimum supported version:** `2026.3.24` (from `openclaw.plugin.json` `minOpenClawVersion`).
|
|
86
89
|
- **Release validation matrix (this release):**
|
|
87
90
|
- **Baseline:** `2026.3.28` (includes WhatsApp E2E path)
|
|
88
|
-
- **Latest-at-release:** `2026.4.
|
|
91
|
+
- **Latest-at-release:** `2026.4.14` (Slack E2E pass)
|
|
89
92
|
- **Latest caveat:** on OpenClaw builds where WhatsApp provisioning via `channels add` is unsupported, latest-focused compat runs skip WhatsApp E2E and validate Slack E2E.
|
|
90
93
|
- **Source of truth for current matrix:** `docs/ci-current-state.md` and `CHANGELOG.md`.
|
|
91
94
|
|
|
@@ -93,10 +96,10 @@ Shroud does not guarantee compliance — regex-based detection has limitations (
|
|
|
93
96
|
|
|
94
97
|
## Install
|
|
95
98
|
|
|
96
|
-
### OpenClaw (2026.3.
|
|
99
|
+
### OpenClaw (2026.3.24+)
|
|
97
100
|
|
|
98
101
|
```bash
|
|
99
|
-
openclaw --version # ensure 2026.3.
|
|
102
|
+
openclaw --version # ensure 2026.3.24+
|
|
100
103
|
openclaw plugins install shroud-privacy
|
|
101
104
|
```
|
|
102
105
|
|
|
@@ -138,7 +141,20 @@ Add to your project's `.mcp.json` or `~/.claude/.mcp.json`:
|
|
|
138
141
|
}
|
|
139
142
|
```
|
|
140
143
|
|
|
141
|
-
That's it — the MCP server auto-starts
|
|
144
|
+
That's it — the MCP server auto-starts a dedicated APP daemon on `/tmp/shroud-claude-mcp.sock` and writes Claude MCP session state under `OPENCLAW_STATE_DIR` (or `~/.openclaw`) as `shroud-claude-mcp-sessions.json`. Claude gains six tools: `shroud_obfuscate`, `shroud_deobfuscate`, `shroud_status`, `shroud_scan_tool`, `shroud_configure`, and `shroud_reset`.
|
|
145
|
+
|
|
146
|
+
If you want automatic tool-boundary obfuscation/deobfuscation instead of explicit MCP tools, use the shipped Claude hooks bridge in `clients/claude-code/shroud-bridge.mjs` together with `clients/claude-code/hooks.json`.
|
|
147
|
+
|
|
148
|
+
### Codex
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm install shroud-privacy
|
|
152
|
+
codex mcp add shroud -- node node_modules/shroud-privacy/clients/codex/shroud-mcp.mjs
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Codex uses the same six MCP tools as Claude, but on a separate APP daemon and socket: `/tmp/shroud-codex-mcp.sock`. Its APP session file defaults to `shroud-codex-mcp-sessions.json` under `OPENCLAW_STATE_DIR` or `~/.openclaw`.
|
|
156
|
+
|
|
157
|
+
The Codex MCP wrapper also auto-starts `clients/codex/shroud-bridge.mjs` on the host. That bridge watches Codex's local `~/.codex/history.jsonl` and writes `shroud-codex-cli-sessions.json` into the shared OpenClaw state dir, so Codex call/session counters stay current even when Codex is not actively invoking Shroud MCP tools. Set `SHROUD_CODEX_BRIDGE=0` only if you explicitly want to disable that behavior.
|
|
142
158
|
|
|
143
159
|
### Any agent (via APP)
|
|
144
160
|
|
|
@@ -165,6 +181,8 @@ with ShroudClient() as shroud:
|
|
|
165
181
|
|
|
166
182
|
Copy `clients/python/shroud_client.py` into your project, or import it directly from the npm install path. Requires Node.js on the PATH.
|
|
167
183
|
|
|
184
|
+
For direct APP clients such as NCG, call `identify` first if you want per-agent session counters, then `obfuscate` / `deobfuscate`, and optionally wrap tool execution with `tool_call` / `tool_result` for telemetry.
|
|
185
|
+
|
|
168
186
|
**Any language:**
|
|
169
187
|
|
|
170
188
|
Spawn the APP server and talk JSON-RPC over stdin/stdout:
|
package/app-server.mjs
CHANGED
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
* SHROUD_PLUGIN_CONFIG JSON config for the engine
|
|
14
14
|
* SHROUD_STORE_FILE Persistent store file path
|
|
15
15
|
* SHROUD_STATS_FILE Stats dump file path
|
|
16
|
+
* SHROUD_APP_EVENTS_FILE JSONL file for APP-side event export
|
|
17
|
+
* SHROUD_APP_SESSIONS_FILE JSON file for APP-side agent session state
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import { createHash, randomBytes } from "node:crypto";
|
|
19
21
|
import { createInterface } from "node:readline";
|
|
20
22
|
import { pathToFileURL } from "node:url";
|
|
21
23
|
import { resolve, dirname } from "node:path";
|
|
22
|
-
import { writeFileSync, readFileSync } from "node:fs";
|
|
24
|
+
import { writeFileSync, readFileSync, unlinkSync } from "node:fs";
|
|
23
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { createServer as createNetServer } from "node:net";
|
|
24
27
|
|
|
25
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
29
|
const shroudDist = process.argv[2] || resolve(__dirname, "dist");
|
|
@@ -61,6 +64,8 @@ let obfuscator = new Obfuscator(config);
|
|
|
61
64
|
|
|
62
65
|
const STATS_FILE = process.env.SHROUD_STATS_FILE || "/tmp/shroud-stats.json";
|
|
63
66
|
const STORE_FILE = process.env.SHROUD_STORE_FILE || "";
|
|
67
|
+
const APP_EVENTS_FILE = process.env.SHROUD_APP_EVENTS_FILE || "/tmp/shroud-app-events.jsonl";
|
|
68
|
+
const APP_SESSIONS_FILE = process.env.SHROUD_APP_SESSIONS_FILE || "/tmp/shroud-app-sessions.json";
|
|
64
69
|
|
|
65
70
|
// ---------------------------------------------------------------------------
|
|
66
71
|
// Audit chain
|
|
@@ -116,6 +121,20 @@ function resolvePartition(params) {
|
|
|
116
121
|
const startTime = Date.now();
|
|
117
122
|
let requestCount = 0;
|
|
118
123
|
let totalProcessingMs = 0;
|
|
124
|
+
const toolSequence = [];
|
|
125
|
+
const privacy = {
|
|
126
|
+
obfuscationCalls: 0,
|
|
127
|
+
deobfuscationCalls: 0,
|
|
128
|
+
entitiesObfuscated: 0,
|
|
129
|
+
replacementsDeobfuscated: 0,
|
|
130
|
+
categoryCounts: {},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
let agentIdentified = false;
|
|
134
|
+
let agentLabel = null;
|
|
135
|
+
let agentBuildId = null;
|
|
136
|
+
let agentVersion = null;
|
|
137
|
+
let agentChannel = null;
|
|
119
138
|
|
|
120
139
|
// ---------------------------------------------------------------------------
|
|
121
140
|
// Stats dump helper
|
|
@@ -130,6 +149,36 @@ function dumpStats() {
|
|
|
130
149
|
} catch { /* best-effort */ }
|
|
131
150
|
}
|
|
132
151
|
|
|
152
|
+
function dumpSessionFile() {
|
|
153
|
+
if (!agentIdentified) return;
|
|
154
|
+
try {
|
|
155
|
+
const session = {
|
|
156
|
+
agentLabel,
|
|
157
|
+
agentBuildId,
|
|
158
|
+
agentVersion,
|
|
159
|
+
channel: agentChannel,
|
|
160
|
+
source: "app-server",
|
|
161
|
+
pid: process.pid,
|
|
162
|
+
requestCount,
|
|
163
|
+
uptimeMs: Date.now() - startTime,
|
|
164
|
+
securityEvents: 0,
|
|
165
|
+
storeSize: getObfuscator().getStats().storeMappings ?? 0,
|
|
166
|
+
classification: {
|
|
167
|
+
role: "APP Agent",
|
|
168
|
+
confidencePct: 100,
|
|
169
|
+
confidence: "high",
|
|
170
|
+
colour: "#06b6d4",
|
|
171
|
+
signals: ["app-server"],
|
|
172
|
+
},
|
|
173
|
+
toolSequence: toolSequence.slice(-20),
|
|
174
|
+
privacy,
|
|
175
|
+
eventsFile: APP_EVENTS_FILE,
|
|
176
|
+
updatedAt: new Date().toISOString(),
|
|
177
|
+
};
|
|
178
|
+
writeFileSync(APP_SESSIONS_FILE, JSON.stringify(session, null, 2) + "\n");
|
|
179
|
+
} catch { /* best-effort */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
133
182
|
// ---------------------------------------------------------------------------
|
|
134
183
|
// JSON-RPC helpers
|
|
135
184
|
// ---------------------------------------------------------------------------
|
|
@@ -153,6 +202,37 @@ function jsonResult(id, result) {
|
|
|
153
202
|
// APP method handlers
|
|
154
203
|
// ---------------------------------------------------------------------------
|
|
155
204
|
|
|
205
|
+
function handleIdentify(id, params) {
|
|
206
|
+
if (!params || typeof params.agent !== "string" || !params.agent.trim()) {
|
|
207
|
+
return jsonError(id, ERR_BAD_PARAMS, 'Missing required param: agent (string)');
|
|
208
|
+
}
|
|
209
|
+
if (typeof params.version !== "string" || !params.version.trim()) {
|
|
210
|
+
return jsonError(id, ERR_BAD_PARAMS, 'Missing required param: version (string)');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
agentLabel = params.agent.trim();
|
|
214
|
+
agentVersion = params.version.trim();
|
|
215
|
+
agentChannel = typeof params.channel === "string" && params.channel.trim() ? params.channel.trim() : "app";
|
|
216
|
+
agentBuildId = createHash("sha256")
|
|
217
|
+
.update(agentLabel + ":" + agentVersion)
|
|
218
|
+
.digest("hex")
|
|
219
|
+
.slice(0, 16);
|
|
220
|
+
agentIdentified = true;
|
|
221
|
+
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
`[app-server] Agent identified: ${agentLabel} v${agentVersion} (${agentChannel}) buildId=${agentBuildId}\n`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
dumpSessionFile();
|
|
227
|
+
|
|
228
|
+
return jsonResult(id, {
|
|
229
|
+
ok: true,
|
|
230
|
+
agent: agentLabel,
|
|
231
|
+
buildId: agentBuildId,
|
|
232
|
+
security: false,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
156
236
|
function handleObfuscate(id, params) {
|
|
157
237
|
if (!params || typeof params.text !== "string") {
|
|
158
238
|
return jsonError(id, ERR_BAD_PARAMS, "Missing required param: text");
|
|
@@ -193,6 +273,12 @@ function handleObfuscate(id, params) {
|
|
|
193
273
|
audit.fakesSample = Object.values(out.mappingsUsed || {}).slice(0, maxFakes);
|
|
194
274
|
result.audit = audit;
|
|
195
275
|
|
|
276
|
+
privacy.obfuscationCalls++;
|
|
277
|
+
privacy.entitiesObfuscated += out.entities.length;
|
|
278
|
+
for (const [cat, count] of Object.entries(categories)) {
|
|
279
|
+
privacy.categoryCounts[cat] = (privacy.categoryCounts[cat] || 0) + count;
|
|
280
|
+
}
|
|
281
|
+
|
|
196
282
|
dumpStats();
|
|
197
283
|
return jsonResult(id, result);
|
|
198
284
|
}
|
|
@@ -231,6 +317,11 @@ function handleDeobfuscate(id, params) {
|
|
|
231
317
|
};
|
|
232
318
|
result.audit = audit;
|
|
233
319
|
|
|
320
|
+
if ((deobResult.replacementCount || 0) > 0) {
|
|
321
|
+
privacy.deobfuscationCalls++;
|
|
322
|
+
privacy.replacementsDeobfuscated += deobResult.replacementCount || 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
234
325
|
dumpStats();
|
|
235
326
|
return jsonResult(id, result);
|
|
236
327
|
}
|
|
@@ -381,8 +472,53 @@ function handleConfigure(id, params) {
|
|
|
381
472
|
return jsonResult(id, { ok: true, appliedKeys });
|
|
382
473
|
}
|
|
383
474
|
|
|
475
|
+
function handleSecurity(id) {
|
|
476
|
+
return jsonResult(id, {
|
|
477
|
+
enabled: false,
|
|
478
|
+
mode: "off",
|
|
479
|
+
events: 0,
|
|
480
|
+
byThreatClass: {},
|
|
481
|
+
agent: agentIdentified ? {
|
|
482
|
+
label: agentLabel,
|
|
483
|
+
buildId: agentBuildId,
|
|
484
|
+
version: agentVersion,
|
|
485
|
+
channel: agentChannel,
|
|
486
|
+
requestCount,
|
|
487
|
+
} : null,
|
|
488
|
+
recentEvents: [],
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function handleToolCall(id, params) {
|
|
493
|
+
if (!params || typeof params.tool !== "string" || !params.tool.trim()) {
|
|
494
|
+
return jsonError(id, ERR_BAD_PARAMS, "Missing required param: tool (string)");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
toolSequence.push(params.tool.trim());
|
|
498
|
+
|
|
499
|
+
return jsonResult(id, {
|
|
500
|
+
blocked: false,
|
|
501
|
+
reason: null,
|
|
502
|
+
sequenceLength: toolSequence.length,
|
|
503
|
+
events: [],
|
|
504
|
+
securityEnabled: false,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function handleToolResult(id, params) {
|
|
509
|
+
if (!params || typeof params.tool !== "string" || !params.tool.trim()) {
|
|
510
|
+
return jsonError(id, ERR_BAD_PARAMS, "Missing required param: tool (string)");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return jsonResult(id, {
|
|
514
|
+
ok: true,
|
|
515
|
+
recorded: true,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
384
519
|
function handleShutdown(id) {
|
|
385
520
|
dumpStats();
|
|
521
|
+
dumpSessionFile();
|
|
386
522
|
const response = jsonResult(id, { ok: true, flushed: true });
|
|
387
523
|
process.stdout.write(response + "\n");
|
|
388
524
|
process.exit(0);
|
|
@@ -413,32 +549,37 @@ function handleSetPartition(id, params) {
|
|
|
413
549
|
// ---------------------------------------------------------------------------
|
|
414
550
|
|
|
415
551
|
const METHODS = {
|
|
552
|
+
identify: handleIdentify,
|
|
416
553
|
obfuscate: handleObfuscate,
|
|
417
554
|
deobfuscate: handleDeobfuscate,
|
|
418
555
|
batch: handleBatch,
|
|
419
556
|
reset: handleReset,
|
|
420
557
|
stats: handleStats,
|
|
421
558
|
health: handleHealth,
|
|
559
|
+
security: handleSecurity,
|
|
560
|
+
tool_call: handleToolCall,
|
|
561
|
+
tool_result: handleToolResult,
|
|
422
562
|
configure: handleConfigure,
|
|
423
563
|
shutdown: handleShutdown,
|
|
424
564
|
setPartition: handleSetPartition,
|
|
425
565
|
};
|
|
426
566
|
|
|
427
|
-
function dispatch(line) {
|
|
567
|
+
function dispatch(line, writeFn) {
|
|
428
568
|
if (!line.trim()) return;
|
|
569
|
+
const write = writeFn || ((s) => process.stdout.write(s));
|
|
429
570
|
|
|
430
571
|
let req;
|
|
431
572
|
try {
|
|
432
573
|
req = JSON.parse(line);
|
|
433
574
|
} catch (e) {
|
|
434
|
-
|
|
575
|
+
write(jsonError(null, ERR_PARSE, `Parse error: ${e.message}`) + "\n");
|
|
435
576
|
return;
|
|
436
577
|
}
|
|
437
578
|
|
|
438
579
|
const { id, method, params } = req;
|
|
439
580
|
|
|
440
581
|
if (id === undefined || id === null || !method) {
|
|
441
|
-
|
|
582
|
+
write(
|
|
442
583
|
jsonError(id ?? null, ERR_INVALID_REQ, "Missing required field: id and method") + "\n"
|
|
443
584
|
);
|
|
444
585
|
return;
|
|
@@ -446,7 +587,7 @@ function dispatch(line) {
|
|
|
446
587
|
|
|
447
588
|
const handler = METHODS[method];
|
|
448
589
|
if (!handler) {
|
|
449
|
-
|
|
590
|
+
write(
|
|
450
591
|
jsonError(id, ERR_NO_METHOD, `Method not found: ${method}`) + "\n"
|
|
451
592
|
);
|
|
452
593
|
return;
|
|
@@ -459,10 +600,11 @@ function dispatch(line) {
|
|
|
459
600
|
const response = handler(id, params);
|
|
460
601
|
// shutdown writes its own response and exits
|
|
461
602
|
if (method !== "shutdown") {
|
|
462
|
-
|
|
603
|
+
write(response + "\n");
|
|
604
|
+
dumpSessionFile();
|
|
463
605
|
}
|
|
464
606
|
} catch (e) {
|
|
465
|
-
|
|
607
|
+
write(
|
|
466
608
|
jsonError(id, ERR_ENGINE, `Engine error: ${e.message}`) + "\n"
|
|
467
609
|
);
|
|
468
610
|
}
|
|
@@ -507,28 +649,103 @@ const handshake = {
|
|
|
507
649
|
engine: "shroud",
|
|
508
650
|
version: engineVersion,
|
|
509
651
|
capabilities: [
|
|
652
|
+
"identify",
|
|
510
653
|
"obfuscate",
|
|
511
654
|
"deobfuscate",
|
|
512
655
|
"batch",
|
|
513
656
|
"stats",
|
|
514
657
|
"health",
|
|
515
658
|
"configure",
|
|
659
|
+
"security",
|
|
660
|
+
"tool_call",
|
|
661
|
+
"tool_result",
|
|
516
662
|
"audit",
|
|
517
663
|
"partitions",
|
|
518
664
|
],
|
|
665
|
+
security: null,
|
|
519
666
|
};
|
|
520
667
|
|
|
521
668
|
process.stderr.write(`[app-server] Starting APP server v${engineVersion}\n`);
|
|
522
|
-
process.stdout.write(JSON.stringify(handshake) + "\n");
|
|
523
669
|
|
|
524
670
|
// ---------------------------------------------------------------------------
|
|
525
|
-
//
|
|
671
|
+
// Socket listener mode (--listen <path>)
|
|
526
672
|
// ---------------------------------------------------------------------------
|
|
527
673
|
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
674
|
+
const listenFlag = process.argv.indexOf("--listen");
|
|
675
|
+
const SOCKET_PATH = listenFlag !== -1 ? (process.argv[listenFlag + 1] || "/tmp/shroud-app.sock") : null;
|
|
676
|
+
|
|
677
|
+
if (SOCKET_PATH) {
|
|
678
|
+
// Remove stale socket file
|
|
679
|
+
try { unlinkSync(SOCKET_PATH); } catch {}
|
|
680
|
+
|
|
681
|
+
let socketClients = 0;
|
|
682
|
+
const socketServer = createNetServer((conn) => {
|
|
683
|
+
socketClients++;
|
|
684
|
+
const clientId = socketClients;
|
|
685
|
+
process.stderr.write(`[app-server] Socket client #${clientId} connected\n`);
|
|
686
|
+
|
|
687
|
+
// Send handshake to this client
|
|
688
|
+
conn.write(JSON.stringify(handshake) + "\n");
|
|
689
|
+
|
|
690
|
+
let connected = true;
|
|
691
|
+
const connRl = createInterface({ input: conn, crlfDelay: Infinity });
|
|
692
|
+
const connWrite = (s) => { if (connected) try { conn.write(s); } catch {} };
|
|
693
|
+
|
|
694
|
+
connRl.on("line", (line) => dispatch(line, connWrite));
|
|
695
|
+
connRl.on("error", () => {});
|
|
696
|
+
conn.on("error", () => { connected = false; });
|
|
697
|
+
conn.on("close", () => {
|
|
698
|
+
connected = false;
|
|
699
|
+
process.stderr.write(`[app-server] Socket client #${clientId} disconnected\n`);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
socketServer.listen(SOCKET_PATH, () => {
|
|
704
|
+
process.stderr.write(`[app-server] Listening on socket: ${SOCKET_PATH}\n`);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
socketServer.on("error", (err) => {
|
|
708
|
+
process.stderr.write(`[app-server] Socket error: ${err.message}\n`);
|
|
709
|
+
process.exit(1);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Also still serve stdin for backwards compat
|
|
713
|
+
process.stdout.write(JSON.stringify(handshake) + "\n");
|
|
714
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
715
|
+
rl.on("line", (line) => dispatch(line));
|
|
716
|
+
rl.on("close", () => {
|
|
717
|
+
try { unlinkSync(SOCKET_PATH); } catch {}
|
|
718
|
+
socketServer.close();
|
|
719
|
+
clearInterval(heartbeatInterval);
|
|
720
|
+
dumpStats();
|
|
721
|
+
dumpSessionFile();
|
|
722
|
+
process.exit(0);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Cleanup socket on exit
|
|
726
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
727
|
+
process.on(sig, () => {
|
|
728
|
+
try { unlinkSync(SOCKET_PATH); } catch {}
|
|
729
|
+
socketServer.close();
|
|
730
|
+
clearInterval(heartbeatInterval);
|
|
731
|
+
dumpStats();
|
|
732
|
+
dumpSessionFile();
|
|
733
|
+
process.exit(0);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
// Default: stdin/stdout mode
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
|
|
741
|
+
process.stdout.write(JSON.stringify(handshake) + "\n");
|
|
742
|
+
|
|
743
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
744
|
+
rl.on("line", (line) => dispatch(line));
|
|
745
|
+
rl.on("close", () => {
|
|
746
|
+
clearInterval(heartbeatInterval);
|
|
747
|
+
dumpStats();
|
|
748
|
+
dumpSessionFile();
|
|
749
|
+
process.exit(0);
|
|
750
|
+
});
|
|
751
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.anthropic.com/schemas/claude-code-hooks.json",
|
|
3
|
+
"_comment": "Add this 'hooks' block to your .claude/settings.json or .claude/settings.local.json",
|
|
4
|
+
"hooks": {
|
|
5
|
+
"PreToolUse": [
|
|
6
|
+
{
|
|
7
|
+
"matcher": "Read|Bash|Grep|Glob|Write|Edit|NotebookEdit|WebFetch|WebSearch",
|
|
8
|
+
"hooks": [
|
|
9
|
+
{
|
|
10
|
+
"type": "http",
|
|
11
|
+
"url": "http://127.0.0.1:17380/pre-tool-use",
|
|
12
|
+
"timeout": 5000,
|
|
13
|
+
"statusMessage": "Shroud: scanning tool call"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"PostToolUse": [
|
|
19
|
+
{
|
|
20
|
+
"matcher": "Read|Bash|Grep|Glob|WebFetch|WebSearch",
|
|
21
|
+
"hooks": [
|
|
22
|
+
{
|
|
23
|
+
"type": "http",
|
|
24
|
+
"url": "http://127.0.0.1:17380/post-tool-use",
|
|
25
|
+
"timeout": 5000,
|
|
26
|
+
"statusMessage": "Shroud: obfuscating output"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|