From 0afa6a07b160220adbb6ca9aa3cbd1f4539fefdf Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:07:08 +0100 Subject: [PATCH 1/3] Add CODEX_HOME .env support; patch Electron bundle; ignore .env --- .gitignore | 2 + scripts/run.ps1 | 197 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 743132d..61455fa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ *.log *.dmg + +.env diff --git a/scripts/run.ps1 b/scripts/run.ps1 index 742b0a7..f6fbfef 100644 --- a/scripts/run.ps1 +++ b/scripts/run.ps1 @@ -2,6 +2,7 @@ param( [string]$DmgPath, [string]$WorkDir = (Join-Path $PSScriptRoot "..\work"), [string]$CodexCliPath, + [string]$CodexHome, [switch]$Reuse, [switch]$NoLaunch ) @@ -9,6 +10,76 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +function Parse-DotEnvValue([string]$Value) { + if ($null -eq $Value) { return $null } + $v = $Value.Trim() + if ($v.StartsWith('"') -and $v.EndsWith('"') -and $v.Length -ge 2) { + return $v.Substring(1, $v.Length - 2) -replace '\"', '"' + } + if ($v.StartsWith("'") -and $v.EndsWith("'") -and $v.Length -ge 2) { + return $v.Substring(1, $v.Length - 2) -replace "''", "'" + } + return $v +} + +function Read-DotEnv([string]$Path) { + $map = @{} + if (-not (Test-Path $Path)) { return $map } + foreach ($line in (Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue)) { + if ($null -eq $line) { continue } + $t = $line.Trim() + if ($t.Length -eq 0) { continue } + if ($t.StartsWith("#")) { continue } + $idx = $t.IndexOf("=") + if ($idx -lt 1) { continue } + $k = $t.Substring(0, $idx).Trim() + if ($k.Length -gt 0 -and $k[0] -eq [char]0xFEFF) { $k = $k.TrimStart([char]0xFEFF) } + $v = $t.Substring($idx + 1) + if (-not $k) { continue } + $map[$k] = (Parse-DotEnvValue $v) + } + return $map +} + +function Set-DotEnvVar([string]$Path, [string]$Key, [string]$Value) { + $dir = Split-Path $Path -Parent + if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } + + $lines = @() + if (Test-Path $Path) { + $lines = @(Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue) + } + + $escaped = ($Value -replace '"', '\"') + $newLine = "$Key=""$escaped""" + + $updated = $false + for ($i = 0; $i -lt $lines.Count; $i++) { + $current = $lines[$i] + if ($null -eq $current) { $current = "" } + $t = $current.Trim() + if ($t.StartsWith("#")) { continue } + if ($t -match ("^\s*" + [regex]::Escape($Key) + "\s*=")) { + $lines[$i] = $newLine + $updated = $true + break + } + } + + if (-not $updated) { + $last = $null + if ($lines.Count -gt 0) { $last = $lines[$lines.Count - 1] } + if ($null -eq $last) { $last = "" } + if ($lines.Count -gt 0 -and $last.Trim().Length -ne 0) { + $lines += "" + } + $lines += $newLine + } + + # Avoid UTF-8 BOM (it can break simple dotenv key parsing). + [System.IO.File]::WriteAllLines($Path, $lines, [System.Text.UTF8Encoding]::new($false)) +} + function Ensure-Command([string]$Name) { if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "$Name not found." @@ -99,6 +170,71 @@ function Resolve-CodexCliPath([string]$Explicit) { return $null } +function Patch-AppMainJs([string]$AppDir) { + $mainJs = Join-Path $AppDir ".vite\\build\\main.js" + if (-not (Test-Path $mainJs)) { return } + + $text = Get-Content -LiteralPath $mainJs -Raw + $insertions = @() + + # Implement "open-config-toml" in Electron by opening (and creating if needed) $CODEX_HOME/config.toml. + if ($text -notmatch "Failed to open config\\.toml") { + $insertions += ';(()=>{try{const e=Sue?.prototype?.handleMessage;if(typeof e!="function")return;Sue.prototype.handleMessage=async function(t,n){if(n?.type==="open-config-toml"){try{const r=[zn({preferWsl:!0}),zn({preferWsl:!1})].filter(i=>typeof i=="string"&&i.length>0),a=r.map(i=>ae.join(i,"config.toml"));let o=a.find(i=>be.existsSync(i))??a[a.length-1];o&&(be.mkdirSync(ae.dirname(o),{recursive:!0}),be.existsSync(o)||be.writeFileSync(o,"# Codex configuration\n","utf8"),await F.shell.openPath(o))}catch(r){try{Ft().error("Failed to open config.toml",r)}catch{}}return}return e.call(this,t,n)}}catch{}})();' + } + + # If a persisted host config points at a Windows npm shim (e.g. %APPDATA%\\npm\\codex), + # patch child_process.spawn to transparently resolve a real codex.exe instead. + if ($text -notmatch "__codexWindowsPatched") { + $insertions += ';(()=>{try{if(process.platform!=="win32")return;const e=require("child_process"),t=require("fs"),n=require("path");if(e.spawn&&e.spawn.__codexWindowsPatched)return;const r=e.spawn;function i(){try{const a=process.env.CODEX_CLI_PATH||process.env.CUSTOM_CLI_PATH;if(a&&/\\.exe$/i.test(a)&&t.existsSync(a))return a}catch{}try{const a=process.env.APPDATA;if(a){for(const o of["x86_64-pc-windows-msvc","aarch64-pc-windows-msvc"]){const s=n.join(a,"npm","node_modules","@openai","codex","vendor",o,"codex","codex.exe");if(t.existsSync(s))return s}}}catch{}return null}e.spawn=function(a,o,s){try{if(typeof a=="string"){const c=n.basename(a.toLowerCase());if(c==="codex"||c==="codex.cmd"||c==="codex.ps1"){const u=i();u&&(a=u)}}}catch{}return r.call(this,a,o,s)};e.spawn.__codexWindowsPatched=!0}catch{}})();' + } + + if ($insertions.Count -eq 0) { return } + + $insertionText = ($insertions -join "`n") + "`n" + $pattern = "(?m)^//# sourceMappingURL=main\\.js\\.map\\s*$" + if ($text -match $pattern) { + $text = [regex]::Replace($text, $pattern, $insertionText + "//# sourceMappingURL=main.js.map", 1) + } else { + $text = $text + "`n" + $insertionText + } + + [System.IO.File]::WriteAllText($mainJs, $text, [System.Text.UTF8Encoding]::new($false)) + + # Patch renderer bundle logging so errors don't show up as "[object Object]". + $assetsDir = Join-Path $AppDir "webview\\assets" + if (Test-Path $assetsDir) { + $indexFiles = Get-ChildItem -LiteralPath $assetsDir -Filter "index-*.js" -File -ErrorAction SilentlyContinue + foreach ($f in $indexFiles) { + try { + $rt = Get-Content -LiteralPath $f.FullName -Raw + $orig = $rt + + # Prefer a more verbose formatter than sanitizeLogValue() for key error logs. + # sanitizeLogValue intentionally collapses objects to "object(keys=N)", which is still too opaque for debugging. + $verboseLt = '${(()=>{try{return typeof lt==="string"?lt:lt&&typeof lt==="object"?JSON.stringify(lt,(k,v)=>v instanceof Error?{name:v.name,message:v.message,stack:v.stack}:v):String(lt)}catch(e){try{return String(lt)}catch(e2){return "unserializable"}}})()}' + $verboseKt = '${(()=>{try{return typeof Kt==="string"?Kt:Kt&&typeof Kt==="object"?JSON.stringify(Kt,(k,v)=>v instanceof Error?{name:v.name,message:v.message,stack:v.stack}:v):String(Kt)}catch(e){try{return String(Kt)}catch(e2){return "unserializable"}}})()}' + + # Update older variants and the intermediate sanitizeLogValue variant. + $rt = $rt.Replace('Received app server error: ${Ye} ${String(lt)}', ('Received app server error: ${Ye} ' + $verboseLt)) + $rt = $rt.Replace('Received app server error: ${Ye} ${sanitizeLogValue(lt)}', ('Received app server error: ${Ye} ' + $verboseLt)) + + $rt = $rt.Replace('[worktree-cleanup] failed to refresh cleanup inputs: ${String(Kt)}', ('[worktree-cleanup] failed to refresh cleanup inputs: ' + $verboseKt)) + $rt = $rt.Replace('[worktree-cleanup] failed to refresh cleanup inputs: ${sanitizeLogValue(Kt)}', ('[worktree-cleanup] failed to refresh cleanup inputs: ' + $verboseKt)) + + # A few other common noisy logs. + $rt = $rt.Replace('[automation-run-cleanup] failed to refresh conversations: ${String(St)}', '[automation-run-cleanup] failed to refresh conversations: ${sanitizeLogValue(St)}') + $rt = $rt.Replace('[automation-run-cleanup] failed to load more conversations: ${String(St)}', '[automation-run-cleanup] failed to load more conversations: ${sanitizeLogValue(St)}') + $rt = $rt.Replace('[automation-run-cleanup] failed to archive conversation ${Et}: ${String(Ct)}', '[automation-run-cleanup] failed to archive conversation ${Et}: ${sanitizeLogValue(Ct)}') + $rt = $rt.Replace('[automation-run-cleanup] failed to mark run archived ${Et}: ${String(Ct)}', '[automation-run-cleanup] failed to mark run archived ${Et}: ${sanitizeLogValue(Ct)}') + + if ($rt -ne $orig) { + [System.IO.File]::WriteAllText($f.FullName, $rt, [System.Text.UTF8Encoding]::new($false)) + } + } catch {} + } + } +} + function Write-Header([string]$Text) { Write-Host "`n=== $Text ===" -ForegroundColor Cyan } @@ -132,6 +268,57 @@ function Ensure-GitOnPath() { } } +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$dotEnvPath = Join-Path $repoRoot ".env" +$dotEnv = Read-DotEnv $dotEnvPath + +$dotEnvCodexHome = if ($dotEnv.ContainsKey("CODEX_HOME")) { $dotEnv["CODEX_HOME"] } else { $null } +$envCodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } elseif ($env:CODEX_Home) { $env:CODEX_Home } else { $null } + +function Normalize-PathString([string]$Path) { + if (-not $Path) { return $null } + $p = [Environment]::ExpandEnvironmentVariables($Path).Trim().Trim('"').Trim("'") + $p = $p -replace "/", "\" + try { + $full = [System.IO.Path]::GetFullPath($p) + return $full.TrimEnd("\") + } catch { + return $p.TrimEnd("\") + } +} + +$defaultCodexHome = Join-Path ([Environment]::GetFolderPath("UserProfile")) ".codex" +$envIsDefaultHome = (Normalize-PathString $envCodexHome) -eq (Normalize-PathString $defaultCodexHome) +$envDiffersFromDotEnv = $dotEnvCodexHome -and ((Normalize-PathString $envCodexHome) -ne (Normalize-PathString $dotEnvCodexHome)) + +# Treat CODEX_HOME from the environment as an explicit override only when: +# - there is no `.env` yet, OR +# - it differs from `.env` AND it's not the default "~/.codex" value (avoids a system default overriding a chosen profile). +$envCodexHomeExplicit = $false +if ($envCodexHome) { + if (-not $dotEnvCodexHome) { $envCodexHomeExplicit = $true } + elseif ($envDiffersFromDotEnv -and -not $envIsDefaultHome) { $envCodexHomeExplicit = $true } +} + +# Precedence: +# - `-CodexHome` (always explicit + persist) +# - env CODEX_HOME (if explicit per rules above + persist) +# - `.env` (persisted default) +# - env CODEX_HOME (system/user env fallback) +$desiredCodexHome = if ($CodexHome) { $CodexHome } elseif ($envCodexHomeExplicit) { $envCodexHome } elseif ($dotEnvCodexHome) { $dotEnvCodexHome } elseif ($envCodexHome) { $envCodexHome } else { $null } +if ($desiredCodexHome) { + Write-Header "Configuring CODEX_HOME" + $resolvedCodexHome = [Environment]::ExpandEnvironmentVariables($desiredCodexHome) + New-Item -ItemType Directory -Force -Path $resolvedCodexHome | Out-Null + $env:CODEX_HOME = (Resolve-Path $resolvedCodexHome).Path + + # Only rewrite `.env` when the user explicitly requests a change. + if ($CodexHome -or $envCodexHomeExplicit) { + Set-DotEnvVar $dotEnvPath "CODEX_HOME" $env:CODEX_HOME + } + Write-Host "CODEX_HOME=$($env:CODEX_HOME)" -ForegroundColor Cyan +} + Ensure-Command node Ensure-Command npm Ensure-Command npx @@ -200,11 +387,15 @@ if (-not $Reuse) { Write-Header "Syncing app.asar.unpacked" $unpacked = Join-Path $electronDir "Codex Installer\Codex.app\Contents\Resources\app.asar.unpacked" - if (Test-Path $unpacked) { - & robocopy $unpacked $appDir /E /NFL /NDL /NJH /NJS /NC /NS | Out-Null - } + if (Test-Path $unpacked) { + & robocopy $unpacked $appDir /E /NFL /NDL /NJH /NJS /NC /NS | Out-Null + } + } +Write-Header "Patching Electron bundle" +Patch-AppMainJs $appDir + Write-Header "Patching preload" Patch-Preload $appDir From 98ee83360fa85bd36bc293552c3d7178bfadd5e7 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:24:53 +0100 Subject: [PATCH 2/3] Preserve root trailing slash in Normalize-PathString --- scripts/run.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/run.ps1 b/scripts/run.ps1 index f6fbfef..37a37cd 100644 --- a/scripts/run.ps1 +++ b/scripts/run.ps1 @@ -281,8 +281,12 @@ function Normalize-PathString([string]$Path) { $p = $p -replace "/", "\" try { $full = [System.IO.Path]::GetFullPath($p) + $root = [System.IO.Path]::GetPathRoot($full) + if ($root -and ($full.Length -le $root.Length)) { return $root } return $full.TrimEnd("\") } catch { + if ($p -match "^[a-zA-Z]:\\$") { return $p } + if ($p -match "^\\\\[^\\]+\\[^\\]+\\$") { return $p } return $p.TrimEnd("\") } } From c1a914d78952e0c58161f950fba8505535af643e Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:27:56 +0100 Subject: [PATCH 3/3] Create .env template and ignore empty dotenv values --- scripts/run.ps1 | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/scripts/run.ps1 b/scripts/run.ps1 index 37a37cd..e88251a 100644 --- a/scripts/run.ps1 +++ b/scripts/run.ps1 @@ -36,11 +36,45 @@ function Read-DotEnv([string]$Path) { if ($k.Length -gt 0 -and $k[0] -eq [char]0xFEFF) { $k = $k.TrimStart([char]0xFEFF) } $v = $t.Substring($idx + 1) if (-not $k) { continue } - $map[$k] = (Parse-DotEnvValue $v) + $parsed = (Parse-DotEnvValue $v) + if ($null -eq $parsed) { continue } + if (($parsed -is [string]) -and ($parsed.Trim().Length -eq 0)) { continue } + $map[$k] = $parsed } return $map } +function Ensure-DotEnvTemplate([string]$Path) { + if (-not $Path) { return } + $dir = Split-Path $Path -Parent + if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } + + $enc = [System.Text.UTF8Encoding]::new($false) + if (-not (Test-Path $Path)) { + $content = @( + "# Codex Windows local config" + "# Set CODEX_HOME to your Codex profile directory (leave empty to use default)." + "CODEX_HOME=" + "" + ) -join "`n" + [System.IO.File]::WriteAllText($Path, $content, $enc) + return + } + + try { + $text = Get-Content -LiteralPath $Path -Raw -ErrorAction SilentlyContinue + } catch { + return + } + if ($null -eq $text) { $text = "" } + if ($text -match "(?m)^\\s*CODEX_HOME\\s*=") { return } + + if ($text.Length -gt 0 -and -not $text.EndsWith("`n")) { $text += "`n" } + if ($text.Length -gt 0 -and -not $text.EndsWith("`n`n")) { $text += "`n" } + $text += "CODEX_HOME=`n" + [System.IO.File]::WriteAllText($Path, $text, $enc) +} + function Set-DotEnvVar([string]$Path, [string]$Key, [string]$Value) { $dir = Split-Path $Path -Parent if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } @@ -270,6 +304,7 @@ function Ensure-GitOnPath() { $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $dotEnvPath = Join-Path $repoRoot ".env" +Ensure-DotEnvTemplate $dotEnvPath $dotEnv = Read-DotEnv $dotEnvPath $dotEnvCodexHome = if ($dotEnv.ContainsKey("CODEX_HOME")) { $dotEnv["CODEX_HOME"] } else { $null }