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.
Files changed (158) hide show
  1. package/LICENSE +11 -7
  2. package/README.md +77 -118
  3. package/dist/approval.d.ts +1 -0
  4. package/dist/approval.js +9 -0
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +62 -16
  7. package/dist/crypto/e2eeManager.d.ts +49 -52
  8. package/dist/crypto/e2eeManager.js +256 -181
  9. package/dist/crypto/encryption.d.ts +8 -10
  10. package/dist/crypto/encryption.js +29 -94
  11. package/dist/crypto/index.d.ts +10 -0
  12. package/dist/crypto/index.js +22 -0
  13. package/dist/crypto/keyExchange.d.ts +6 -20
  14. package/dist/crypto/keyExchange.js +18 -110
  15. package/dist/crypto/keyGeneration.d.ts +2 -13
  16. package/dist/crypto/keyGeneration.js +14 -88
  17. package/dist/crypto/keyStorage.d.ts +32 -5
  18. package/dist/crypto/keyStorage.js +152 -8
  19. package/dist/crypto/sessionPersistence.d.ts +7 -13
  20. package/dist/crypto/sessionPersistence.js +108 -33
  21. package/dist/crypto/types.d.ts +24 -3
  22. package/dist/crypto/types.js +2 -1
  23. package/dist/crypto/websocketE2EE.d.ts +6 -17
  24. package/dist/crypto/websocketE2EE.js +21 -38
  25. package/dist/index.js +203 -280
  26. package/dist/integration.d.ts +0 -1
  27. package/dist/integration.js +2 -4
  28. package/dist/logger.d.ts +15 -0
  29. package/dist/logger.js +209 -1
  30. package/dist/server.d.ts +30 -0
  31. package/dist/server.js +162 -0
  32. package/dist/startup.js +15 -6
  33. package/dist/terminal.d.ts +1 -0
  34. package/dist/terminal.js +94 -1
  35. package/dist/tools/claude-process.d.ts +8 -0
  36. package/dist/tools/claude-process.js +199 -26
  37. package/dist/tools/claude-sessions.d.ts +1 -0
  38. package/dist/tools/claude-sessions.js +36 -10
  39. package/dist/tools/detector.js +11 -3
  40. package/dist/tools/permission-hook.js +94 -27
  41. package/dist/tools/permission-ipc.d.ts +1 -0
  42. package/dist/tools/permission-ipc.js +61 -14
  43. package/dist/transcript-streamer.d.ts +1 -0
  44. package/dist/transcript-streamer.js +18 -4
  45. package/dist/usage-tracker.d.ts +45 -0
  46. package/dist/usage-tracker.js +243 -0
  47. package/dist/websocket.d.ts +43 -12
  48. package/dist/websocket.js +418 -214
  49. package/package.json +5 -4
  50. package/dist/__tests__/cli-commands.test.d.ts +0 -6
  51. package/dist/__tests__/cli-commands.test.d.ts.map +0 -1
  52. package/dist/__tests__/cli-commands.test.js +0 -213
  53. package/dist/__tests__/cli-commands.test.js.map +0 -1
  54. package/dist/__tests__/crypto/e2e-integration.test.d.ts +0 -17
  55. package/dist/__tests__/crypto/e2e-integration.test.d.ts.map +0 -1
  56. package/dist/__tests__/crypto/e2e-integration.test.js +0 -338
  57. package/dist/__tests__/crypto/e2e-integration.test.js.map +0 -1
  58. package/dist/__tests__/crypto/e2eeManager.test.d.ts +0 -2
  59. package/dist/__tests__/crypto/e2eeManager.test.d.ts.map +0 -1
  60. package/dist/__tests__/crypto/e2eeManager.test.js +0 -242
  61. package/dist/__tests__/crypto/e2eeManager.test.js.map +0 -1
  62. package/dist/__tests__/crypto/encryption.test.d.ts +0 -2
  63. package/dist/__tests__/crypto/encryption.test.d.ts.map +0 -1
  64. package/dist/__tests__/crypto/encryption.test.js +0 -116
  65. package/dist/__tests__/crypto/encryption.test.js.map +0 -1
  66. package/dist/__tests__/crypto/keyExchange.test.d.ts +0 -2
  67. package/dist/__tests__/crypto/keyExchange.test.d.ts.map +0 -1
  68. package/dist/__tests__/crypto/keyExchange.test.js +0 -84
  69. package/dist/__tests__/crypto/keyExchange.test.js.map +0 -1
  70. package/dist/__tests__/crypto/keyGeneration.test.d.ts +0 -2
  71. package/dist/__tests__/crypto/keyGeneration.test.d.ts.map +0 -1
  72. package/dist/__tests__/crypto/keyGeneration.test.js +0 -61
  73. package/dist/__tests__/crypto/keyGeneration.test.js.map +0 -1
  74. package/dist/__tests__/crypto/keyStorage.test.d.ts +0 -2
  75. package/dist/__tests__/crypto/keyStorage.test.d.ts.map +0 -1
  76. package/dist/__tests__/crypto/keyStorage.test.js +0 -133
  77. package/dist/__tests__/crypto/keyStorage.test.js.map +0 -1
  78. package/dist/__tests__/crypto/websocketIntegration.test.d.ts +0 -2
  79. package/dist/__tests__/crypto/websocketIntegration.test.d.ts.map +0 -1
  80. package/dist/__tests__/crypto/websocketIntegration.test.js +0 -259
  81. package/dist/__tests__/crypto/websocketIntegration.test.js.map +0 -1
  82. package/dist/__tests__/startup.test.d.ts +0 -11
  83. package/dist/__tests__/startup.test.d.ts.map +0 -1
  84. package/dist/__tests__/startup.test.js +0 -241
  85. package/dist/__tests__/startup.test.js.map +0 -1
  86. package/dist/__tests__/tools/claude-process.test.d.ts +0 -8
  87. package/dist/__tests__/tools/claude-process.test.d.ts.map +0 -1
  88. package/dist/__tests__/tools/claude-process.test.js +0 -430
  89. package/dist/__tests__/tools/claude-process.test.js.map +0 -1
  90. package/dist/__tests__/tools/permission-hook.test.d.ts +0 -17
  91. package/dist/__tests__/tools/permission-hook.test.d.ts.map +0 -1
  92. package/dist/__tests__/tools/permission-hook.test.js +0 -616
  93. package/dist/__tests__/tools/permission-hook.test.js.map +0 -1
  94. package/dist/__tests__/tools/permission-ipc.test.d.ts +0 -11
  95. package/dist/__tests__/tools/permission-ipc.test.d.ts.map +0 -1
  96. package/dist/__tests__/tools/permission-ipc.test.js +0 -612
  97. package/dist/__tests__/tools/permission-ipc.test.js.map +0 -1
  98. package/dist/__tests__/websocket.test.d.ts +0 -13
  99. package/dist/__tests__/websocket.test.d.ts.map +0 -1
  100. package/dist/__tests__/websocket.test.js +0 -204
  101. package/dist/__tests__/websocket.test.js.map +0 -1
  102. package/dist/api.d.ts +0 -44
  103. package/dist/api.d.ts.map +0 -1
  104. package/dist/api.js +0 -76
  105. package/dist/api.js.map +0 -1
  106. package/dist/approval.d.ts.map +0 -1
  107. package/dist/approval.js.map +0 -1
  108. package/dist/config.d.ts.map +0 -1
  109. package/dist/config.js.map +0 -1
  110. package/dist/crypto/e2eeManager.d.ts.map +0 -1
  111. package/dist/crypto/e2eeManager.js.map +0 -1
  112. package/dist/crypto/encryption.d.ts.map +0 -1
  113. package/dist/crypto/encryption.js.map +0 -1
  114. package/dist/crypto/keyExchange.d.ts.map +0 -1
  115. package/dist/crypto/keyExchange.js.map +0 -1
  116. package/dist/crypto/keyGeneration.d.ts.map +0 -1
  117. package/dist/crypto/keyGeneration.js.map +0 -1
  118. package/dist/crypto/keyStorage.d.ts.map +0 -1
  119. package/dist/crypto/keyStorage.js.map +0 -1
  120. package/dist/crypto/sessionPersistence.d.ts.map +0 -1
  121. package/dist/crypto/sessionPersistence.js.map +0 -1
  122. package/dist/crypto/types.d.ts.map +0 -1
  123. package/dist/crypto/types.js.map +0 -1
  124. package/dist/crypto/websocketE2EE.d.ts.map +0 -1
  125. package/dist/crypto/websocketE2EE.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/integration.d.ts.map +0 -1
  129. package/dist/integration.js.map +0 -1
  130. package/dist/logger.d.ts.map +0 -1
  131. package/dist/logger.js.map +0 -1
  132. package/dist/startup.d.ts.map +0 -1
  133. package/dist/startup.js.map +0 -1
  134. package/dist/terminal.d.ts.map +0 -1
  135. package/dist/terminal.js.map +0 -1
  136. package/dist/tools/__tests__/claude-sessions.test.d.ts +0 -2
  137. package/dist/tools/__tests__/claude-sessions.test.d.ts.map +0 -1
  138. package/dist/tools/__tests__/claude-sessions.test.js +0 -306
  139. package/dist/tools/__tests__/claude-sessions.test.js.map +0 -1
  140. package/dist/tools/claude-hooks.d.ts.map +0 -1
  141. package/dist/tools/claude-hooks.js.map +0 -1
  142. package/dist/tools/claude-process.d.ts.map +0 -1
  143. package/dist/tools/claude-process.js.map +0 -1
  144. package/dist/tools/claude-sessions.d.ts.map +0 -1
  145. package/dist/tools/claude-sessions.js.map +0 -1
  146. package/dist/tools/detector.d.ts.map +0 -1
  147. package/dist/tools/detector.js.map +0 -1
  148. package/dist/tools/index.d.ts.map +0 -1
  149. package/dist/tools/index.js.map +0 -1
  150. package/dist/tools/permission-hook.d.ts.map +0 -1
  151. package/dist/tools/permission-hook.js.map +0 -1
  152. package/dist/tools/permission-ipc.d.ts.map +0 -1
  153. package/dist/tools/permission-ipc.js.map +0 -1
  154. package/dist/transcript-streamer.d.ts.map +0 -1
  155. package/dist/transcript-streamer.js.map +0 -1
  156. package/dist/websocket.d.ts.map +0 -1
  157. package/dist/websocket.js.map +0 -1
  158. 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: { ...process.env, TERM: 'xterm-256color' },
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.join(' ')}`);
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: { ...process.env, TERM: 'xterm-256color' },
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: ${jsonLine.substring(0, 100)}...`);
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: { ...process.env, TERM: 'xterm-256color' },
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 in ${resolvedDir}`);
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} in ${directory}${dangerouslySkipPermissions ? ' (unrestricted)' : ''}${interactivePermissions ? ' (interactive)' : ''}${effectiveIsReal === false ? ' (fresh — no real Claude session)' : ''}`);
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: ${JSON.stringify(message.error || 'unknown')}`);
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: ${line.substring(0, 50)}...`);
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 that could be dangerous
588
- if (/[;&|`$()<>]/.test(dir)) {
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
- return dir === '~' ? os.homedir() : dir.replace('~', os.homedir());
691
+ resolved = dir === '~' ? os.homedir() : dir.replace('~', os.homedir());
692
+ }
693
+ else {
694
+ resolved = path.resolve(dir);
593
695
  }
594
- return path.resolve(dir);
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
- fs.writeFileSync(rulesFile, JSON.stringify(rules, null, 2), 'utf-8');
732
- console.log(`[Claude Process] Permission rules written to ${rulesFile}`);
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 at ${hookScriptPath}, skipping hook configuration`);
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 in ${settingsFile}`);
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 from ${settingsFile}`);
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
- inputSummary = JSON.stringify(toolInput).substring(0, 200);
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}) - ${inputSummary.substring(0, 80)}`);
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 '${response}' for ${approvalId} ignored — SDK mode processes don't accept raw stdin approval characters`);
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.subject}`);
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
@@ -10,6 +10,7 @@ import { EventEmitter } from 'events';
10
10
  export interface ClaudeSessionInfo {
11
11
  sessionKey: string;
12
12
  directory: string;
13
+ name?: string;
13
14
  state: 'active' | 'inactive';
14
15
  lastUsedAt: string;
15
16
  transcriptPath?: string;
@@ -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
- // Try to read the first line to get session ID
167
+ // Read transcript lines to get session ID and name
168
168
  const content = fs.readFileSync(filePath, 'utf8');
169
- const firstLine = content.split('\n')[0];
169
+ const lines = content.split('\n');
170
170
  let sessionId = fileName;
171
- try {
172
- const firstMessage = JSON.parse(firstLine);
173
- if (firstMessage.sessionId) {
174
- sessionId = firstMessage.sessionId;
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,
@@ -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.execSync)(`${cmd} ${command}`, {
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
- }).trim();
331
- return result.split('\n')[0] || null;
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') {