[Bug]: Windows terminal ignores an empty lowercase colorterm override #36004
Send to my agentSummary#
An explicit empty terminal color setting is ignored on Windows when its variable name uses lowercase. The equivalent uppercase setting is preserved, so changing only the spelling unexpectedly re-enables the terminal's truecolor advertisement. I reproduced the inconsistency by executing T3's environment-construction helper from source; a native Windows shell was not launched.
Before submitting#
- Related issues: listed under References; searched open and closed issues and pull requests for COLORTERM, empty values, and Windows casing without finding this override inconsistency.
- Removed credentials, tokens, private paths, and sensitive data.
Area#
apps/server
Actual behavior#
With a parent environment containing COLORTERM=truecolor, the Windows branch of createTerminalSpawnEnv preserves runtime { COLORTERM: "" } as an empty value but converts runtime { colorterm: "" } back to COLORTERM=truecolor. A lowercase nonempty override, { colorterm: "8bit" }, is preserved correctly. These are observed results from executing the production helper and its production path-expansion dependency in isolation, not a native Windows shell observation.
The supported trigger is terminal.open on a Windows environment with a valid cwd, a new terminal ID, no provider instance, and env: { colorterm: "" } while the server inherits COLORTERM=truecolor. The environment schema accepts lowercase names and empty string values: packages/contracts/src/terminal.ts#L42-L73. Without a provider instance the environment passes through unchanged, is stored as runtimeEnv, reaches the helper, and is passed to the PTY adapter: apps/server/src/terminal/Manager.ts#L1593-L1614, apps/server/src/terminal/Manager.ts#L2926-L2975, apps/server/src/terminal/Manager.ts#L2540-L2546, apps/server/src/terminal/Manager.ts#L2474-L2482.
Steps to reproduce#
- Use the checkout identified under Version and Node.js with
node:module'sstripTypeScriptTypessupport. - Save the script below as
repro-colorterm.mjsoutside the checkout and runnode repro-colorterm.mjs /path/to/t3code. - Observe that the default, uppercase empty override, and lowercase nonempty override controls pass, but the lowercase empty override produces
COLORTERM=truecolorand exit status 1.
The focused script was run once and reproduced the discrepancy. An earlier combined source-helper and engine smoke probe observed the same input/output pair. Native Windows execution, the full TerminalManager.open Effect graph, and a downstream color-aware application were not run.
The script reads the checkout without changing it, extracts and executes the production environment helper plus its actual dependencies, and creates disposable HOME, TMPDIR, and T3CODE_HOME directories that it removes on exit. It needs no repository dependency installation and starts no server or shell. Exit 0 means the empty override is preserved, exit 1 means the defect reproduced, and exit 2 means setup or a control failed.
Reproduction script: repro-colorterm.mjs
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, readFile, rm } from 'node:fs/promises';
import { stripTypeScriptTypes } from 'node:module';
import path from 'node:path';
import os from 'node:os';
const root = path.resolve(process.argv[2] ?? '.');
const scratch = await mkdtemp(path.join(os.tmpdir(), 't3-colorterm-'));
let result = 'inconclusive';
let status = 2;
try {
for (const dir of ['home', 'tmp', 'state']) await mkdir(path.join(scratch, dir));
process.env.HOME = path.join(scratch, 'home');
process.env.TMPDIR = path.join(scratch, 'tmp');
process.env.T3CODE_HOME = path.join(scratch, 'state');
const read = (file) => readFile(path.join(root, file), 'utf8');
const moduleUrl = (source) => 'data:text/javascript;base64,' +
Buffer.from(stripTypeScriptTypes(source)).toString('base64');
const manager = await read('apps/server/src/terminal/Manager.ts');
const begin = manager.indexOf('function shouldExcludeTerminalEnvKey(');
const end = manager.indexOf('function normalizedRuntimeEnv(');
assert.ok(begin >= 0 && end > begin, 'Could not locate environment helpers');
const blocklist = manager.match(/const TERMINAL_ENV_BLOCKLIST = new Set\(\[[\s\S]*?\]\);/);
assert.ok(blocklist, 'Could not locate terminal environment blocklist');
const expansion = moduleUrl(await read('packages/provider-core/src/server/pathExpansion.ts'));
const packageJson = JSON.parse(await read('apps/server/package.json'));
const source = `import { expandHomeVariable } from '${expansion}';
const packageJson = ${JSON.stringify({ version: packageJson.version })};
${blocklist[0]}
${manager.slice(begin, end)}
export { createTerminalSpawnEnv };`;
const { createTerminalSpawnEnv: make } = await import(moduleUrl(source));
console.log('Node ' + process.version);
console.log('T3 server package ' + packageJson.version);
const parent = Object.freeze({ COLORTERM: 'truecolor' });
const create = (env) => make(parent, env, 'win32', process.env.HOME);
assert.equal(create(undefined).COLORTERM, 'truecolor');
assert.equal(create({ COLORTERM: '' }).COLORTERM, '');
assert.equal(create({ colorterm: '8bit' }).COLORTERM, '8bit');
console.log('PASS controls: default truecolor; uppercase empty preserved; lowercase nonempty override preserved');
const actual = create({ colorterm: '' });
const values = Object.entries(actual).filter(([key]) => key.toLowerCase() === 'colorterm');
console.log('Input runtime env: ' + JSON.stringify({ colorterm: '' }));
console.log('Actual spawn env: ' + JSON.stringify(Object.fromEntries(values)));
assert.deepEqual(parent, { COLORTERM: 'truecolor' });
if (values.length === 1 && values[0][1] === '') {
result = 'expected behavior';
status = 0;
} else {
result = 'defect reproduced';
status = 1;
}
} catch (error) {
console.log('Setup/control failure: ' + error.message);
} finally {
await rm(scratch, { recursive: true, force: true });
}
// Final output is RESULT: expected behavior, RESULT: defect reproduced, or RESULT: inconclusive.
console.log('RESULT: ' + result);
process.exitCode = status;
Expected behavior#
An explicit empty colorterm override on Windows should have the same effect as an explicit empty COLORTERM override: the outgoing color advertisement remains empty. The runtime-overlay code explicitly matches existing environment keys case-insensitively on Windows, and the defaulting condition deliberately exempts an explicit empty canonical runtime value: apps/server/src/terminal/Manager.ts#L1427-L1442. The successful controls establish both behaviors in the same production helper.
Impact#
Impact: P2 - A valid Windows terminal environment override is silently ignored, restoring a capability advertisement the caller explicitly cleared; the uppercase spelling is a safe, verified workaround at the environment-construction boundary.
Bug report form impact: Minor bug or occasional failure. The demonstrated effect is limited to environment construction with the Windows platform selected and an inherited canonical COLORTERM key. No downstream application failure, startup hang, or native Windows rendering defect is claimed. Use env: { COLORTERM: "" } to retain the empty setting; that control passed.
Version or commit#
T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93, server package 0.1.9. The same helper source was also present in the current default-branch file retrieved during investigation.
Environment#
Probe host: macOS 27.2, arm64, Node.js v26.12.1. The probe selects the helper's win32 branch without launching ConPTY. NOT_ENOUGH_INFO: native Windows shell output was not collected; a Windows terminal.open run followed by reading the child environment would verify that end-to-end boundary.
Interface#
API: terminal.open environment overrides; isolated execution of its production environment-construction helper.
Earliest failure stage#
Setup or configuration
Evidence#
Node v26.12.1
T3 server package 0.1.9
PASS controls: default truecolor; uppercase empty preserved; lowercase nonempty override preserved
Input runtime env: {"colorterm":""}
Actual spawn env: {"COLORTERM":"truecolor"}
RESULT: defect reproduced
The process exited 1. Node also emitted its standard experimental warning for stripTypeScriptTypes; the warning did not prevent the controls or failing input from executing. No private identity or machine path was needed in this output.
- Expected source: the case-insensitive Windows runtime overlay and explicit canonical-empty exemption in apps/server/src/terminal/Manager.ts#L1427-L1442.
- Failure source: the source-helper execution above; the overlay writes the empty value to the inherited
COLORTERMspelling, then the case-sensitiveruntimeEnv?.COLORTERMguard permits the default to replace it. - Evidence provenance: observed, for isolated helper input/output; static, for reachability from the public terminal API.
- Local verification: reproduced at the helper boundary; native Windows runtime reproduction was not run.
- Reproduction completeness: complete for the outgoing environment-construction contract; incomplete for native Windows child-process behavior.
- Missing fact: the resulting environment in a real Windows terminal child was not observed.
Restoration check#
With the same parent environment and Windows platform, both { COLORTERM: "" } and { colorterm: "" } must produce one case-insensitively matching color variable whose value is empty. The missing-override control must still default to truecolor, and the lowercase nonempty override must still be preserved. Native Windows verification should read the child environment after opening the terminal; it is additional verification, not an observation already made here.
References#
- https://github.com/pingdotgg/t3code/issues/7648 — reports missing truecolor advertisement; this report concerns an explicit empty override being ignored only for one spelling.
- https://github.com/pingdotgg/t3code/pull/7680 — establishes truecolor defaulting and preservation of configured values; its report and discussion do not cover an empty lowercase Windows override.
- https://github.com/pingdotgg/t3code/pull/29539 — concerns setup-script color opt-outs leaking into a retained shell; this report reaches the shared environment helper directly through terminal-open input and needs no setup script.
tanstack-source added labelsbug
github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:denounced
Sign in to join this conversation.