Yeachan-Heo/oh-my-codex · error · Error
invalid detached leader parent environment
Error message
invalid detached leader parent environment
What it means
Thrown when the optional parentEnv map contains an invalid entry: a key failing SHELL_ENV_NAME_PATTERN, a key in the forbidden DETACHED_SESSION_PANE_ENV_KEYS set, a non-string value, or a value containing a NUL byte.
Source
Thrown at src/cli/index.ts:6963
export function decodeDetachedLeaderPayload(encoded: string | undefined): DetachedLeaderPayload {
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded)) throw new Error("invalid detached leader payload");
let value: unknown;
try { value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); } catch { throw new Error("invalid detached leader payload"); }
if (!value || typeof value !== "object") throw new Error("invalid detached leader payload");
const payload = value as Record<string, unknown>;
const options = payload.preLaunchOptions;
if (typeof payload.cwd !== "string" || typeof payload.sessionName !== "string" || typeof payload.sessionId !== "string" ||
typeof payload.codexCmd !== "string" || !options || typeof options !== "object" ||
typeof (options as Record<string, unknown>).enableNotifyFallbackAuthority !== "boolean" ||
typeof (options as Record<string, unknown>).worktreeDirty !== "boolean" ||
typeof (options as Record<string, unknown>).shouldAttach !== "boolean") throw new Error("invalid detached leader payload");
const notifyTempContract = (options as Record<string, unknown>).notifyTempContract;
if (notifyTempContract !== undefined && (!notifyTempContract || typeof notifyTempContract !== "object")) throw new Error("invalid detached leader payload");
const parentEnv = payload.parentEnv;
if (parentEnv !== undefined && (!parentEnv || typeof parentEnv !== "object" ||
Object.entries(parentEnv).some(([key, value]) => !SHELL_ENV_NAME_PATTERN.test(key) || DETACHED_SESSION_PANE_ENV_KEYS.has(key) || typeof value !== "string" || value.includes("\0")))) {
throw new Error("invalid detached leader parent environment");
}
return {
cwd: payload.cwd, sessionName: payload.sessionName, sessionId: payload.sessionId, codexCmd: payload.codexCmd,
...(typeof payload.codexHomeOverride === "string" ? { codexHomeOverride: payload.codexHomeOverride } : {}),
...(typeof payload.projectLocalCodexHomeForCleanup === "string" ? { projectLocalCodexHomeForCleanup: payload.projectLocalCodexHomeForCleanup } : {}),
...(typeof payload.runtimeCodexHomeForCleanup === "string" ? { runtimeCodexHomeForCleanup: payload.runtimeCodexHomeForCleanup } : {}),
...(parentEnv ? { parentEnv: parentEnv as Record<string, string> } : {}),
...(typeof payload.readyPath === "string" ? { readyPath: payload.readyPath } : {}),
preLaunchOptions: {
...(notifyTempContract ? { notifyTempContract: notifyTempContract as NotifyTempContract } : {}),
enableNotifyFallbackAuthority: (options as DetachedLeaderPreLaunchOptions).enableNotifyFallbackAuthority,
worktreeDirty: (options as DetachedLeaderPreLaunchOptions).worktreeDirty,
shouldAttach: (options as DetachedLeaderPreLaunchOptions).shouldAttach,
},
};
}
function parseDetachedHudAuthorityProof(value: unknown): DetachedHudAuthority | undefined {View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Filter parentEnv through the same SHELL_ENV_NAME_PATTERN and drop DETACHED_SESSION_PANE_ENV_KEYS before encoding
- Coerce all values to strings and strip/reject NUL bytes
- Pass only a curated allowlist of env vars instead of the full environment
Example fix
// before
parentEnv: { ...process.env }
// after
parentEnv: Object.fromEntries(Object.entries(process.env).filter(([k, v]) => SHELL_ENV_NAME_PATTERN.test(k) && !DETACHED_SESSION_PANE_ENV_KEYS.has(k) && typeof v === 'string' && !v.includes('\0'))) Defensive patterns
Strategy: validation
Validate before calling
const env: Record<string,string> = {};
for (const [k, v] of Object.entries(process.env)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) continue;
if (['TMUX_PANE','TMUX_SESSION'].includes(k)) continue;
if (typeof v !== 'string' || v.includes('\0')) continue;
env[k] = v;
} Type guard
function isSafeParentEnv(v: unknown): v is Record<string, string> {
return !!v && typeof v === 'object' && Object.entries(v).every(([k, val]) =>
typeof val === 'string' && !val.includes('\0') && /^[A-Za-z_][A-Za-z0-9_]*$/.test(k));
} Prevention
- Never forward process.env wholesale into the payload
- Maintain an explicit env allowlist per session
- Strip NUL bytes at the source when building env from buffers
When it happens
Trigger: payload.parentEnv includes keys with invalid shell names (e.g. containing '=' or leading digits depending on the pattern), reserved pane keys like TMUX_PANE, numeric values, or embedded '\0'.
Common situations: Forwarding the entire process.env verbatim into the detached payload, which picks up odd variables set by CI systems or shells; NUL bytes appear when env values were built from binary buffers.
Related errors
- invalid state key
- Path traversal detected: path is outside the allowed directo
- invalid auth slot name: use 1-64 letters, numbers, '.', '_'
- invalid auth slot path
- Autoresearch goal ${mission.slug} cannot complete until prof
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/fa8adcf72c38fa84.
Report an issue: GitHub.