claudish 12.3.2 → 13.0.1

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/bin/claudish.cjs CHANGED
@@ -36,6 +36,13 @@
36
36
 
37
37
  const { execFileSync, execSync, spawn } = require("node:child_process");
38
38
  const { resolve } = require("node:path");
39
+ const { NO_ENV_FILE_FLAG, envFileKeys, probeFailedNotice } = require("./env-files.cjs");
40
+
41
+ /**
42
+ * The names `childEnv` left out, handed to the child so it prints ONE line for
43
+ * everything ignored, after its own `.env` load (src/launch-env.ts reads and deletes it).
44
+ */
45
+ const LAUNCHER_DROPPED_VAR = "CLAUDISH_LAUNCHER_DROPPED";
39
46
 
40
47
  /**
41
48
  * The shell command that asks PATH where bun is.
@@ -90,6 +97,44 @@ function findBun(platform = process.platform, env = process.env) {
90
97
  return null;
91
98
  }
92
99
 
100
+ /**
101
+ * The arguments Bun is started with. `--no-env-file` comes first: without it Bun
102
+ * loads `.env`, `.env.local`, `.env.<NODE_ENV>` and `.env.<NODE_ENV>.local` from the
103
+ * caller's working directory into the environment before claudish runs, and that
104
+ * directory is often a checkout nobody has reviewed. claudish reads the provider API
105
+ * keys in `.env` itself, and nothing else (src/dotenv-allowlist.ts).
106
+ */
107
+ function bunArgv(entry, argv) {
108
+ return [NO_ENV_FILE_FLAG, entry, ...argv];
109
+ }
110
+
111
+ /**
112
+ * The environment the Bun child is given, or null when it cannot be built safely.
113
+ *
114
+ * Node never loads env files, so under Node this is the launcher's own environment.
115
+ * Bun does, unless it was started with `--no-env-file`: `bun <path>/claudish.cjs`
116
+ * puts the checkout's env files into THIS process, and the child's `--no-env-file`
117
+ * would then vouch for them as the real environment. So under such a Bun every name
118
+ * those files set is left out (env-files.cjs asks Bun which). A real value sharing a
119
+ * name with a file's is left out too, the safe direction; the child still reads the
120
+ * provider API keys and model settings in `.env`. When Bun cannot answer, there is no child.
121
+ */
122
+ function childEnv({ isBun, execArgv, bunPath, cwd, env }) {
123
+ const out = { ...env };
124
+ delete out[LAUNCHER_DROPPED_VAR];
125
+ if (!isBun || execArgv.includes(NO_ENV_FILE_FLAG)) return { env: out, dropped: [] };
126
+ const fileKeys = envFileKeys({ bunPath, execArgv, cwd, env });
127
+ if (fileKeys === null) return null;
128
+ const dropped = [];
129
+ for (const key of fileKeys) {
130
+ if (out[key] === undefined) continue;
131
+ delete out[key];
132
+ dropped.push(key);
133
+ }
134
+ if (dropped.length > 0) out[LAUNCHER_DROPPED_VAR] = dropped.join(",");
135
+ return { env: out, dropped };
136
+ }
137
+
93
138
  function main() {
94
139
  const bun = findBun();
95
140
  if (!bun) {
@@ -107,9 +152,21 @@ Learn more: https://bun.sh`);
107
152
 
108
153
  const entry = resolve(__dirname, "..", "dist", "index.js");
109
154
 
155
+ const base = childEnv({
156
+ isBun: process.versions.bun !== undefined,
157
+ execArgv: process.execArgv,
158
+ bunPath: process.execPath,
159
+ cwd: process.cwd(),
160
+ env: process.env,
161
+ });
162
+ if (base === null) {
163
+ console.error(probeFailedNotice());
164
+ process.exit(1);
165
+ }
166
+
110
167
  let child;
111
168
  try {
112
- child = spawn(bun, [entry, ...process.argv.slice(2)], {
169
+ child = spawn(bun, bunArgv(entry, process.argv.slice(2)), {
113
170
  stdio: "inherit",
114
171
  // The pid pair tells the Bun child who launched it: this launcher, and the
115
172
  // launcher's own parent (Claude Code, for an MCP server). The child
@@ -117,7 +174,7 @@ Learn more: https://bun.sh`);
117
174
  // (channel/parent-session.ts `hostPidFrom`), so a value leaked into a
118
175
  // nested claudish is inert.
119
176
  env: {
120
- ...process.env,
177
+ ...base.env,
121
178
  CLAUDISH_LAUNCHER_PID: String(process.pid),
122
179
  CLAUDISH_LAUNCHER_PPID: String(process.ppid),
123
180
  },
@@ -180,5 +237,5 @@ Learn more: https://bun.sh`);
180
237
  if (require.main === module) {
181
238
  main();
182
239
  } else {
183
- module.exports = { findBun, lookupCommand, bunCandidates, installHint };
240
+ module.exports = { findBun, lookupCommand, bunCandidates, installHint, bunArgv, childEnv };
184
241
  }
@@ -0,0 +1,104 @@
1
+ // Which environment names Bun's env-file autoload sets in a directory — asked of Bun.
2
+ //
3
+ // Bun loads `.env`, `.env.local`, `.env.<NODE_ENV>` and `.env.<NODE_ENV>.local` (plus
4
+ // any `--env-file`) into `process.env` before a script runs, and marks nothing as
5
+ // coming from a file. Every claudish launch path turns that off with `--no-env-file`.
6
+ // When a process was started without it anyway (`bun <path>/claudish.cjs`,
7
+ // `bun <path>/index.ts`), claudish cannot recover the real environment, but it can
8
+ // learn which NAMES the files set, and drop those. Bun is asked rather than imitated:
9
+ // its file list and its parser differ from dotenv's and change between releases, and a
10
+ // re-implementation of either is how an earlier check was bypassed.
11
+ //
12
+ // The probe runs Bun in the same directory with an empty environment and prints the
13
+ // names it ends up with. It runs once with NODE_ENV unset and once with the current
14
+ // value, because NODE_ENV picks the file list and a `.env` may itself have set it.
15
+ // `--config=<null device>` keeps the directory's `bunfig.toml` out, so the probe runs
16
+ // no `preload` from the checkout; a bunfig can only turn the autoload off, which makes
17
+ // this answer a superset of what was loaded, the safe direction.
18
+ //
19
+ // Shared on purpose: the npm launcher requires it, and so does `src/launch-env.ts`
20
+ // (lazily, from the package's `bin/`), so there is one implementation.
21
+
22
+ const { spawnSync } = require("node:child_process");
23
+ const { devNull } = require("node:os");
24
+
25
+ const PROBE_TIMEOUT_MS = 15_000;
26
+ const PRINT_NAMES = "JSON.stringify(Object.keys(process.env))";
27
+
28
+ /** Bun's flag that turns its env-file autoload off. */
29
+ const NO_ENV_FILE_FLAG = "--no-env-file";
30
+
31
+ /** The `--env-file` arguments in Bun's own argv, so the probe loads the same files. */
32
+ function envFileArgs(execArgv) {
33
+ const out = [];
34
+ for (let i = 0; i < execArgv.length; i++) {
35
+ const arg = execArgv[i];
36
+ if (arg.startsWith("--env-file=")) out.push(arg);
37
+ else if (arg === "--env-file" && i + 1 < execArgv.length) out.push(arg, execArgv[++i]);
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** The minimal environment a probe needs to start on this platform. */
43
+ function probeBaseEnv(platform, env) {
44
+ if (platform !== "win32") return {};
45
+ const base = {};
46
+ for (const key of ["SystemRoot", "SYSTEMROOT"]) if (env[key]) base[key] = env[key];
47
+ return base;
48
+ }
49
+
50
+ /** One probe run: the names Bun's process has under `runEnv`, or null on any failure. */
51
+ function probeNames(bunPath, args, cwd, runEnv) {
52
+ const r = spawnSync(bunPath, args, {
53
+ cwd,
54
+ env: runEnv,
55
+ encoding: "utf-8",
56
+ stdio: ["ignore", "pipe", "ignore"],
57
+ timeout: PROBE_TIMEOUT_MS,
58
+ windowsHide: true,
59
+ });
60
+ if (r.status !== 0 || typeof r.stdout !== "string") return null;
61
+ try {
62
+ const parsed = JSON.parse(r.stdout.trim().split("\n").pop() || "");
63
+ return Array.isArray(parsed) ? parsed.filter((name) => typeof name === "string") : null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * The names Bun's env-file loading sets in `cwd` for a Bun started with `execArgv`.
71
+ * Returns null when Bun could not answer: the caller must then fail closed.
72
+ */
73
+ function envFileKeys({ bunPath, execArgv, cwd, env, platform = process.platform }) {
74
+ const base = probeBaseEnv(platform, env);
75
+ const runs = [base];
76
+ if (env.NODE_ENV !== undefined) runs.push({ ...base, NODE_ENV: env.NODE_ENV });
77
+ const args = [...envFileArgs(execArgv), `--config=${devNull}`, "--print", PRINT_NAMES];
78
+ const names = new Set();
79
+ for (const runEnv of runs) {
80
+ const found = probeNames(bunPath, args, cwd, runEnv);
81
+ if (found === null) return null;
82
+ for (const name of found) if (!Object.hasOwn(runEnv, name)) names.add(name);
83
+ }
84
+ return [...names];
85
+ }
86
+
87
+ /** The one stderr line for names dropped because Bun's env-file autoload was on. */
88
+ function droppedByAutoloadNotice(names) {
89
+ const them = names.length === 1 ? "it" : "them";
90
+ return `[claudish] ignoring ${names.join(", ")}: Bun loaded this directory's env files before claudish started, so a repository may have set ${them}. claudish reads provider API keys and model settings from .env itself. Run the installed claudish command, or start Bun with ${NO_ENV_FILE_FLAG}.`;
91
+ }
92
+
93
+ /** The one stderr line when Bun could not say which names its env files set. */
94
+ function probeFailedNotice() {
95
+ return `[claudish] Bun loaded this directory's env files before claudish started, and claudish could not tell which values came from them, so it will not start. Run the installed claudish command, or start Bun with ${NO_ENV_FILE_FLAG}.`;
96
+ }
97
+
98
+ module.exports = {
99
+ NO_ENV_FILE_FLAG,
100
+ envFileArgs,
101
+ envFileKeys,
102
+ droppedByAutoloadNotice,
103
+ probeFailedNotice,
104
+ };