forkoff 1.0.17 → 1.0.19
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/LICENSE +11 -7
- package/README.md +77 -118
- package/dist/approval.d.ts +1 -0
- package/dist/approval.js +9 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +62 -16
- package/dist/crypto/e2eeManager.d.ts +49 -52
- package/dist/crypto/e2eeManager.js +256 -181
- package/dist/crypto/encryption.d.ts +8 -10
- package/dist/crypto/encryption.js +29 -94
- package/dist/crypto/index.d.ts +10 -0
- package/dist/crypto/index.js +22 -0
- package/dist/crypto/keyExchange.d.ts +6 -20
- package/dist/crypto/keyExchange.js +18 -110
- package/dist/crypto/keyGeneration.d.ts +2 -13
- package/dist/crypto/keyGeneration.js +14 -88
- package/dist/crypto/keyStorage.d.ts +32 -5
- package/dist/crypto/keyStorage.js +152 -8
- package/dist/crypto/sessionPersistence.d.ts +7 -13
- package/dist/crypto/sessionPersistence.js +108 -33
- package/dist/crypto/types.d.ts +24 -3
- package/dist/crypto/types.js +2 -1
- package/dist/crypto/websocketE2EE.d.ts +6 -17
- package/dist/crypto/websocketE2EE.js +21 -38
- package/dist/index.js +203 -280
- package/dist/integration.d.ts +0 -1
- package/dist/integration.js +2 -4
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +209 -1
- package/dist/server.d.ts +30 -0
- package/dist/server.js +162 -0
- package/dist/startup.js +15 -6
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +94 -1
- package/dist/tools/claude-process.d.ts +8 -0
- package/dist/tools/claude-process.js +199 -26
- package/dist/tools/claude-sessions.d.ts +1 -0
- package/dist/tools/claude-sessions.js +36 -10
- package/dist/tools/detector.js +11 -3
- package/dist/tools/permission-hook.js +94 -27
- package/dist/tools/permission-ipc.d.ts +1 -0
- package/dist/tools/permission-ipc.js +61 -14
- package/dist/transcript-streamer.d.ts +1 -0
- package/dist/transcript-streamer.js +18 -4
- package/dist/usage-tracker.d.ts +45 -0
- package/dist/usage-tracker.js +243 -0
- package/dist/websocket.d.ts +43 -12
- package/dist/websocket.js +418 -214
- package/package.json +5 -4
- package/dist/__tests__/cli-commands.test.d.ts +0 -6
- package/dist/__tests__/cli-commands.test.d.ts.map +0 -1
- package/dist/__tests__/cli-commands.test.js +0 -213
- package/dist/__tests__/cli-commands.test.js.map +0 -1
- package/dist/__tests__/crypto/e2e-integration.test.d.ts +0 -17
- package/dist/__tests__/crypto/e2e-integration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/e2e-integration.test.js +0 -338
- package/dist/__tests__/crypto/e2e-integration.test.js.map +0 -1
- package/dist/__tests__/crypto/e2eeManager.test.d.ts +0 -2
- package/dist/__tests__/crypto/e2eeManager.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/e2eeManager.test.js +0 -242
- package/dist/__tests__/crypto/e2eeManager.test.js.map +0 -1
- package/dist/__tests__/crypto/encryption.test.d.ts +0 -2
- package/dist/__tests__/crypto/encryption.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/encryption.test.js +0 -116
- package/dist/__tests__/crypto/encryption.test.js.map +0 -1
- package/dist/__tests__/crypto/keyExchange.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyExchange.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyExchange.test.js +0 -84
- package/dist/__tests__/crypto/keyExchange.test.js.map +0 -1
- package/dist/__tests__/crypto/keyGeneration.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyGeneration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyGeneration.test.js +0 -61
- package/dist/__tests__/crypto/keyGeneration.test.js.map +0 -1
- package/dist/__tests__/crypto/keyStorage.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyStorage.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyStorage.test.js +0 -133
- package/dist/__tests__/crypto/keyStorage.test.js.map +0 -1
- package/dist/__tests__/crypto/websocketIntegration.test.d.ts +0 -2
- package/dist/__tests__/crypto/websocketIntegration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/websocketIntegration.test.js +0 -259
- package/dist/__tests__/crypto/websocketIntegration.test.js.map +0 -1
- package/dist/__tests__/startup.test.d.ts +0 -11
- package/dist/__tests__/startup.test.d.ts.map +0 -1
- package/dist/__tests__/startup.test.js +0 -241
- package/dist/__tests__/startup.test.js.map +0 -1
- package/dist/__tests__/tools/claude-process.test.d.ts +0 -8
- package/dist/__tests__/tools/claude-process.test.d.ts.map +0 -1
- package/dist/__tests__/tools/claude-process.test.js +0 -430
- package/dist/__tests__/tools/claude-process.test.js.map +0 -1
- package/dist/__tests__/tools/permission-hook.test.d.ts +0 -17
- package/dist/__tests__/tools/permission-hook.test.d.ts.map +0 -1
- package/dist/__tests__/tools/permission-hook.test.js +0 -616
- package/dist/__tests__/tools/permission-hook.test.js.map +0 -1
- package/dist/__tests__/tools/permission-ipc.test.d.ts +0 -11
- package/dist/__tests__/tools/permission-ipc.test.d.ts.map +0 -1
- package/dist/__tests__/tools/permission-ipc.test.js +0 -612
- package/dist/__tests__/tools/permission-ipc.test.js.map +0 -1
- package/dist/__tests__/websocket.test.d.ts +0 -13
- package/dist/__tests__/websocket.test.d.ts.map +0 -1
- package/dist/__tests__/websocket.test.js +0 -204
- package/dist/__tests__/websocket.test.js.map +0 -1
- package/dist/api.d.ts +0 -44
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -76
- package/dist/api.js.map +0 -1
- package/dist/approval.d.ts.map +0 -1
- package/dist/approval.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/crypto/e2eeManager.d.ts.map +0 -1
- package/dist/crypto/e2eeManager.js.map +0 -1
- package/dist/crypto/encryption.d.ts.map +0 -1
- package/dist/crypto/encryption.js.map +0 -1
- package/dist/crypto/keyExchange.d.ts.map +0 -1
- package/dist/crypto/keyExchange.js.map +0 -1
- package/dist/crypto/keyGeneration.d.ts.map +0 -1
- package/dist/crypto/keyGeneration.js.map +0 -1
- package/dist/crypto/keyStorage.d.ts.map +0 -1
- package/dist/crypto/keyStorage.js.map +0 -1
- package/dist/crypto/sessionPersistence.d.ts.map +0 -1
- package/dist/crypto/sessionPersistence.js.map +0 -1
- package/dist/crypto/types.d.ts.map +0 -1
- package/dist/crypto/types.js.map +0 -1
- package/dist/crypto/websocketE2EE.d.ts.map +0 -1
- package/dist/crypto/websocketE2EE.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integration.d.ts.map +0 -1
- package/dist/integration.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/startup.d.ts.map +0 -1
- package/dist/startup.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/tools/__tests__/claude-sessions.test.d.ts +0 -2
- package/dist/tools/__tests__/claude-sessions.test.d.ts.map +0 -1
- package/dist/tools/__tests__/claude-sessions.test.js +0 -306
- package/dist/tools/__tests__/claude-sessions.test.js.map +0 -1
- package/dist/tools/claude-hooks.d.ts.map +0 -1
- package/dist/tools/claude-hooks.js.map +0 -1
- package/dist/tools/claude-process.d.ts.map +0 -1
- package/dist/tools/claude-process.js.map +0 -1
- package/dist/tools/claude-sessions.d.ts.map +0 -1
- package/dist/tools/claude-sessions.js.map +0 -1
- package/dist/tools/detector.d.ts.map +0 -1
- package/dist/tools/detector.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/permission-hook.d.ts.map +0 -1
- package/dist/tools/permission-hook.js.map +0 -1
- package/dist/tools/permission-ipc.d.ts.map +0 -1
- package/dist/tools/permission-ipc.js.map +0 -1
- package/dist/transcript-streamer.d.ts.map +0 -1
- package/dist/transcript-streamer.js.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/dist/websocket.js.map +0 -1
- package/jest.config.js +0 -18
|
@@ -52,6 +52,8 @@ interface ClaudeApprovalRequest {
|
|
|
52
52
|
context: string[];
|
|
53
53
|
options: string[];
|
|
54
54
|
promptText: string;
|
|
55
|
+
toolName?: string;
|
|
56
|
+
toolInput?: any;
|
|
55
57
|
}
|
|
56
58
|
/** SDK message structure received from Claude CLI JSONL output */
|
|
57
59
|
interface SdkMessage {
|
|
@@ -122,12 +124,15 @@ interface ToolActivityEvent {
|
|
|
122
124
|
inputSummary: string;
|
|
123
125
|
}
|
|
124
126
|
declare class ClaudeProcessManager extends EventEmitter {
|
|
127
|
+
private static readonly MAX_ACTIVE_PROCESSES;
|
|
128
|
+
private static readonly MAX_PENDING_APPROVALS;
|
|
125
129
|
private processes;
|
|
126
130
|
private pendingApprovals;
|
|
127
131
|
private readonly APPROVAL_TIMEOUT_MS;
|
|
128
132
|
private readonly MAX_OUTPUT_BUFFER_LINES;
|
|
129
133
|
/** Track closed sessions for auto-restart */
|
|
130
134
|
private closedSessions;
|
|
135
|
+
private static readonly MAX_CLOSED_SESSIONS;
|
|
131
136
|
/** Maximum number of auto-restarts per session to prevent chaos */
|
|
132
137
|
private readonly MAX_AUTO_RESTARTS;
|
|
133
138
|
/** Permission IPC managers per session */
|
|
@@ -138,6 +143,7 @@ declare class ClaudeProcessManager extends EventEmitter {
|
|
|
138
143
|
private hookConfiguredDirs;
|
|
139
144
|
/** Track sessions that mobile has explicitly taken over (via claude_resume_session) */
|
|
140
145
|
private takenOverSessions;
|
|
146
|
+
private static readonly MAX_TAKEN_OVER_SESSIONS;
|
|
141
147
|
/** Type-safe emit for known events */
|
|
142
148
|
emit<K extends keyof ClaudeProcessManagerEvents>(event: K, ...args: ClaudeProcessManagerEvents[K]): boolean;
|
|
143
149
|
/** Type-safe on for known events */
|
|
@@ -207,6 +213,8 @@ declare class ClaudeProcessManager extends EventEmitter {
|
|
|
207
213
|
sessionKey?: string;
|
|
208
214
|
directory: string;
|
|
209
215
|
}>;
|
|
216
|
+
/** Enforce cap on active processes to prevent resource exhaustion */
|
|
217
|
+
private enforceProcessCap;
|
|
210
218
|
/**
|
|
211
219
|
* Clean up old closed session entries to prevent memory leaks.
|
|
212
220
|
* Sessions older than 1 hour are removed.
|
|
@@ -47,6 +47,94 @@ const os = __importStar(require("os"));
|
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const fs = __importStar(require("fs"));
|
|
49
49
|
const permission_ipc_1 = require("./permission-ipc");
|
|
50
|
+
/**
|
|
51
|
+
* Returns a filtered copy of process.env with sensitive variables removed.
|
|
52
|
+
* Prevents accidental leakage of credentials to Claude child processes.
|
|
53
|
+
*/
|
|
54
|
+
function getSafeEnv() {
|
|
55
|
+
const sensitivePatterns = [
|
|
56
|
+
/^AWS_/i,
|
|
57
|
+
/^AZURE_/i,
|
|
58
|
+
/^GCP_/i,
|
|
59
|
+
/^GOOGLE_/i,
|
|
60
|
+
/SECRET/i,
|
|
61
|
+
/PASSWORD/i,
|
|
62
|
+
/PRIVATE_KEY/i,
|
|
63
|
+
/^SUPABASE_SERVICE/i,
|
|
64
|
+
/^DATABASE_URL$/i,
|
|
65
|
+
/^ADMIN_API_KEY$/i,
|
|
66
|
+
// Prevent code injection via environment variables
|
|
67
|
+
/^NODE_OPTIONS$/i,
|
|
68
|
+
/^NODE_EXTRA_CA_CERTS$/i,
|
|
69
|
+
/^LD_PRELOAD$/i,
|
|
70
|
+
/^LD_LIBRARY_PATH$/i,
|
|
71
|
+
/^DYLD_INSERT_LIBRARIES$/i,
|
|
72
|
+
/^DYLD_LIBRARY_PATH$/i,
|
|
73
|
+
/^ELECTRON_RUN_AS_NODE$/i,
|
|
74
|
+
// Language-specific code injection vectors
|
|
75
|
+
/^PYTHONPATH$/i,
|
|
76
|
+
/^PYTHONSTARTUP$/i,
|
|
77
|
+
/^RUBYLIB$/i,
|
|
78
|
+
/^PERL5LIB$/i,
|
|
79
|
+
/^PERL5OPT$/i,
|
|
80
|
+
/^JAVA_TOOL_OPTIONS$/i,
|
|
81
|
+
/^_JAVA_OPTIONS$/i,
|
|
82
|
+
// Git/SSH injection
|
|
83
|
+
/^GIT_SSH_COMMAND$/i,
|
|
84
|
+
/^GIT_EXEC_PATH$/i,
|
|
85
|
+
// Pager/editor injection
|
|
86
|
+
/^LESSOPEN$/i,
|
|
87
|
+
/^LESSCLOSE$/i,
|
|
88
|
+
// Shell startup injection
|
|
89
|
+
/^BASH_ENV$/i,
|
|
90
|
+
/^ENV$/i,
|
|
91
|
+
/^PROMPT_COMMAND$/i,
|
|
92
|
+
/^SHELLOPTS$/i,
|
|
93
|
+
// Field separator injection
|
|
94
|
+
/^IFS$/i,
|
|
95
|
+
// Editor/browser auto-launch injection
|
|
96
|
+
/^EDITOR$/i,
|
|
97
|
+
/^VISUAL$/i,
|
|
98
|
+
/^BROWSER$/i,
|
|
99
|
+
// Proxy injection (MITM child process HTTP traffic)
|
|
100
|
+
/^HTTPS?_PROXY$/i,
|
|
101
|
+
/^ALL_PROXY$/i,
|
|
102
|
+
/^NO_PROXY$/i,
|
|
103
|
+
// TLS verification bypass
|
|
104
|
+
/^SSL_CERT_FILE$/i,
|
|
105
|
+
/^SSL_CERT_DIR$/i,
|
|
106
|
+
/^NODE_TLS_REJECT_UNAUTHORIZED$/i,
|
|
107
|
+
// npm config injection
|
|
108
|
+
/^npm_config_/i,
|
|
109
|
+
// Pager injection (git, man, etc.)
|
|
110
|
+
/^PAGER$/i,
|
|
111
|
+
// Zsh startup injection
|
|
112
|
+
/^ZDOTDIR$/i,
|
|
113
|
+
// Curl config injection
|
|
114
|
+
/^CURL_HOME$/i,
|
|
115
|
+
// Third-party API keys (defense-in-depth for child processes)
|
|
116
|
+
/^OPENAI_/i,
|
|
117
|
+
/^ANTHROPIC_/i,
|
|
118
|
+
/^GITHUB_TOKEN$/i,
|
|
119
|
+
/^GITLAB_TOKEN$/i,
|
|
120
|
+
/^NPM_TOKEN$/i,
|
|
121
|
+
/^DOCKER_PASSWORD$/i,
|
|
122
|
+
/^SLACK_TOKEN$/i,
|
|
123
|
+
/^SLACK_BOT_TOKEN$/i,
|
|
124
|
+
/^SENDGRID_/i,
|
|
125
|
+
/^TWILIO_/i,
|
|
126
|
+
/^DATADOG_/i,
|
|
127
|
+
/TOKEN$/i,
|
|
128
|
+
/API_KEY$/i,
|
|
129
|
+
];
|
|
130
|
+
const filtered = {};
|
|
131
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
132
|
+
if (!sensitivePatterns.some(pattern => pattern.test(key))) {
|
|
133
|
+
filtered[key] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return filtered;
|
|
137
|
+
}
|
|
50
138
|
/**
|
|
51
139
|
* Regular expression patterns used to detect approval prompts in Claude CLI output.
|
|
52
140
|
* When any of these patterns match the output, an approval request is triggered
|
|
@@ -161,10 +249,11 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
161
249
|
// SECURITY: Using cross-spawn instead of shell: true to prevent command injection
|
|
162
250
|
const proc = (0, cross_spawn_1.default)('claude', args, {
|
|
163
251
|
cwd: resolvedDir,
|
|
164
|
-
env: { ...
|
|
252
|
+
env: { ...getSafeEnv(), TERM: 'xterm-256color' },
|
|
165
253
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
166
254
|
});
|
|
167
255
|
this.setupProcessHandlers(terminalSessionId, proc, resolvedDir);
|
|
256
|
+
this.enforceProcessCap();
|
|
168
257
|
this.processes.set(terminalSessionId, { terminalSessionId, process: proc, directory: resolvedDir, outputBuffer: [], wasAutoRestarted: false, dangerouslySkipPermissions: !!dangerouslySkipPermissions, interactivePermissions: !!interactivePermissions });
|
|
169
258
|
return { cwd: resolvedDir };
|
|
170
259
|
}
|
|
@@ -193,14 +282,15 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
193
282
|
// Start IPC manager to bridge hook ↔ WebSocket
|
|
194
283
|
this.startPermissionIpc(terminalSessionId, sessionKey);
|
|
195
284
|
}
|
|
196
|
-
console.log(`[Claude Process] Spawning: claude ${args.
|
|
285
|
+
console.log(`[Claude Process] Spawning: claude (${args.length} args)`);
|
|
197
286
|
// SECURITY: Using cross-spawn instead of shell: true to prevent command injection
|
|
198
287
|
const proc = (0, cross_spawn_1.default)('claude', args, {
|
|
199
288
|
cwd: resolvedDir,
|
|
200
|
-
env: { ...
|
|
289
|
+
env: { ...getSafeEnv(), TERM: 'xterm-256color' },
|
|
201
290
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
202
291
|
});
|
|
203
292
|
this.setupProcessHandlers(terminalSessionId, proc, resolvedDir, sessionKey);
|
|
293
|
+
this.enforceProcessCap();
|
|
204
294
|
this.processes.set(terminalSessionId, { terminalSessionId, process: proc, directory: resolvedDir, sessionKey, outputBuffer: [], wasAutoRestarted: false, dangerouslySkipPermissions: !!dangerouslySkipPermissions, interactivePermissions: !!interactivePermissions });
|
|
205
295
|
// Store session info for future message sends (needed since we spawn fresh process per message)
|
|
206
296
|
this.closedSessions.set(terminalSessionId, {
|
|
@@ -278,7 +368,7 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
278
368
|
return false;
|
|
279
369
|
}
|
|
280
370
|
// Write message to stdin IMMEDIATELY - no waiting
|
|
281
|
-
console.log(`[Claude Process] Sending JSONL immediately
|
|
371
|
+
console.log(`[Claude Process] Sending JSONL immediately (${jsonLine.length} chars)`);
|
|
282
372
|
return new Promise((resolve) => {
|
|
283
373
|
try {
|
|
284
374
|
info.process.stdin.write(jsonLine, (err) => {
|
|
@@ -320,10 +410,11 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
320
410
|
}
|
|
321
411
|
const proc = (0, cross_spawn_1.default)('claude', args, {
|
|
322
412
|
cwd: resolvedDir,
|
|
323
|
-
env: { ...
|
|
413
|
+
env: { ...getSafeEnv(), TERM: 'xterm-256color' },
|
|
324
414
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
325
415
|
});
|
|
326
416
|
this.setupProcessHandlers(terminalSessionId, proc, resolvedDir);
|
|
417
|
+
this.enforceProcessCap();
|
|
327
418
|
this.processes.set(terminalSessionId, {
|
|
328
419
|
terminalSessionId,
|
|
329
420
|
process: proc,
|
|
@@ -364,7 +455,7 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
364
455
|
resolve(false);
|
|
365
456
|
}
|
|
366
457
|
else {
|
|
367
|
-
console.log(`[Claude Process] Initial message written to new session
|
|
458
|
+
console.log(`[Claude Process] Initial message written to new session`);
|
|
368
459
|
resolve(true);
|
|
369
460
|
}
|
|
370
461
|
});
|
|
@@ -393,13 +484,14 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
393
484
|
* Used when mobile opens a session view - we store the info so we can spawn later on first message.
|
|
394
485
|
*/
|
|
395
486
|
registerSession(sessionKey, directory, terminalSessionId, dangerouslySkipPermissions, interactivePermissions, isRealSession) {
|
|
487
|
+
this.cleanupOldClosedSessions();
|
|
396
488
|
// If we already captured a real Claude session key (from SDK output) for this
|
|
397
489
|
// terminalSessionId, preserve it. This prevents reconnects from overwriting
|
|
398
490
|
// the real key with the mobile-generated one (e.g. brainstorm-*).
|
|
399
491
|
const existing = this.closedSessions.get(terminalSessionId);
|
|
400
492
|
const effectiveSessionKey = (existing?.isRealSession && existing.sessionKey) ? existing.sessionKey : sessionKey;
|
|
401
493
|
const effectiveIsReal = (existing?.isRealSession) ? true : isRealSession;
|
|
402
|
-
console.log(`[Claude Process] Registering session: ${effectiveSessionKey}
|
|
494
|
+
console.log(`[Claude Process] Registering session: ${effectiveSessionKey}${dangerouslySkipPermissions ? ' (unrestricted)' : ''}${interactivePermissions ? ' (interactive)' : ''}${effectiveIsReal === false ? ' (fresh — no real Claude session)' : ''}`);
|
|
403
495
|
this.closedSessions.set(terminalSessionId, {
|
|
404
496
|
sessionKey: effectiveSessionKey,
|
|
405
497
|
directory,
|
|
@@ -416,10 +508,16 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
416
508
|
*/
|
|
417
509
|
setupProcessHandlers(terminalSessionId, proc, directory, sessionKey) {
|
|
418
510
|
// Buffer for incomplete JSONL lines
|
|
511
|
+
const MAX_LINE_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
|
|
419
512
|
let jsonLineBuffer = '';
|
|
420
513
|
proc.stdout?.on('data', (data) => {
|
|
421
514
|
const rawOutput = data.toString();
|
|
422
515
|
jsonLineBuffer += rawOutput;
|
|
516
|
+
// Prevent unbounded buffer growth from missing newlines
|
|
517
|
+
if (jsonLineBuffer.length > MAX_LINE_BUFFER_SIZE) {
|
|
518
|
+
console.warn(`[Claude] JSONL line buffer exceeded ${MAX_LINE_BUFFER_SIZE} bytes, resetting`);
|
|
519
|
+
jsonLineBuffer = '';
|
|
520
|
+
}
|
|
423
521
|
// Update output buffer for approval context
|
|
424
522
|
const processInfo = this.processes.get(terminalSessionId);
|
|
425
523
|
if (processInfo) {
|
|
@@ -448,7 +546,7 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
448
546
|
if (message.type === 'result') {
|
|
449
547
|
console.log(`[Claude Process] Received result message - turn complete. Subtype: ${message.subtype}, Cost: $${message.cost_usd || 'unknown'}`);
|
|
450
548
|
if (message.is_error) {
|
|
451
|
-
console.log(`[Claude Process] Result indicates error
|
|
549
|
+
console.log(`[Claude Process] Result indicates error`);
|
|
452
550
|
}
|
|
453
551
|
// Capture session_id from result so future sendInput() can --resume
|
|
454
552
|
if (message.session_id && processInfo && !processInfo.sessionKey) {
|
|
@@ -482,7 +580,7 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
482
580
|
}
|
|
483
581
|
catch (e) {
|
|
484
582
|
// Non-JSON output (shouldn't happen with SDK flags, but log it)
|
|
485
|
-
console.log(`[Claude Process] Non-JSON stdout
|
|
583
|
+
console.log(`[Claude Process] Non-JSON stdout (${line.length} chars)`);
|
|
486
584
|
}
|
|
487
585
|
}
|
|
488
586
|
}
|
|
@@ -584,14 +682,28 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
584
682
|
* SECURITY: Validates path doesn't contain dangerous characters
|
|
585
683
|
*/
|
|
586
684
|
resolvePath(dir) {
|
|
587
|
-
// SECURITY: Reject paths with shell metacharacters
|
|
588
|
-
if (/[;&|`$()
|
|
685
|
+
// SECURITY: Reject paths with shell metacharacters or control characters
|
|
686
|
+
if (/[;&|`$()<>\n\r\0]/.test(dir)) {
|
|
589
687
|
throw new Error('Invalid directory path: contains disallowed characters');
|
|
590
688
|
}
|
|
689
|
+
let resolved;
|
|
591
690
|
if (dir === '~' || dir.startsWith('~/')) {
|
|
592
|
-
|
|
691
|
+
resolved = dir === '~' ? os.homedir() : dir.replace('~', os.homedir());
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
resolved = path.resolve(dir);
|
|
593
695
|
}
|
|
594
|
-
|
|
696
|
+
// SECURITY: Prevent path traversal — resolved path must be under home directory
|
|
697
|
+
const homeDir = os.homedir();
|
|
698
|
+
const normalized = path.normalize(resolved);
|
|
699
|
+
// On Windows, paths are case-insensitive — use lowercase comparison
|
|
700
|
+
const isUnderHome = os.platform() === 'win32'
|
|
701
|
+
? normalized.toLowerCase().startsWith(homeDir.toLowerCase())
|
|
702
|
+
: normalized.startsWith(homeDir);
|
|
703
|
+
if (!isUnderHome) {
|
|
704
|
+
throw new Error('Invalid directory path: path traversal detected (must be under home directory)');
|
|
705
|
+
}
|
|
706
|
+
return normalized;
|
|
595
707
|
}
|
|
596
708
|
/**
|
|
597
709
|
* Kill a Claude process
|
|
@@ -618,6 +730,25 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
618
730
|
directory: info.directory,
|
|
619
731
|
}));
|
|
620
732
|
}
|
|
733
|
+
/** Enforce cap on active processes to prevent resource exhaustion */
|
|
734
|
+
enforceProcessCap() {
|
|
735
|
+
while (this.processes.size >= ClaudeProcessManager.MAX_ACTIVE_PROCESSES) {
|
|
736
|
+
const oldestKey = this.processes.keys().next().value;
|
|
737
|
+
if (oldestKey) {
|
|
738
|
+
const oldProcess = this.processes.get(oldestKey);
|
|
739
|
+
if (oldProcess?.process) {
|
|
740
|
+
try {
|
|
741
|
+
oldProcess.process.kill();
|
|
742
|
+
}
|
|
743
|
+
catch { /* best effort */ }
|
|
744
|
+
}
|
|
745
|
+
this.processes.delete(oldestKey);
|
|
746
|
+
console.warn(`[Claude Process] MAX_ACTIVE_PROCESSES (${ClaudeProcessManager.MAX_ACTIVE_PROCESSES}) reached, killed: ${oldestKey}`);
|
|
747
|
+
}
|
|
748
|
+
else
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
621
752
|
/**
|
|
622
753
|
* Clean up old closed session entries to prevent memory leaks.
|
|
623
754
|
* Sessions older than 1 hour are removed.
|
|
@@ -632,6 +763,16 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
632
763
|
cleanedCount++;
|
|
633
764
|
}
|
|
634
765
|
}
|
|
766
|
+
// Also enforce hard cap with FIFO eviction
|
|
767
|
+
while (this.closedSessions.size > ClaudeProcessManager.MAX_CLOSED_SESSIONS) {
|
|
768
|
+
const oldestKey = this.closedSessions.keys().next().value;
|
|
769
|
+
if (oldestKey) {
|
|
770
|
+
this.closedSessions.delete(oldestKey);
|
|
771
|
+
cleanedCount++;
|
|
772
|
+
}
|
|
773
|
+
else
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
635
776
|
if (cleanedCount > 0) {
|
|
636
777
|
console.log(`[Claude Process] Cleaned up ${cleanedCount} old closed session(s)`);
|
|
637
778
|
}
|
|
@@ -651,6 +792,14 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
651
792
|
* Mark a session as taken over by the mobile user.
|
|
652
793
|
*/
|
|
653
794
|
markTakenOver(terminalSessionId) {
|
|
795
|
+
// Evict oldest if at cap (FIFO — Set iteration order is insertion order)
|
|
796
|
+
if (this.takenOverSessions.size >= ClaudeProcessManager.MAX_TAKEN_OVER_SESSIONS) {
|
|
797
|
+
const oldest = this.takenOverSessions.values().next().value;
|
|
798
|
+
if (oldest) {
|
|
799
|
+
this.takenOverSessions.delete(oldest);
|
|
800
|
+
console.warn(`[Claude Process] MAX_TAKEN_OVER_SESSIONS (${ClaudeProcessManager.MAX_TAKEN_OVER_SESSIONS}) reached, evicted: ${oldest}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
654
803
|
this.takenOverSessions.add(terminalSessionId);
|
|
655
804
|
console.log(`[Claude Process] Session marked as taken over: ${terminalSessionId}`);
|
|
656
805
|
}
|
|
@@ -726,10 +875,25 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
726
875
|
const rulesFile = path.join(rulesDir, 'rules.json');
|
|
727
876
|
try {
|
|
728
877
|
if (!fs.existsSync(rulesDir)) {
|
|
729
|
-
fs.mkdirSync(rulesDir, { recursive: true });
|
|
878
|
+
fs.mkdirSync(rulesDir, { recursive: true, mode: 0o700 });
|
|
730
879
|
}
|
|
731
|
-
|
|
732
|
-
|
|
880
|
+
else {
|
|
881
|
+
// SECURITY: Validate temp dir permissions aren't world-writable
|
|
882
|
+
// Skip on Windows — Unix permission bits aren't enforced and produce false positives
|
|
883
|
+
if (process.platform !== 'win32') {
|
|
884
|
+
const dirStat = fs.statSync(rulesDir);
|
|
885
|
+
const dirMode = dirStat.mode & 0o777;
|
|
886
|
+
if (dirMode & 0o022) { // group or other writable
|
|
887
|
+
console.error(`[Security] Temp dir has unsafe permissions (${dirMode.toString(8)}), refusing to write`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// SECURITY: Atomic write via temp file + rename to prevent TOCTOU
|
|
893
|
+
const tmpFile = rulesFile + '.tmp.' + process.pid;
|
|
894
|
+
fs.writeFileSync(tmpFile, JSON.stringify(rules, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
895
|
+
fs.renameSync(tmpFile, rulesFile);
|
|
896
|
+
console.log(`[Claude Process] Permission rules written`);
|
|
733
897
|
}
|
|
734
898
|
catch (err) {
|
|
735
899
|
console.error(`[Claude Process] Failed to write permission rules:`, err.message);
|
|
@@ -743,14 +907,14 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
743
907
|
const hookScriptPath = path.resolve(__dirname, 'permission-hook.js');
|
|
744
908
|
// Verify hook script exists (it should be compiled alongside this file)
|
|
745
909
|
if (!fs.existsSync(hookScriptPath)) {
|
|
746
|
-
console.log(`[Claude Process] Hook script not found
|
|
910
|
+
console.log(`[Claude Process] Hook script not found, skipping hook configuration`);
|
|
747
911
|
return;
|
|
748
912
|
}
|
|
749
913
|
const claudeDir = path.join(cwd, '.claude');
|
|
750
914
|
const settingsFile = path.join(claudeDir, 'settings.local.json');
|
|
751
915
|
try {
|
|
752
916
|
// Create .claude dir if needed
|
|
753
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
917
|
+
fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
|
|
754
918
|
// Read existing settings or start fresh
|
|
755
919
|
let settings = {};
|
|
756
920
|
if (fs.existsSync(settingsFile)) {
|
|
@@ -778,9 +942,9 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
778
942
|
}],
|
|
779
943
|
});
|
|
780
944
|
}
|
|
781
|
-
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), 'utf-8');
|
|
945
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
782
946
|
this.hookConfiguredDirs.add(cwd);
|
|
783
|
-
console.log(`[Claude Process] Hook configured
|
|
947
|
+
console.log(`[Claude Process] Hook configured`);
|
|
784
948
|
}
|
|
785
949
|
catch (err) {
|
|
786
950
|
console.log(`[Claude Process] Failed to configure hook: ${err.message}`);
|
|
@@ -811,10 +975,10 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
811
975
|
fs.unlinkSync(settingsFile);
|
|
812
976
|
}
|
|
813
977
|
else {
|
|
814
|
-
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), 'utf-8');
|
|
978
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
815
979
|
}
|
|
816
980
|
this.hookConfiguredDirs.delete(cwd);
|
|
817
|
-
console.log(`[Claude Process] Hook removed
|
|
981
|
+
console.log(`[Claude Process] Hook removed`);
|
|
818
982
|
}
|
|
819
983
|
catch (err) {
|
|
820
984
|
console.log(`[Claude Process] Failed to remove hook: ${err.message}`);
|
|
@@ -921,10 +1085,15 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
921
1085
|
inputSummary = `Pattern: ${toolInput.pattern}`;
|
|
922
1086
|
}
|
|
923
1087
|
else {
|
|
924
|
-
|
|
1088
|
+
try {
|
|
1089
|
+
inputSummary = JSON.stringify(toolInput).substring(0, 200);
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
inputSummary = '[unserializable input]';
|
|
1093
|
+
}
|
|
925
1094
|
}
|
|
926
1095
|
}
|
|
927
|
-
console.log(`[Claude Process] Tool activity: ${toolName} (${toolId})
|
|
1096
|
+
console.log(`[Claude Process] Tool activity: ${toolName} (${toolId})`);
|
|
928
1097
|
// Emit non-blocking tool_activity event (not an approval request)
|
|
929
1098
|
const toolActivity = {
|
|
930
1099
|
terminalSessionId,
|
|
@@ -984,7 +1153,7 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
984
1153
|
// SDK mode (stream-json) does not accept raw character input — writing 'y' or 'n'
|
|
985
1154
|
// to stdin would be interpreted as a malformed JSONL message, not an approval response.
|
|
986
1155
|
// The tool has already executed by the time the user sees the notification.
|
|
987
|
-
console.log(`[Claude Process] Approval response
|
|
1156
|
+
console.log(`[Claude Process] Approval response for ${approvalId} ignored — SDK mode processes don't accept raw stdin approval characters`);
|
|
988
1157
|
}
|
|
989
1158
|
/**
|
|
990
1159
|
* Retrieves a pending approval request for a specific terminal session.
|
|
@@ -1117,7 +1286,7 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
1117
1286
|
status: 'pending',
|
|
1118
1287
|
activeForm: input.activeForm,
|
|
1119
1288
|
};
|
|
1120
|
-
console.log(`[Claude Process] Task created: ${task.
|
|
1289
|
+
console.log(`[Claude Process] Task created: ${task.id || 'unknown'}`);
|
|
1121
1290
|
this.emit('task_progress', {
|
|
1122
1291
|
terminalSessionId,
|
|
1123
1292
|
sessionKey,
|
|
@@ -1181,6 +1350,10 @@ class ClaudeProcessManager extends events_1.EventEmitter {
|
|
|
1181
1350
|
}
|
|
1182
1351
|
}
|
|
1183
1352
|
exports.ClaudeProcessManager = ClaudeProcessManager;
|
|
1353
|
+
ClaudeProcessManager.MAX_ACTIVE_PROCESSES = 20;
|
|
1354
|
+
ClaudeProcessManager.MAX_PENDING_APPROVALS = 50;
|
|
1355
|
+
ClaudeProcessManager.MAX_CLOSED_SESSIONS = 200;
|
|
1356
|
+
ClaudeProcessManager.MAX_TAKEN_OVER_SESSIONS = 100;
|
|
1184
1357
|
exports.claudeProcessManager = new ClaudeProcessManager();
|
|
1185
1358
|
exports.default = exports.claudeProcessManager;
|
|
1186
1359
|
//# sourceMappingURL=claude-process.js.map
|
|
@@ -146,7 +146,7 @@ class ClaudeSessionDetector extends events_1.EventEmitter {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
catch (error) {
|
|
149
|
-
console.error('[ClaudeSessionDetector] Error scanning sessions:', error);
|
|
149
|
+
console.error('[ClaudeSessionDetector] Error scanning sessions:', error.message);
|
|
150
150
|
}
|
|
151
151
|
// Sort by lastUsedAt descending
|
|
152
152
|
sessions.sort((a, b) => new Date(b.lastUsedAt).getTime() - new Date(a.lastUsedAt).getTime());
|
|
@@ -164,22 +164,48 @@ class ClaudeSessionDetector extends events_1.EventEmitter {
|
|
|
164
164
|
// We must validate against the filesystem to preserve literal hyphens
|
|
165
165
|
// in directory names (e.g. Dev-SharbelAS should NOT become Dev\SharbelAS)
|
|
166
166
|
let directory = this.decodeProjectDir(projectDir);
|
|
167
|
-
//
|
|
167
|
+
// Read transcript lines to get session ID and name
|
|
168
168
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
169
|
-
const
|
|
169
|
+
const lines = content.split('\n');
|
|
170
170
|
let sessionId = fileName;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
171
|
+
let name;
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
if (!line.trim())
|
|
174
|
+
continue;
|
|
175
|
+
try {
|
|
176
|
+
const entry = JSON.parse(line);
|
|
177
|
+
// Extract session ID from first entry
|
|
178
|
+
if (entry.sessionId && sessionId === fileName) {
|
|
179
|
+
sessionId = entry.sessionId;
|
|
180
|
+
}
|
|
181
|
+
// Extract name from first human/user message
|
|
182
|
+
if (!name && entry.type === 'human' || !name && entry.role === 'user') {
|
|
183
|
+
const text = typeof entry.message === 'string'
|
|
184
|
+
? entry.message
|
|
185
|
+
: typeof entry.content === 'string'
|
|
186
|
+
? entry.content
|
|
187
|
+
: Array.isArray(entry.message?.content)
|
|
188
|
+
? entry.message.content.find((b) => b.type === 'text')?.text
|
|
189
|
+
: typeof entry.message?.content === 'string'
|
|
190
|
+
? entry.message.content
|
|
191
|
+
: null;
|
|
192
|
+
if (text) {
|
|
193
|
+
// Truncate to 120 chars for display
|
|
194
|
+
name = text.length > 120 ? text.substring(0, 120) + '...' : text;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Stop after finding both
|
|
198
|
+
if (sessionId !== fileName && name)
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
continue;
|
|
175
203
|
}
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
// Use filename as session ID
|
|
179
204
|
}
|
|
180
205
|
return {
|
|
181
206
|
sessionKey: sessionId,
|
|
182
207
|
directory,
|
|
208
|
+
name,
|
|
183
209
|
state: 'inactive', // Will be updated if Claude is running
|
|
184
210
|
lastUsedAt: stat.mtime.toISOString(),
|
|
185
211
|
transcriptPath: filePath,
|
package/dist/tools/detector.js
CHANGED
|
@@ -323,12 +323,15 @@ class ToolDetector {
|
|
|
323
323
|
findCommand(command) {
|
|
324
324
|
try {
|
|
325
325
|
const cmd = this.platform === 'windows' ? 'where' : 'which';
|
|
326
|
-
const result = (0, child_process_1.
|
|
326
|
+
const result = (0, child_process_1.spawnSync)(cmd, [command], {
|
|
327
327
|
encoding: 'utf8',
|
|
328
328
|
timeout: 5000,
|
|
329
329
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
330
|
-
})
|
|
331
|
-
|
|
330
|
+
});
|
|
331
|
+
if (result.status === 0 && result.stdout) {
|
|
332
|
+
return result.stdout.trim().split('\n')[0] || null;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
332
335
|
}
|
|
333
336
|
catch {
|
|
334
337
|
return null;
|
|
@@ -338,6 +341,11 @@ class ToolDetector {
|
|
|
338
341
|
* Check if a process is running
|
|
339
342
|
*/
|
|
340
343
|
isProcessRunning(processName) {
|
|
344
|
+
// Validate processName to prevent command injection
|
|
345
|
+
const SAFE_PROCESS_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
346
|
+
if (!SAFE_PROCESS_NAME.test(processName)) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
341
349
|
try {
|
|
342
350
|
let cmd;
|
|
343
351
|
if (this.platform === 'windows') {
|