@lifeaitools/clauth 1.30.26 → 2.0.0

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/cli/api.js CHANGED
@@ -1,238 +1,238 @@
1
- // cli/api.js
2
- // Thin client that calls the auth-vault Edge Function
3
-
4
- import { createRequire } from "module";
5
- const require = createRequire(import.meta.url);
6
-
7
- import Conf from "conf";
8
- import { getConfOptions } from "./conf-path.js";
9
-
10
- const config = new Conf(getConfOptions());
11
-
12
- // ============================================================
13
- // Get Edge Function base URL from local config
14
- // ============================================================
15
- export function getBaseUrl() {
16
- const url = config.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL;
17
- if (!url) throw new Error("Supabase URL not configured. Run: clauth setup");
18
- return `${url}/functions/v1/auth-vault`;
19
- }
20
-
21
- export function getAnonKey() {
22
- const key = config.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY;
23
- if (!key) throw new Error("Supabase anon key not configured. Run: clauth setup");
24
- return key;
25
- }
26
-
27
- // ============================================================
28
- // Core POST helper
29
- // ============================================================
30
- // Bound every vault round-trip with a wall-clock timeout. Without this, a TCP
31
- // stall (DNS black-hole, half-open socket — NOT a clean 4xx) hangs the caller
32
- // forever. Callers that treat persistence as best-effort (e.g. the call_agent
33
- // scratchpad mirror) rely on this resolving to an error rather than blocking.
34
- // Override via CLAUTH_VAULT_TIMEOUT_MS; default 8s.
35
- const VAULT_FETCH_TIMEOUT_MS = (() => {
36
- const n = Number(process.env.CLAUTH_VAULT_TIMEOUT_MS);
37
- return Number.isFinite(n) && n > 0 ? n : 8000;
38
- })();
39
-
40
- // ============================================================
41
- // Backend / external-resource error model
42
- // ============================================================
43
- // A VaultBackendError means the vault backend (the auth-vault Edge Function or
44
- // the Postgres database behind it) never rendered an auth verdict — the request
45
- // timed out, the network failed, the function 5xx'd, the DB was unreachable, or
46
- // we were rate-limited. These are TRANSIENT and must NEVER be counted as an
47
- // authentication failure (a wrong password). Treating a database timeout as a
48
- // bad password is the bug that turned a DB blip into a permanent vault lockout.
49
- export class VaultBackendError extends Error {
50
- constructor(kind, message, { status = null, detail = null, cause = null } = {}) {
51
- super(message);
52
- this.name = "VaultBackendError";
53
- this.kind = kind; // timeout | network | server_error | rate_limited | db_error | unknown
54
- this.isBackend = true; // marker for callers: do not strike
55
- this.retriable = true; // the verdict was never rendered — safe to retry
56
- this.status = status; // HTTP status if the server responded
57
- this.detail = detail; // human-readable backend detail
58
- if (cause) this.cause = cause;
59
- }
60
- }
61
-
62
- // Server `reason` strings that are genuine AUTH VERDICTS (the backend evaluated
63
- // the credential and answered). These DO count — they are not backend errors.
64
- const AUTH_VERDICT_REASONS = [
65
- "invalid_token", "wrong_password", "invalid_password",
66
- "machine_locked", "machine_disabled", "machine_not_found",
67
- ];
68
-
69
- // Classify a server-supplied `reason` string. Returns a VaultBackendError kind
70
- // when the reason describes a backend/transient condition, or null when the
71
- // reason is a real auth verdict (or unknown — caller decides).
72
- export function classifyServerReason(reason) {
73
- if (!reason || typeof reason !== "string") return null;
74
- const r = reason.toLowerCase();
75
- if (AUTH_VERDICT_REASONS.some((v) => r.includes(v))) return null; // genuine verdict
76
- if (/rate[_\s-]?limit/.test(r)) return "rate_limited";
77
- if (/(database|\bdb\b|postgres|connection|pool|statement timeout|unavailable|internal|timeout|5\d\d)/.test(r)) return "db_error";
78
- return null;
79
- }
80
-
81
- // Classify a thrown error (transport/timeout/network/typed). Returns a kind
82
- // string when it is a backend/transient failure, or null when it is not.
83
- export function classifyBackendError(err) {
84
- if (!err) return null;
85
- if (err instanceof VaultBackendError) return err.kind;
86
- const name = err.name || "";
87
- const msg = (err.message || "").toString();
88
- if (name === "TimeoutError" || name === "AbortError" || /\babort(ed)?\b|\btim(e|ed)?\s*out\b|timeout/i.test(msg)) return "timeout";
89
- if (/fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ECONNRESET|EPIPE|network|socket hang/i.test(msg)) return "network";
90
- return null;
91
- }
92
-
93
- async function post(route, body) {
94
- const url = `${getBaseUrl()}/${route}`;
95
- const anonKey = getAnonKey();
96
-
97
- let res;
98
- try {
99
- res = await fetch(url, {
100
- method: "POST",
101
- headers: {
102
- "Content-Type": "application/json",
103
- "Authorization": `Bearer ${anonKey}`
104
- },
105
- body: JSON.stringify(body),
106
- signal: AbortSignal.timeout(VAULT_FETCH_TIMEOUT_MS)
107
- });
108
- } catch (err) {
109
- // Transport-level failure — the backend never answered. Surface a typed,
110
- // non-strike error so callers report "backend unreachable", not "bad password".
111
- const kind = classifyBackendError(err) || "network";
112
- const why = kind === "timeout"
113
- ? `vault backend did not respond within ${VAULT_FETCH_TIMEOUT_MS}ms`
114
- : `cannot reach vault backend (${err.message || "network error"})`;
115
- throw new VaultBackendError(kind, why, { detail: err.message || String(err), cause: err });
116
- }
117
-
118
- let data;
119
- try {
120
- data = await res.json();
121
- } catch (err) {
122
- // 2xx/5xx with a non-JSON or empty body — the function errored without a
123
- // structured verdict. Transient backend condition, not an auth failure.
124
- throw new VaultBackendError("server_error", `vault backend returned an unreadable response (HTTP ${res.status})`, { status: res.status, detail: err.message });
125
- }
126
-
127
- // HTTP 5xx (or any non-ok with no structured error) = backend problem, never a verdict.
128
- if (res.status >= 500 || (!res.ok && !data.error)) {
129
- const reasonKind = classifyServerReason(data.reason || data.error) || "server_error";
130
- throw new VaultBackendError(reasonKind, `vault backend error (HTTP ${res.status})`, { status: res.status, detail: data.error || data.reason || `HTTP ${res.status}` });
131
- }
132
-
133
- // 4xx WITH a structured error: could be a real verdict (invalid_token) or a
134
- // transient backend signal surfaced as 4xx (rate_limited). Promote the latter
135
- // to a typed backend error; leave genuine verdicts in `data` for the caller.
136
- if (data.error) {
137
- const reasonKind = classifyServerReason(data.reason || data.error);
138
- if (reasonKind) {
139
- throw new VaultBackendError(reasonKind, `vault backend ${reasonKind.replace("_", " ")}`, { status: res.status, detail: data.error || data.reason });
140
- }
141
- }
142
-
143
- return data;
144
- }
145
-
146
- // ============================================================
147
- // Auth-bearing calls (require HMAC token)
148
- // ============================================================
149
- async function authPost(route, password, machineHash, token, timestamp, extra = {}) {
150
- return post(route, {
151
- machine_hash: machineHash,
152
- token,
153
- timestamp,
154
- password,
155
- ...extra
156
- });
157
- }
158
-
159
- // ============================================================
160
- // Exported API surface
161
- // ============================================================
162
-
163
- export async function retrieve(password, machineHash, token, timestamp, service) {
164
- return authPost("retrieve", password, machineHash, token, timestamp, { service });
165
- }
166
-
167
- export async function write(password, machineHash, token, timestamp, service, value) {
168
- return authPost("write", password, machineHash, token, timestamp, { service, value });
169
- }
170
-
171
- export async function enable(password, machineHash, token, timestamp, service, enabled) {
172
- return authPost("enable", password, machineHash, token, timestamp, { service, enabled });
173
- }
174
-
175
- export async function addService(password, machineHash, token, timestamp, name, label, key_type, description, project) {
176
- const extra = { name, label, key_type, description };
177
- if (project) extra.project = project;
178
- return authPost("add", password, machineHash, token, timestamp, extra);
179
- }
180
-
181
- export async function updateService(password, machineHash, token, timestamp, service, updates) {
182
- return authPost("update", password, machineHash, token, timestamp, { service, ...updates });
183
- }
184
-
185
- export async function removeService(password, machineHash, token, timestamp, service, confirm) {
186
- return authPost("remove", password, machineHash, token, timestamp, { service, confirm });
187
- }
188
-
189
- export async function revoke(password, machineHash, token, timestamp, service, confirm) {
190
- return authPost("revoke", password, machineHash, token, timestamp, { service, confirm });
191
- }
192
-
193
- export async function status(password, machineHash, token, timestamp, project) {
194
- const extra = {};
195
- if (project) extra.project = project;
196
- return authPost("status", password, machineHash, token, timestamp, extra);
197
- }
198
-
199
- export async function test(password, machineHash, token, timestamp) {
200
- return authPost("test", password, machineHash, token, timestamp);
201
- }
202
-
203
- export async function changePassword(password, machineHash, token, timestamp, newSeedHash) {
204
- return authPost("change-password", password, machineHash, token, timestamp, { new_hmac_seed_hash: newSeedHash });
205
- }
206
-
207
- export async function createEnrollment(password, machineHash, token, timestamp, label, ttlMinutes, installId) {
208
- const extra = {};
209
- if (label) extra.label = label;
210
- if (ttlMinutes) extra.ttl_minutes = ttlMinutes;
211
- if (installId) extra.install_id = installId;
212
- return authPost("create-enrollment", password, machineHash, token, timestamp, extra);
213
- }
214
-
215
- export async function registerMachine(machineHash, seedHash, label, adminToken, extras = {}) {
216
- return post("register-machine", {
217
- machine_hash: machineHash,
218
- hmac_seed_hash: seedHash,
219
- label,
220
- admin_token: adminToken,
221
- ...extras
222
- });
223
- }
224
-
225
- export async function redeemEnrollment(machineHash, seedHash, label, enrollmentCode) {
226
- return post("redeem-enrollment", {
227
- machine_hash: machineHash,
228
- hmac_seed_hash: seedHash,
229
- label,
230
- enrollment_code: enrollmentCode
231
- });
232
- }
233
-
234
- export default {
235
- retrieve, write, enable, addService, updateService, removeService, revoke,
236
- status, test, createEnrollment, registerMachine, redeemEnrollment, getBaseUrl, getAnonKey,
237
- VaultBackendError, classifyBackendError, classifyServerReason
238
- };
1
+ // cli/api.js
2
+ // Thin client that calls the auth-vault Edge Function
3
+
4
+ import { createRequire } from "module";
5
+ const require = createRequire(import.meta.url);
6
+
7
+ import Conf from "conf";
8
+ import { getConfOptions } from "./conf-path.js";
9
+
10
+ const config = new Conf(getConfOptions());
11
+
12
+ // ============================================================
13
+ // Get Edge Function base URL from local config
14
+ // ============================================================
15
+ export function getBaseUrl() {
16
+ const url = config.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL;
17
+ if (!url) throw new Error("Supabase URL not configured. Run: clauth setup");
18
+ return `${url}/functions/v1/auth-vault`;
19
+ }
20
+
21
+ export function getAnonKey() {
22
+ const key = config.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY;
23
+ if (!key) throw new Error("Supabase anon key not configured. Run: clauth setup");
24
+ return key;
25
+ }
26
+
27
+ // ============================================================
28
+ // Core POST helper
29
+ // ============================================================
30
+ // Bound every vault round-trip with a wall-clock timeout. Without this, a TCP
31
+ // stall (DNS black-hole, half-open socket — NOT a clean 4xx) hangs the caller
32
+ // forever. Callers that treat persistence as best-effort (e.g. the call_agent
33
+ // scratchpad mirror) rely on this resolving to an error rather than blocking.
34
+ // Override via CLAUTH_VAULT_TIMEOUT_MS; default 8s.
35
+ const VAULT_FETCH_TIMEOUT_MS = (() => {
36
+ const n = Number(process.env.CLAUTH_VAULT_TIMEOUT_MS);
37
+ return Number.isFinite(n) && n > 0 ? n : 8000;
38
+ })();
39
+
40
+ // ============================================================
41
+ // Backend / external-resource error model
42
+ // ============================================================
43
+ // A VaultBackendError means the vault backend (the auth-vault Edge Function or
44
+ // the Postgres database behind it) never rendered an auth verdict — the request
45
+ // timed out, the network failed, the function 5xx'd, the DB was unreachable, or
46
+ // we were rate-limited. These are TRANSIENT and must NEVER be counted as an
47
+ // authentication failure (a wrong password). Treating a database timeout as a
48
+ // bad password is the bug that turned a DB blip into a permanent vault lockout.
49
+ export class VaultBackendError extends Error {
50
+ constructor(kind, message, { status = null, detail = null, cause = null } = {}) {
51
+ super(message);
52
+ this.name = "VaultBackendError";
53
+ this.kind = kind; // timeout | network | server_error | rate_limited | db_error | unknown
54
+ this.isBackend = true; // marker for callers: do not strike
55
+ this.retriable = true; // the verdict was never rendered — safe to retry
56
+ this.status = status; // HTTP status if the server responded
57
+ this.detail = detail; // human-readable backend detail
58
+ if (cause) this.cause = cause;
59
+ }
60
+ }
61
+
62
+ // Server `reason` strings that are genuine AUTH VERDICTS (the backend evaluated
63
+ // the credential and answered). These DO count — they are not backend errors.
64
+ const AUTH_VERDICT_REASONS = [
65
+ "invalid_token", "wrong_password", "invalid_password",
66
+ "machine_locked", "machine_disabled", "machine_not_found",
67
+ ];
68
+
69
+ // Classify a server-supplied `reason` string. Returns a VaultBackendError kind
70
+ // when the reason describes a backend/transient condition, or null when the
71
+ // reason is a real auth verdict (or unknown — caller decides).
72
+ export function classifyServerReason(reason) {
73
+ if (!reason || typeof reason !== "string") return null;
74
+ const r = reason.toLowerCase();
75
+ if (AUTH_VERDICT_REASONS.some((v) => r.includes(v))) return null; // genuine verdict
76
+ if (/rate[_\s-]?limit/.test(r)) return "rate_limited";
77
+ if (/(database|\bdb\b|postgres|connection|pool|statement timeout|unavailable|internal|timeout|5\d\d)/.test(r)) return "db_error";
78
+ return null;
79
+ }
80
+
81
+ // Classify a thrown error (transport/timeout/network/typed). Returns a kind
82
+ // string when it is a backend/transient failure, or null when it is not.
83
+ export function classifyBackendError(err) {
84
+ if (!err) return null;
85
+ if (err instanceof VaultBackendError) return err.kind;
86
+ const name = err.name || "";
87
+ const msg = (err.message || "").toString();
88
+ if (name === "TimeoutError" || name === "AbortError" || /\babort(ed)?\b|\btim(e|ed)?\s*out\b|timeout/i.test(msg)) return "timeout";
89
+ if (/fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ECONNRESET|EPIPE|network|socket hang/i.test(msg)) return "network";
90
+ return null;
91
+ }
92
+
93
+ async function post(route, body) {
94
+ const url = `${getBaseUrl()}/${route}`;
95
+ const anonKey = getAnonKey();
96
+
97
+ let res;
98
+ try {
99
+ res = await fetch(url, {
100
+ method: "POST",
101
+ headers: {
102
+ "Content-Type": "application/json",
103
+ "Authorization": `Bearer ${anonKey}`
104
+ },
105
+ body: JSON.stringify(body),
106
+ signal: AbortSignal.timeout(VAULT_FETCH_TIMEOUT_MS)
107
+ });
108
+ } catch (err) {
109
+ // Transport-level failure — the backend never answered. Surface a typed,
110
+ // non-strike error so callers report "backend unreachable", not "bad password".
111
+ const kind = classifyBackendError(err) || "network";
112
+ const why = kind === "timeout"
113
+ ? `vault backend did not respond within ${VAULT_FETCH_TIMEOUT_MS}ms`
114
+ : `cannot reach vault backend (${err.message || "network error"})`;
115
+ throw new VaultBackendError(kind, why, { detail: err.message || String(err), cause: err });
116
+ }
117
+
118
+ let data;
119
+ try {
120
+ data = await res.json();
121
+ } catch (err) {
122
+ // 2xx/5xx with a non-JSON or empty body — the function errored without a
123
+ // structured verdict. Transient backend condition, not an auth failure.
124
+ throw new VaultBackendError("server_error", `vault backend returned an unreadable response (HTTP ${res.status})`, { status: res.status, detail: err.message });
125
+ }
126
+
127
+ // HTTP 5xx (or any non-ok with no structured error) = backend problem, never a verdict.
128
+ if (res.status >= 500 || (!res.ok && !data.error)) {
129
+ const reasonKind = classifyServerReason(data.reason || data.error) || "server_error";
130
+ throw new VaultBackendError(reasonKind, `vault backend error (HTTP ${res.status})`, { status: res.status, detail: data.error || data.reason || `HTTP ${res.status}` });
131
+ }
132
+
133
+ // 4xx WITH a structured error: could be a real verdict (invalid_token) or a
134
+ // transient backend signal surfaced as 4xx (rate_limited). Promote the latter
135
+ // to a typed backend error; leave genuine verdicts in `data` for the caller.
136
+ if (data.error) {
137
+ const reasonKind = classifyServerReason(data.reason || data.error);
138
+ if (reasonKind) {
139
+ throw new VaultBackendError(reasonKind, `vault backend ${reasonKind.replace("_", " ")}`, { status: res.status, detail: data.error || data.reason });
140
+ }
141
+ }
142
+
143
+ return data;
144
+ }
145
+
146
+ // ============================================================
147
+ // Auth-bearing calls (require HMAC token)
148
+ // ============================================================
149
+ async function authPost(route, password, machineHash, token, timestamp, extra = {}) {
150
+ return post(route, {
151
+ machine_hash: machineHash,
152
+ token,
153
+ timestamp,
154
+ password,
155
+ ...extra
156
+ });
157
+ }
158
+
159
+ // ============================================================
160
+ // Exported API surface
161
+ // ============================================================
162
+
163
+ export async function retrieve(password, machineHash, token, timestamp, service) {
164
+ return authPost("retrieve", password, machineHash, token, timestamp, { service });
165
+ }
166
+
167
+ export async function write(password, machineHash, token, timestamp, service, value) {
168
+ return authPost("write", password, machineHash, token, timestamp, { service, value });
169
+ }
170
+
171
+ export async function enable(password, machineHash, token, timestamp, service, enabled) {
172
+ return authPost("enable", password, machineHash, token, timestamp, { service, enabled });
173
+ }
174
+
175
+ export async function addService(password, machineHash, token, timestamp, name, label, key_type, description, project) {
176
+ const extra = { name, label, key_type, description };
177
+ if (project) extra.project = project;
178
+ return authPost("add", password, machineHash, token, timestamp, extra);
179
+ }
180
+
181
+ export async function updateService(password, machineHash, token, timestamp, service, updates) {
182
+ return authPost("update", password, machineHash, token, timestamp, { service, ...updates });
183
+ }
184
+
185
+ export async function removeService(password, machineHash, token, timestamp, service, confirm) {
186
+ return authPost("remove", password, machineHash, token, timestamp, { service, confirm });
187
+ }
188
+
189
+ export async function revoke(password, machineHash, token, timestamp, service, confirm) {
190
+ return authPost("revoke", password, machineHash, token, timestamp, { service, confirm });
191
+ }
192
+
193
+ export async function status(password, machineHash, token, timestamp, project) {
194
+ const extra = {};
195
+ if (project) extra.project = project;
196
+ return authPost("status", password, machineHash, token, timestamp, extra);
197
+ }
198
+
199
+ export async function test(password, machineHash, token, timestamp) {
200
+ return authPost("test", password, machineHash, token, timestamp);
201
+ }
202
+
203
+ export async function changePassword(password, machineHash, token, timestamp, newSeedHash) {
204
+ return authPost("change-password", password, machineHash, token, timestamp, { new_hmac_seed_hash: newSeedHash });
205
+ }
206
+
207
+ export async function createEnrollment(password, machineHash, token, timestamp, label, ttlMinutes, installId) {
208
+ const extra = {};
209
+ if (label) extra.label = label;
210
+ if (ttlMinutes) extra.ttl_minutes = ttlMinutes;
211
+ if (installId) extra.install_id = installId;
212
+ return authPost("create-enrollment", password, machineHash, token, timestamp, extra);
213
+ }
214
+
215
+ export async function registerMachine(machineHash, seedHash, label, adminToken, extras = {}) {
216
+ return post("register-machine", {
217
+ machine_hash: machineHash,
218
+ hmac_seed_hash: seedHash,
219
+ label,
220
+ admin_token: adminToken,
221
+ ...extras
222
+ });
223
+ }
224
+
225
+ export async function redeemEnrollment(machineHash, seedHash, label, enrollmentCode) {
226
+ return post("redeem-enrollment", {
227
+ machine_hash: machineHash,
228
+ hmac_seed_hash: seedHash,
229
+ label,
230
+ enrollment_code: enrollmentCode
231
+ });
232
+ }
233
+
234
+ export default {
235
+ retrieve, write, enable, addService, updateService, removeService, revoke,
236
+ status, test, createEnrollment, registerMachine, redeemEnrollment, getBaseUrl, getAnonKey,
237
+ VaultBackendError, classifyBackendError, classifyServerReason
238
+ };
@@ -403,6 +403,39 @@ function resolveClaudeBinary(explicit) {
403
403
  return null;
404
404
  }
