fix: sanitize __NEXT_PRIVATE_* env vars from child processes (#161)

* fix: preserve paragraph breaks between text blocks in bridge responses

When Claude's response includes tool calls (Read, Edit, Bash, etc.),
the response text is split into multiple content blocks — one before
each tool call and one after. These blocks were joined with an empty
string (.join('')), causing all paragraphs to concatenate without
any spacing in the final Telegram message.

Changed .join('') to .join('\n\n') so paragraph breaks between text
blocks are preserved, matching how they appear in the desktop UI.

* fix: sanitize __NEXT_PRIVATE_* env vars from child processes

The bundled Next.js standalone server sets __NEXT_PRIVATE_STANDALONE_CONFIG
and __NEXT_PRIVATE_ORIGIN in process.env at runtime. These leak into every
child process spawned by the Electron main process (Claude Code CLI,
node version checks, package installs, etc.), which in turn pollutes
tmux/terminal sessions.

When other Next.js projects detect __NEXT_PRIVATE_STANDALONE_CONFIG in their
environment, they skip normal next.config loading and use CodePilot's baked-in
config instead — silently breaking builds and dev servers system-wide.

Fix: strip all __NEXT_PRIVATE_* variables when constructing env objects for
child processes. The standalone server.js already sets these internally, so
they don't need to be passed from the parent.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
authored and GitHub committed 2026-03-05 10:59:39 +08:00
1 parent 113e15e2f4
commit 315aac0f04
2 files changed
+24 -5

No files matched your search

+22 -3
View File
@@ -5,6 +5,25 @@ import fs from 'fs';
import net from 'net';
import os from 'os';
/**
* Return a copy of process.env without __NEXT_PRIVATE_* variables.
*
* The bundled Next.js standalone server sets these at runtime
* (e.g. __NEXT_PRIVATE_STANDALONE_CONFIG, __NEXT_PRIVATE_ORIGIN).
* If they leak into child-process environments they cause every
* other Next.js project on the machine to skip its own config
* loading, breaking builds and dev servers.
*/
function sanitizedProcessEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith('__NEXT_PRIVATE_') && value !== undefined) {
env[key] = value;
}
}
return env;
}
let mainWindow: BrowserWindow | null = null;
let serverProcess: Electron.UtilityProcess | null = null;
let serverPort: number | null = null;
@@ -388,7 +407,7 @@ function startServer(port: number): Electron.UtilityProcess {
const env: Record<string, string> = {
...userShellEnv,
...(process.env as Record<string, string>),
...sanitizedProcessEnv(),
// Ensure user shell env vars override (especially API keys)
...userShellEnv,
PORT: String(port),
@@ -551,7 +570,7 @@ app.whenReady().then(async () => {
ipcMain.handle('install:check-prerequisites', async () => {
const expandedPath = getExpandedShellPath();
const execEnv = { ...process.env, ...userShellEnv, PATH: expandedPath };
const execEnv = { ...sanitizedProcessEnv(), ...userShellEnv, PATH: expandedPath };
const execOpts = { timeout: 5000, encoding: 'utf-8' as const, env: execEnv };
let hasNode = false;
@@ -615,7 +634,7 @@ app.whenReady().then(async () => {
const expandedPath = getExpandedShellPath();
const execEnv: Record<string, string> = {
...userShellEnv,
...(process.env as Record<string, string>),
...sanitizedProcessEnv(),
...userShellEnv,
PATH: expandedPath,
};
+2 -2
View File
@@ -363,7 +363,7 @@ async function consumeStream(
: contentBlocks
.filter((b): b is Extract<MessageContentBlock, { type: 'text' }> => b.type === 'text')
.map((b) => b.text)
.join('')
.join('\n\n')
.trim();
if (content) {
@@ -400,7 +400,7 @@ async function consumeStream(
: contentBlocks
.filter((b): b is Extract<MessageContentBlock, { type: 'text' }> => b.type === 'text')
.map((b) => b.text)
.join('')
.join('\n\n')
.trim();
if (content) {
addMessage(sessionId, 'assistant', content);