405
405
 
406
+ function quoteCmdArgument(value) {
407
+ return `"${String(value)
408
+ .replace(/"/g, '""')
409
+ .replace(/[%^&|<>()!]/g, "^$&")}"`;
410
+ }
411
+
412
+ function buildClaudeSpawn(binary, args, windowsHide) {
413
+ const isCmdShim = process.platform === "win32" && /\.cmd$/i.test(binary);
414
+ if (!isCmdShim) {
415
+ return {
416
+ command: binary,
417
+ spawnArgs: args,
418
+ spawnOptions: { shell: false, windowsHide },
419
+ };
420
+ }
421
+
422
+ // `cmd.exe` parses the target as a command line, not an argv vector. Passing
423
+ // a separately quoted target lets Node backslash-escape it; passing every
424
+ // argument verbatim loses multi-word system prompts. A single `call` command
425
+ // line preserves both the target and its arguments, including profile paths
426
+ // with spaces.
427
+ return {
428
+ command: "cmd.exe",
429
+ spawnArgs: [
430
+ "/d",
431
+ "/s",
432
+ "/c",
433
+ ["call", quoteCmdArgument(binary), ...args.map(quoteCmdArgument)].join(" "),
434
+ ],
435
+ spawnOptions: { shell: false, windowsVerbatimArguments: true, windowsHide },
436
+ };
437
+ }
438
+
406
439
  function makeJobId(prefix = "call") {
407
440
  return `${prefix}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
408
441
  }
@@ -556,16 +589,17 @@ export class WarmWorker {
556
589
  async start() {
557
590
  let proc;
558
591
  try {
559
- const isCmdShim = process.platform === "win32" && /\.cmd$/i.test(this.binary);
560
592
  const args = buildWarmArgs({ model: this.model, appendSystemPrompt: this.appendSystemPrompt });
561
- const command = isCmdShim ? "cmd" : this.binary;
562
- const spawnArgs = isCmdShim ? ["/d", "/s", "/c", `"${this.binary}"`, ...args] : args;
593
+ const { command, spawnArgs, spawnOptions } = buildClaudeSpawn(
594
+ this.binary,
595
+ args,
596
+ !this.allowVisibleWindow,
597
+ );
563
598
  proc = this._spawn(command, spawnArgs, {
564
599
  cwd: this.cwd,
565
600
  env: process.env, // inherits CLI login session; never sets ANTHROPIC_API_KEY
566
601
  stdio: ["pipe", "pipe", "pipe"],
567
- shell: false,
568
- windowsHide: !this.allowVisibleWindow,
602
+ ...spawnOptions,
569
603
  });
570
604
  } catch (e) {
571
605
  this.state = "dead";
@@ -584,6 +618,14 @@ export class WarmWorker {
584
618
  proc.stderr?.on("data", (d) => {
585
619
  this._stderr = `${this._stderr || ""}${d.toString()}`.slice(-20000);
586
620
  });
621
+ // Without this, a write() to a stdin pipe whose child already exited fails
622
+ // ASYNCHRONOUSLY (EPIPE) via the stream's own 'error' event, not via the
623
+ // synchronous try/catch around write() in send() below. An unhandled
624
+ // 'error' event on a stream is fatal to the whole process by Node's
625
+ // design — one dead warm worker was taking down the entire clauth daemon
626
+ // for the whole fleet. The two other spawn sites in this file already
627
+ // guard this the same way; this one was missed.
628
+ proc.stdin?.on("error", () => {});
587
629
  proc.on("error", (e) => this._onDeath(`proc_error: ${e.message}`));
588
630
  proc.on("close", (code) => this._onDeath(`exit_${code}`));
589
631
 
@@ -1463,15 +1505,12 @@ export class AgentPool {
1463
1505
 
1464
1506
  let proc;
1465
1507
  try {
1466
- const isCmdShim = process.platform === "win32" && /\.cmd$/i.test(this.binary);
1467
- const command = isCmdShim ? "cmd" : this.binary;
1468
- const spawnArgs = isCmdShim ? ["/d", "/s", "/c", `"${this.binary}"`, ...cmdArgs] : cmdArgs;
1508
+ const { command, spawnArgs, spawnOptions } = buildClaudeSpawn(this.binary, cmdArgs, true);
1469
1509
  proc = this._spawn(command, spawnArgs, {
1470
1510
  cwd,
1471
1511
  env: process.env, // inherits CLI login session; no ANTHROPIC_API_KEY injected
1472
1512
  stdio: ["pipe", "pipe", "pipe"],
1473
- shell: false,
1474
- windowsHide: true,
1513
+ ...spawnOptions,
1475
1514
  });
1476
1515
  } catch (e) {
1477
1516
  job.status = "failed";
@@ -1841,15 +1880,12 @@ export class DelegationLane {
1841
1880
 
1842
1881
  let proc;
1843
1882
  try {
1844
- const isCmdShim = process.platform === "win32" && /\.cmd$/i.test(this.binary);
1845
- const command = isCmdShim ? "cmd" : this.binary;
1846
- const spawnArgs = isCmdShim ? ["/d", "/s", "/c", `"${this.binary}"`, ...cmdArgs] : cmdArgs;
1883
+ const { command, spawnArgs, spawnOptions } = buildClaudeSpawn(this.binary, cmdArgs, true);
1847
1884
  proc = this._spawn(command, spawnArgs, {
1848
1885
  cwd,
1849
1886
  env: process.env, // inherits CLI login; never sets ANTHROPIC_API_KEY
1850
1887
  stdio: ["pipe", "pipe", "pipe"],
1851
- shell: false,
1852
- windowsHide: true,
1888
+ ...spawnOptions,
1853
1889
  });
1854
1890
  } catch (e) {
1855
1891
  return resolve({