Codex 在 Windows 上迁移到非系统盘:完整、安全、可回滚指南
🚚 Codex 在 Windows 上迁移到非系统盘:完整、安全、可回滚指南
适用范围:Windows 10/11,Windows 原生 Codex/ChatGPT 桌面应用、Codex CLI、IDE 扩展。
文档版本:2026-08-20。执行前请先完整阅读,尤其是“迁移边界”和“回滚”部分。
🧭 一、先说结论
最稳妥的做法不是手工搬动 WindowsApps,也不是创建目录联接,而是:
- 彻底退出 ChatGPT/Codex、IDE 和所有
codex进程; - 把当前
CODEX_HOME(默认%USERPROFILE%\.codex)复制到非系统盘; - 对相对路径、文件大小做逐项核验,可选 SHA-256 深度核验;
- 将用户级环境变量
CODEX_HOME指向新目录; - 重启应用并运行检查脚本;
- 保留旧目录一段时间,确认稳定后再手工清理。
OpenAI 官方文档说明,CODEX_HOME 的默认值为 ~/.codex,它是 Codex 配置、认证、日志、会话、技能和独立安装包元数据的根目录;若显式设置,新目录必须已经存在。Windows 桌面应用与 Windows 原生 Codex 默认共用 %USERPROFILE%\.codex。参见 环境变量、Windows 桌面应用 和 配置基础。
📦 能迁移与不能直接迁移的内容
| 内容 | 推荐办法 | 说明 |
|---|---|---|
| 配置、登录状态、会话、日志、技能、插件/独立包元数据 | 设置 CODEX_HOME | 本文主要迁移对象 |
| SQLite 状态 | 通常随 CODEX_HOME 迁移 | CODEX_SQLITE_HOME 默认等于 CODEX_HOME;若显式设置,脚本会谨慎处理 |
独立版 codex.exe 的可见命令目录 | 设置 CODEX_INSTALL_DIR 后重新运行官方安装器 | Windows 默认是 %LOCALAPPDATA%\Programs\OpenAI\Codex\bin;包缓存仍在 CODEX_HOME\packages\standalone |
| Microsoft Store/MSIX 桌面应用本体 | 由 Windows“已安装的应用”管理 | 不要取得 WindowsApps 所有权后手工剪切;能否选择其他盘取决于 Windows/组织策略 |
| 项目仓库、源码、构建缓存 | 单独移动项目 | 不属于 CODEX_HOME;移动后需在应用中重新打开项目,并注意 Git/工具链中的绝对路径 |
WSL 内部的 ~/.codex | 单独配置 WSL | Windows 环境变量不会自动进入 WSL;文末给出做法 |
“完整迁移”在本文中指 Codex 的可配置用户状态和可选的独立 CLI 命令位置,不代表强行搬动 Microsoft Store/MSIX 应用包、Windows 沙箱账户或系统级依赖。
🗺️ 迁移后的总体架构
图中实线表示多个 Codex 入口共享的新状态根目录;虚线表示“有关联但不随
CODEX_HOME一起搬迁”的内容。
🔑 关键变量速查
| 变量 | 默认位置 | 本文建议 | 是否必须设置 |
|---|---|---|---|
CODEX_HOME | %USERPROFILE%\.codex | D:\CodexData\.codex | ✅ 是,迁移核心 |
CODEX_SQLITE_HOME | 跟随 CODEX_HOME | 保持未设置,除非原本显式配置 | ❌ 通常不需要 |
CODEX_INSTALL_DIR | %LOCALAPPDATA%\Programs\OpenAI\Codex\bin | D:\Apps\OpenAI\Codex\bin | 🟡 仅迁移独立 CLI 时需要 |
⚡ 最短执行路线
| 阶段 | 要做什么 | 成功标志 | 失败时怎么办 |
|---|---|---|---|
| 1️⃣ 准备 | 退出应用、IDE、CLI,确认目标为本机 NTFS 非系统盘 | 没有 codex/ChatGPT 进程 | 停止迁移,先关闭进程 |
| 2️⃣ 迁移 | 运行 Move-CodexHome.ps1 | 复制核验通过,用户变量已切换 | 不手工切变量,修正问题后重试 |
| 3️⃣ 验证 | 新开 PowerShell,运行 Test-CodexMigration.ps1 | Failures = 0,实际任务正常 | 保留两边数据,排错或回滚 |
| 4️⃣ 观察 | 使用数天,确认只写入新目录 | 新目录持续更新,旧目录不再更新 | 运行回滚脚本 |
| 5️⃣ 清理 | 先把旧目录改名/离线备份,再延迟删除 | 系统盘空间释放且应用稳定 | 从备份恢复 |
🧰 二、推荐目录与迁移前检查
建议使用本机固定 NTFS 盘,例如:
D:\CodexData\.codex
D:\Apps\OpenAI\Codex\bin # 可选:独立 CLI 的命令目录
D:\Projects # 可选:项目仓库,和 CODEX_HOME 分开
不要把 CODEX_HOME 放在 FAT32/exFAT、网络共享、临时盘、容易被拔出的移动盘或云盘“仅联机”目录中。认证资料和会话可能具有敏感性;非系统盘最好启用 BitLocker,并只允许当前用户和管理员访问。
迁移前确认:
- 非系统盘空间充足、盘符长期固定;
- 当前 Codex 工作已经保存,没有仍在运行的任务;
- ChatGPT/Codex 桌面应用、VS Code/JetBrains 等 IDE、所有
codex终端进程均已关闭; - 不要在 Codex 自己仍运行时执行迁移脚本;
- 公司设备若受组策略管理,先确认允许用户环境变量和目标盘读写;
- 记住目标路径。下文示例统一使用
D:\CodexData\.codex,请按实际情况替换。
🚦 迁移前风险信号
| 信号 | 风险 | 处理方式 |
|---|---|---|
| 🔴 Codex/ChatGPT/IDE 仍在运行 | SQLite、日志或会话可能在复制时变化 | 必须完全退出后再执行 |
| 🔴 目标目录已有文件 | 两套状态可能被错误合并 | 换用新的空目录 |
| 🔴 目标是网络盘、移动盘或 exFAT | 权限、锁、掉盘和原子写入不可靠 | 改用本机固定 NTFS 盘 |
| 🟠 源目录含符号链接/目录联接 | 复制边界不明确,可能越界或丢内容 | 人工检查后单独处理 |
| 🟠 公司设备受策略管理 | 用户变量、ACL 或沙箱可能被限制 | 先咨询管理员 |
| 🟡 使用 WSL CLI | Windows 与 WSL 的 home 默认不同 | 阅读“WSL 用户”章节 |
🔄 端到端迁移流程
可先在一个新开的普通 PowerShell 中查看当前状态:
[pscustomobject]@{
Process_CODEX_HOME = $env:CODEX_HOME
User_CODEX_HOME = [Environment]::GetEnvironmentVariable('CODEX_HOME', 'User')
Default_HOME = Join-Path $env:USERPROFILE '.codex'
Codex_Command = (Get-Command codex -ErrorAction SilentlyContinue).Source
} | Format-List
输出可能包含用户名和路径,但不应显示令牌。不要把 auth.json、.sandbox-secrets 或任何访问令牌发给他人。
🚛 三、迁移脚本(复制、核验、切换,默认不删除源目录)
将下面完整代码另存为 Move-CodexHome.ps1,在已关闭所有 Codex/ChatGPT/IDE 进程后,从普通 PowerShell 执行。脚本不会删除旧目录,也不会操作桌面应用包。
#requires -Version 5.1
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory = $true)]
[string]$DestinationPath,
[switch]$DeepVerify
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-FullPath([string]$Path) {
return [IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($Path)).TrimEnd('\')
}
function Test-IsSameOrChild([string]$Candidate, [string]$Parent) {
$c = (Get-FullPath $Candidate) + '\'
$p = (Get-FullPath $Parent) + '\'
return $c.StartsWith($p, [StringComparison]::OrdinalIgnoreCase)
}
function Get-Inventory([string]$Root, [switch]$Hash) {
$rootFull = Get-FullPath $Root
$items = Get-ChildItem -LiteralPath $rootFull -File -Recurse -Force |
Where-Object { $_.FullName -notlike (Join-Path $rootFull '.migration\*') } |
ForEach-Object {
$relative = $_.FullName.Substring($rootFull.Length).TrimStart('\')
$entry = [ordered]@{
RelativePath = $relative
Length = [int64]$_.Length
}
if ($Hash) {
$entry.SHA256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash
}
[pscustomobject]$entry
}
return @($items | Sort-Object RelativePath)
}
function Compare-Inventory($SourceInventory, $DestinationInventory, [switch]$Hash) {
$src = @{}
foreach ($item in $SourceInventory) { $src[$item.RelativePath] = $item }
$dst = @{}
foreach ($item in $DestinationInventory) { $dst[$item.RelativePath] = $item }
$errors = [Collections.Generic.List[string]]::new()
foreach ($key in $src.Keys) {
if (-not $dst.ContainsKey($key)) {
$errors.Add("目标缺少:$key")
continue
}
if ($src[$key].Length -ne $dst[$key].Length) {
$errors.Add("大小不同:$key")
} elseif ($Hash -and $src[$key].SHA256 -ne $dst[$key].SHA256) {
$errors.Add("哈希不同:$key")
}
}
foreach ($key in $dst.Keys) {
if (-not $src.ContainsKey($key)) { $errors.Add("目标多出:$key") }
}
return @($errors)
}
$running = @(Get-Process -Name 'codex', 'ChatGPT' -ErrorAction SilentlyContinue)
if ($running.Count -gt 0) {
$names = ($running | Select-Object -ExpandProperty ProcessName -Unique) -join ', '
throw "检测到仍在运行的进程:$names。请彻底退出应用、IDE 和 CLI 后重试。"
}
$userHomeValue = [Environment]::GetEnvironmentVariable('CODEX_HOME', 'User')
$userHomeWasSet = $null -ne $userHomeValue
if (-not [string]::IsNullOrWhiteSpace($env:CODEX_HOME)) {
$source = Get-FullPath $env:CODEX_HOME
} elseif ($userHomeWasSet -and -not [string]::IsNullOrWhiteSpace($userHomeValue)) {
$source = Get-FullPath $userHomeValue
} else {
$source = Get-FullPath (Join-Path $env:USERPROFILE '.codex')
}
$destination = Get-FullPath $DestinationPath
if (-not (Test-Path -LiteralPath $source -PathType Container)) {
throw "源 CODEX_HOME 不存在:$source"
}
if ($source.Equals($destination, [StringComparison]::OrdinalIgnoreCase)) {
throw '源目录和目标目录相同,无需迁移。'
}
if ((Test-IsSameOrChild $destination $source) -or (Test-IsSameOrChild $source $destination)) {
throw '源目录与目标目录不能互相包含。请选择完全独立的目标目录。'
}
if ($destination.StartsWith('\\')) {
throw '不建议把 CODEX_HOME 放到 UNC/网络路径;请选择本机固定 NTFS 盘。'
}
$destinationRoot = [IO.Path]::GetPathRoot($destination)
if ($destinationRoot.TrimEnd('\').Equals($env:SystemDrive, [StringComparison]::OrdinalIgnoreCase)) {
throw "目标仍位于系统盘 $env:SystemDrive。请指定非系统盘。"
}
$volume = Get-Volume -DriveLetter $destinationRoot.Substring(0, 1) -ErrorAction Stop
if ($volume.FileSystem -ne 'NTFS') {
throw "目标卷文件系统为 $($volume.FileSystem),建议且脚本要求使用 NTFS。"
}
$reparsePoints = @(Get-ChildItem -LiteralPath $source -Recurse -Force -Attributes ReparsePoint -ErrorAction Stop)
if ($reparsePoints.Count -gt 0) {
throw "源目录包含 $($reparsePoints.Count) 个重解析点/符号链接。请先人工检查,脚本为避免复制到意外位置而停止。"
}
$sourceBytes = [int64](Get-ChildItem -LiteralPath $source -File -Recurse -Force |
Measure-Object -Property Length -Sum).Sum
$drive = Get-PSDrive -Name $destinationRoot.Substring(0, 1) -ErrorAction Stop
if ($drive.Free -lt ($sourceBytes + 100MB)) {
throw "目标盘空间不足。数据约 $sourceBytes 字节,建议至少额外保留 100 MB。"
}
if (-not (Test-Path -LiteralPath $destination)) {
if ($PSCmdlet.ShouldProcess($destination, '创建目标 CODEX_HOME')) {
New-Item -ItemType Directory -Path $destination -Force | Out-Null
}
}
if ((Get-ChildItem -LiteralPath $destination -Force -ErrorAction Stop | Measure-Object).Count -gt 0) {
throw "目标目录不是空目录:$destination。请换一个新目录,避免覆盖未知数据。"
}
Write-Host "源目录:$source"
Write-Host "目标目录:$destination"
Write-Host '正在复制(不会删除源目录)……'
if ($PSCmdlet.ShouldProcess($destination, '复制 Codex 状态')) {
& robocopy.exe $source $destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:1 /XJ /NP
$robocopyCode = $LASTEXITCODE
if ($robocopyCode -ge 8) {
throw "Robocopy 失败,退出码:$robocopyCode。环境变量尚未切换。"
}
}
Write-Host ($(if ($DeepVerify) { '正在执行 SHA-256 深度核验……' } else { '正在核验相对路径和文件大小……' }))
$sourceInventory = Get-Inventory $source -Hash:$DeepVerify
$destinationInventory = Get-Inventory $destination -Hash:$DeepVerify
$differences = Compare-Inventory $sourceInventory $destinationInventory -Hash:$DeepVerify
if ($differences.Count -gt 0) {
$preview = ($differences | Select-Object -First 20) -join [Environment]::NewLine
throw "复制核验失败,共 $($differences.Count) 项差异。前 20 项:`n$preview`n环境变量尚未切换。"
}
$oldSqliteHome = [Environment]::GetEnvironmentVariable('CODEX_SQLITE_HOME', 'User')
$oldSqliteWasSet = $null -ne $oldSqliteHome
$newSqliteHome = $oldSqliteHome
if ($oldSqliteWasSet -and -not [string]::IsNullOrWhiteSpace($oldSqliteHome)) {
$oldSqliteFull = Get-FullPath $oldSqliteHome
if ($oldSqliteFull.Equals($source, [StringComparison]::OrdinalIgnoreCase)) {
$newSqliteHome = $destination
} elseif (Test-IsSameOrChild $oldSqliteFull $source) {
$suffix = $oldSqliteFull.Substring($source.Length).TrimStart('\')
$newSqliteHome = Join-Path $destination $suffix
}
}
$manifestDir = Join-Path $destination '.migration'
New-Item -ItemType Directory -Path $manifestDir -Force | Out-Null
$manifestPath = Join-Path $manifestDir 'codex-home-migration.json'
$manifest = [ordered]@{
SchemaVersion = 1
MigratedAtUtc = [DateTime]::UtcNow.ToString('o')
OriginalHome = $source
DestinationHome = $destination
OldUserCodexHomeWasSet = $userHomeWasSet
OldUserCodexHome = $userHomeValue
OldUserSqliteWasSet = $oldSqliteWasSet
OldUserSqliteHome = $oldSqliteHome
NewUserSqliteHome = $newSqliteHome
VerifiedWithSHA256 = [bool]$DeepVerify
SourceFileCount = $sourceInventory.Count
SourceBytes = $sourceBytes
}
$manifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
if ($PSCmdlet.ShouldProcess('用户环境变量 CODEX_HOME', "设置为 $destination")) {
[Environment]::SetEnvironmentVariable('CODEX_HOME', $destination, 'User')
$env:CODEX_HOME = $destination
if ($oldSqliteWasSet -and $newSqliteHome -ne $oldSqliteHome) {
[Environment]::SetEnvironmentVariable('CODEX_SQLITE_HOME', $newSqliteHome, 'User')
$env:CODEX_SQLITE_HOME = $newSqliteHome
}
}
Write-Host ''
Write-Host '迁移复制、核验和环境变量切换已完成。' -ForegroundColor Green
Write-Host "迁移清单:$manifestPath"
Write-Host '请关闭当前 PowerShell,新开 PowerShell,再启动 Codex/ChatGPT 并运行检查脚本。'
Write-Host "旧目录仍保留在:$source"
普通核验运行示例:
Set-ExecutionPolicy -Scope Process Bypass
.\Move-CodexHome.ps1 -DestinationPath 'D:\CodexData\.codex'
逐文件 SHA-256 深度核验(数据多时会明显变慢):
.\Move-CodexHome.ps1 -DestinationPath 'D:\CodexData\.codex' -DeepVerify
脚本的安全特性:目标必须是非系统盘的本机 NTFS 空目录;源和目标不能互相包含;发现运行中的 Codex/ChatGPT 或重解析点会停止;复制核验通过后才切换环境变量;源目录不会被删除。
🛡️ 脚本保护机制
| 保护机制 | 防止的问题 | 触发后的行为 |
|---|---|---|
| 进程检查 | 复制过程中数据库和日志继续变化 | 立即停止,不复制 |
| 空目标检查 | 两套 Codex 状态混合 | 立即停止,要求新目录 |
| 路径包含检查 | 把目标放进源目录导致递归复制 | 立即停止 |
| NTFS/非系统盘检查 | 权限不兼容或迁移目标选错 | 立即停止 |
| 重解析点检查 | 跟随目录联接复制到意外位置 | 立即停止,要求人工检查 |
| Robocopy 退出码检查 | 部分文件未复制 | 不切换环境变量 |
| 文件清单或 SHA-256 核验 | 静默损坏、漏文件 | 不切换环境变量 |
| 保留旧目录 | 登录、会话或沙箱异常 | 可完整回滚 |
✅ 四、重启与检查脚本
迁移脚本完成后:
- 关闭执行迁移的 PowerShell;
- 新开 PowerShell,确认
$env:CODEX_HOME已指向新盘; - 启动 ChatGPT/Codex;
- 确认历史任务、设置、技能和登录状态可用;
- 退出应用,再运行下面的检查脚本。检查时退出应用可以避免 SQLite/日志文件变化导致误判。
将下面代码另存为 Test-CodexMigration.ps1:
#requires -Version 5.1
[CmdletBinding()]
param(
[string]$ExpectedHome,
[switch]$DeepVerifyInitialCopy
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$failures = [Collections.Generic.List[string]]::new()
$warnings = [Collections.Generic.List[string]]::new()
function Full([string]$Path) {
[IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($Path)).TrimEnd('\')
}
$userHome = [Environment]::GetEnvironmentVariable('CODEX_HOME', 'User')
if ([string]::IsNullOrWhiteSpace($ExpectedHome)) {
if (-not [string]::IsNullOrWhiteSpace($userHome)) {
$ExpectedHome = $userHome
} elseif (-not [string]::IsNullOrWhiteSpace($env:CODEX_HOME)) {
$ExpectedHome = $env:CODEX_HOME
} else {
$failures.Add('未设置 CODEX_HOME,当前仍会使用默认系统盘目录。')
$ExpectedHome = Join-Path $env:USERPROFILE '.codex'
}
}
$expected = Full $ExpectedHome
$processHome = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) { '' } else { Full $env:CODEX_HOME }
$userHomeFull = if ([string]::IsNullOrWhiteSpace($userHome)) { '' } else { Full $userHome }
if ($processHome -ne $expected) { $failures.Add("当前进程 CODEX_HOME 不正确:$processHome") }
if ($userHomeFull -ne $expected) { $failures.Add("用户级 CODEX_HOME 不正确:$userHomeFull") }
if (-not (Test-Path -LiteralPath $expected -PathType Container)) {
$failures.Add("目标目录不存在:$expected")
} else {
$root = [IO.Path]::GetPathRoot($expected)
if ($root.TrimEnd('\').Equals($env:SystemDrive, [StringComparison]::OrdinalIgnoreCase)) {
$failures.Add("CODEX_HOME 仍在系统盘:$expected")
}
try {
$probe = Join-Path $expected ('.write-test-' + [Guid]::NewGuid().ToString('N') + '.tmp')
[IO.File]::WriteAllText($probe, 'Codex migration write test')
Remove-Item -LiteralPath $probe -Force
} catch {
$failures.Add("目标目录不可写:$($_.Exception.Message)")
}
}
$manifestPath = Join-Path $expected '.migration\codex-home-migration.json'
$manifest = $null
if (Test-Path -LiteralPath $manifestPath) {
$manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json
if ((Full $manifest.DestinationHome) -ne $expected) {
$failures.Add('迁移清单中的目标目录与当前 CODEX_HOME 不一致。')
}
} else {
$warnings.Add("未找到迁移清单:$manifestPath")
}
$sqliteUser = [Environment]::GetEnvironmentVariable('CODEX_SQLITE_HOME', 'User')
if (-not [string]::IsNullOrWhiteSpace($sqliteUser) -and $manifest) {
$sqliteFull = Full $sqliteUser
$oldFull = Full $manifest.OriginalHome
if ($sqliteFull.Equals($oldFull, [StringComparison]::OrdinalIgnoreCase) -or
$sqliteFull.StartsWith($oldFull + '\', [StringComparison]::OrdinalIgnoreCase)) {
$failures.Add("CODEX_SQLITE_HOME 仍指向旧目录:$sqliteFull")
}
}
$config = Join-Path $expected 'config.toml'
if (-not (Test-Path -LiteralPath $config)) {
$warnings.Add('未发现 config.toml;如果迁移前从未创建过用户配置,这属于正常情况。')
}
$codexCommand = Get-Command codex -ErrorAction SilentlyContinue
if ($null -eq $codexCommand) {
$warnings.Add('PATH 中未找到 codex 命令;若只使用桌面应用,可忽略。')
} else {
try {
$version = (& codex --version 2>&1 | Out-String).Trim()
} catch {
$warnings.Add("codex --version 执行失败:$($_.Exception.Message)")
}
}
if ($DeepVerifyInitialCopy -and $manifest) {
$old = Full $manifest.OriginalHome
if (-not (Test-Path -LiteralPath $old -PathType Container)) {
$warnings.Add('旧目录已不存在,无法做初始文件的深度复核。')
} else {
# 只核验迁移清单记录时已存在、且两边目前仍同名同大小的文件。
# 使用新 Codex 后目标目录会产生新日志/会话,因此两边出现新增差异是正常的。
$oldFiles = Get-ChildItem -LiteralPath $old -File -Recurse -Force
foreach ($oldFile in $oldFiles) {
$relative = $oldFile.FullName.Substring($old.Length).TrimStart('\')
$newFile = Join-Path $expected $relative
if (-not (Test-Path -LiteralPath $newFile -PathType Leaf)) {
$failures.Add("目标缺少迁移前文件:$relative")
continue
}
if ($oldFile.Length -ne (Get-Item -LiteralPath $newFile).Length) {
$warnings.Add("迁移后已发生变化:$relative")
continue
}
$oldHash = (Get-FileHash -LiteralPath $oldFile.FullName -Algorithm SHA256).Hash
$newHash = (Get-FileHash -LiteralPath $newFile -Algorithm SHA256).Hash
if ($oldHash -ne $newHash) { $failures.Add("同大小但哈希不同:$relative") }
}
}
}
[pscustomobject]@{
Expected_CODEX_HOME = $expected
Process_CODEX_HOME = $processHome
User_CODEX_HOME = $userHomeFull
CODEX_SQLITE_HOME = $sqliteUser
ConfigExists = Test-Path -LiteralPath $config
CodexCommand = if ($codexCommand) { $codexCommand.Source } else { $null }
CodexVersion = if (Get-Variable version -ErrorAction SilentlyContinue) { $version } else { $null }
Failures = $failures.Count
Warnings = $warnings.Count
} | Format-List
foreach ($item in $warnings) { Write-Warning $item }
foreach ($item in $failures) { Write-Error $item -ErrorAction Continue }
if ($failures.Count -gt 0) { exit 1 }
Write-Host '检查通过。请再启动应用,确认历史任务、设置、技能、登录和新建任务均正常。' -ForegroundColor Green
exit 0
运行:
Set-ExecutionPolicy -Scope Process Bypass
.\Test-CodexMigration.ps1 -ExpectedHome 'D:\CodexData\.codex'
如需对旧目录与新目录中仍可比较的初始文件做 SHA-256 复核:
.\Test-CodexMigration.ps1 -ExpectedHome 'D:\CodexData\.codex' -DeepVerifyInitialCopy
☑️ 人工验收清单
- 新 PowerShell 中
$env:CODEX_HOME是新路径; codex --version正常(若安装了 CLI);- 桌面应用能打开,登录状态正常;
- 旧任务/会话、设置、技能和插件可见;
- 新建一个测试任务后,新目录的修改时间发生变化,旧目录不再持续写入;
- Windows 沙箱可正常启动,没有新的权限或错误 1385 提示;
- 日志应位于新
CODEX_HOME下。官方文档也将沙箱日志定位为CODEX_HOME/.sandbox/sandbox.log,并提醒不要对外发送.sandbox-secrets的内容,参见 Windows 沙箱。
📊 检查结果判读
| 输出/现象 | 含义 | 结论 |
|---|---|---|
Failures = 0 且无关键 Warning | 路径、变量、写权限和基础命令均正常 | ✅ 可进入观察期 |
Failures > 0 | 至少一项核心检查失败 | ❌ 不要删除旧目录 |
仅提示没有 config.toml | 迁移前可能从未创建用户配置 | 🟡 通常可忽略 |
仅提示 PATH 中没有 codex | 只使用桌面应用,或 CLI 未安装 | 🟡 按实际需求处理 |
CODEX_SQLITE_HOME 仍指向旧目录 | 数据库仍可能写系统盘 | ❌ 修正变量/配置后重查 |
| 应用能启动但历史内容或登录异常 | 可能是进程继承、权限或凭据存储问题 | ❌ 优先排错,必要时回滚 |
⌨️ 五、可选:把独立版 Codex CLI 命令也安装到非系统盘
这一节只针对官方独立安装脚本安装的 CLI。CODEX_INSTALL_DIR 控制可见 codex 命令的位置;独立包缓存仍在 CODEX_HOME\packages\standalone。不要把它与 Microsoft Store/MSIX 桌面应用的位置混为一谈。
在完成 CODEX_HOME 迁移并检查通过后,新开 PowerShell:
$cliDir = 'D:\Apps\OpenAI\Codex\bin'
New-Item -ItemType Directory -Path $cliDir -Force | Out-Null
[Environment]::SetEnvironmentVariable('CODEX_INSTALL_DIR', $cliDir, 'User')
$env:CODEX_INSTALL_DIR = $cliDir
# 仅在你信任并能访问官方安装地址时执行:
irm https://chatgpt.com/codex/install.ps1 | iex
Get-Command codex | Format-List Source
codex --version
如果安装器提示 PATH 变更,关闭并重新打开 PowerShell。确认新命令工作后,再通过 Windows“已安装的应用”或原安装方式处理旧版本;不要直接猜测并删除目录。
桌面应用本体由 Windows 管理。官方 Windows 安装命令是:
winget install --id 9PLM9XGG6VKS -s msstore
如 Windows 设置的“应用 > 已安装的应用 > ChatGPT > 移动”可用,可由 Windows 执行移动;若按钮不存在或被组织策略禁用,就保留应用本体在系统盘。切勿手工改 C:\Program Files\WindowsApps 的所有权、复制包文件或建立联接,这会影响签名、更新、权限和卸载。
↩️ 六、回滚脚本(先把迁移后的新增数据复制回旧目录,再恢复变量)
回滚前同样要退出 ChatGPT/Codex、IDE 和所有 CLI。此脚本读取迁移时写入的清单,将新目录内容覆盖复制回原目录,核验后恢复迁移前的用户环境变量;它不会删除新目录。
将下面代码另存为 Undo-CodexHomeMigration.ps1:
#requires -Version 5.1
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$DestinationPath,
[switch]$DeepVerify
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Full([string]$Path) {
[IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($Path)).TrimEnd('\')
}
if ([string]::IsNullOrWhiteSpace($DestinationPath)) {
$DestinationPath = [Environment]::GetEnvironmentVariable('CODEX_HOME', 'User')
}
if ([string]::IsNullOrWhiteSpace($DestinationPath)) {
throw '无法确定迁移后的目录。请用 -DestinationPath 明确指定。'
}
$destination = Full $DestinationPath
$manifestPath = Join-Path $destination '.migration\codex-home-migration.json'
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
throw "未找到迁移清单:$manifestPath。为防止覆盖错误目录,脚本停止。"
}
$manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json
if ([int]$manifest.SchemaVersion -ne 1) { throw '不支持的迁移清单版本。' }
if ((Full $manifest.DestinationHome) -ne $destination) {
throw '指定目录与迁移清单中的目标目录不一致。'
}
$original = Full $manifest.OriginalHome
if ($original.Equals($destination, [StringComparison]::OrdinalIgnoreCase)) {
throw '原目录与目标目录相同,拒绝回滚。'
}
$running = @(Get-Process -Name 'codex', 'ChatGPT' -ErrorAction SilentlyContinue)
if ($running.Count -gt 0) {
$names = ($running | Select-Object -ExpandProperty ProcessName -Unique) -join ', '
throw "检测到仍在运行的进程:$names。请彻底退出后重试。"
}
if (-not (Test-Path -LiteralPath $original)) {
if ($PSCmdlet.ShouldProcess($original, '重建原 CODEX_HOME')) {
New-Item -ItemType Directory -Path $original -Force | Out-Null
}
}
Write-Host "迁移后目录:$destination"
Write-Host "回滚到目录:$original"
Write-Host '正在把迁移后的新增/变更数据复制回原目录(不会删除任一目录)……'
if ($PSCmdlet.ShouldProcess($original, '回复制 Codex 状态')) {
& robocopy.exe $destination $original /E /COPY:DAT /DCOPY:DAT /R:2 /W:1 /XJ /NP /XD (Join-Path $destination '.migration')
$code = $LASTEXITCODE
if ($code -ge 8) { throw "Robocopy 回复制失败,退出码:$code。环境变量尚未恢复。" }
}
$destinationFiles = @(Get-ChildItem -LiteralPath $destination -File -Recurse -Force |
Where-Object { $_.FullName -notlike (Join-Path $destination '.migration\*') })
$errors = [Collections.Generic.List[string]]::new()
foreach ($file in $destinationFiles) {
$relative = $file.FullName.Substring($destination.Length).TrimStart('\')
$restored = Join-Path $original $relative
if (-not (Test-Path -LiteralPath $restored -PathType Leaf)) {
$errors.Add("原目录缺少:$relative")
continue
}
if ($file.Length -ne (Get-Item -LiteralPath $restored).Length) {
$errors.Add("文件大小不同:$relative")
continue
}
if ($DeepVerify) {
$a = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash
$b = (Get-FileHash -LiteralPath $restored -Algorithm SHA256).Hash
if ($a -ne $b) { $errors.Add("哈希不同:$relative") }
}
}
if ($errors.Count -gt 0) {
$preview = ($errors | Select-Object -First 20) -join [Environment]::NewLine
throw "回复制核验失败,共 $($errors.Count) 项。前 20 项:`n$preview`n环境变量尚未恢复。"
}
if ($PSCmdlet.ShouldProcess('用户环境变量', '恢复迁移前的 Codex 路径设置')) {
if ([bool]$manifest.OldUserCodexHomeWasSet) {
[Environment]::SetEnvironmentVariable('CODEX_HOME', [string]$manifest.OldUserCodexHome, 'User')
$env:CODEX_HOME = [string]$manifest.OldUserCodexHome
} else {
[Environment]::SetEnvironmentVariable('CODEX_HOME', $null, 'User')
Remove-Item Env:CODEX_HOME -ErrorAction SilentlyContinue
}
if ([bool]$manifest.OldUserSqliteWasSet) {
[Environment]::SetEnvironmentVariable('CODEX_SQLITE_HOME', [string]$manifest.OldUserSqliteHome, 'User')
$env:CODEX_SQLITE_HOME = [string]$manifest.OldUserSqliteHome
} else {
[Environment]::SetEnvironmentVariable('CODEX_SQLITE_HOME', $null, 'User')
Remove-Item Env:CODEX_SQLITE_HOME -ErrorAction SilentlyContinue
}
}
Write-Host '回滚完成。关闭当前 PowerShell,新开 PowerShell,再启动应用并检查。' -ForegroundColor Green
Write-Host "恢复后的目录:$original"
Write-Host "迁移后目录仍保留在:$destination"
运行:
Set-ExecutionPolicy -Scope Process Bypass
.\Undo-CodexHomeMigration.ps1 -DestinationPath 'D:\CodexData\.codex'
深度核验:
.\Undo-CodexHomeMigration.ps1 -DestinationPath 'D:\CodexData\.codex' -DeepVerify
回滚后,新开 PowerShell 执行:
$env:CODEX_HOME
[Environment]::GetEnvironmentVariable('CODEX_HOME', 'User')
codex --version
若迁移前没有显式设置 CODEX_HOME,回滚脚本会删除用户级变量,Codex 将重新使用默认 %USERPROFILE%\.codex。
🔁 回滚数据流
🧹 七、确认稳定后如何释放系统盘空间
至少观察数天,并确认检查脚本、人工验收和实际任务都正常。然后:
- 再次彻底退出应用、IDE 和 CLI;
- 确认新目录最近仍在更新,旧目录不再更新;
- 将旧目录先改名为
.codex.old-日期或移入离线备份,而不是立刻永久删除; - 再使用一段时间;
- 确认无需回滚后,才手工删除旧备份。
本文脚本故意不自动删除旧目录。删除包含认证、会话和技能的目录属于不可逆操作,且实际旧路径因用户配置而异,不应交给通用脚本猜测。
🐧 八、WSL 用户
Windows 原生应用与 WSL CLI 默认不共用 home。完成 Windows 侧迁移后,如果希望 WSL 使用同一份状态,在 WSL 中设置:
export CODEX_HOME=/mnt/d/CodexData/.codex
需要永久生效时写入 ~/.bashrc 或 ~/.zshrc,然后重新打开 WSL shell。注意:
- Windows 路径
D:\CodexData\.codex对应 WSL 路径/mnt/d/CodexData/.codex; - 不要同时从 Windows 和 WSL 并发写同一 SQLite/会话状态;
- 如果 WSL 使用独立状态更符合你的工作流,就迁移 WSL 自己的
~/.codex,不要强行共享; - Windows 原生代理使用 Windows 文件系统中的项目通常更可靠,官方文档也建议原生代理通过
/mnt/<drive>/...与 WSL 共享 Windows 项目。
🩺 九、常见问题
| 症状 | 最可能原因 | 首选动作 |
|---|---|---|
| 🔄 新变量未生效 | 应用/终端继承了旧环境 | 完全退出后新开进程 |
| 🔐 被要求重新登录 | 路径、权限或凭据状态异常 | 保留旧目录,运行检查脚本 |
| 🔒 数据库锁定/复制差异 | 仍有进程在写入 | 退出相关进程或重启 Windows |
| 🧱 沙箱错误 | 迁移后 ACL/沙箱状态需重建 | 重启并重新运行沙箱设置 |
| 🐧 WSL 看不到新状态 | WSL 有独立环境变量和 home | 在 WSL 中单独设置 CODEX_HOME |
🔄 1. 新 PowerShell 中变量正确,但已打开的应用仍用旧目录
环境变量只在进程启动时继承。彻底退出应用(包括托盘/后台进程)、IDE 和终端后重开;必要时注销 Windows 再登录。不要只关闭窗口。
🔐 2. 迁移后要求重新登录
先不要删除旧目录。退出应用,运行检查脚本,确认 CODEX_HOME、文件数量和权限。认证可能依赖 Windows 凭据存储,不能仅凭是否存在某个文件判断。若仍异常,可直接用回滚脚本恢复。
🔒 3. 出现数据库锁定、文件持续变化或复制差异
通常说明仍有 Codex、ChatGPT、IDE 扩展或 CLI 进程在写入。退出所有相关程序,必要时重启 Windows,然后重新选择一个空目标目录执行迁移。不要在差异未解决时强行切变量。
🧱 4. Windows 沙箱迁移后报错
先重启应用并重新运行沙箱设置。目标盘必须保持稳定、权限不能过宽。官方说明移动仓库、改变权限或策略后沙箱可能需要重新设置;持续失败时检查新 CODEX_HOME\.sandbox\sandbox.log。不要发送 .sandbox-secrets。
🔗 5. 能否用 mklink /J 把 %USERPROFILE%\.codex 联接到 D 盘?
技术上可能,但不推荐作为首选:权限、备份工具、沙箱和故障排查都会多一层不透明映射。官方已经提供 CODEX_HOME,直接设置变量更清晰、更容易回滚。
🗃️ 6. 能否把 CODEX_SQLITE_HOME 单独放到另一个盘?
可以,官方变量支持这样做,但普通迁移没有必要。它默认跟随 CODEX_HOME;配置项 sqlite_home 的优先级还高于环境变量。若你曾显式配置 sqlite_home,请同时检查 config.toml,避免数据库仍写到系统盘。
📂 7. 为什么脚本拒绝已有内容的目标目录?
这是为了防止把两套认证、会话、插件或缓存合并。请使用新建的空目录。若此前迁移失败,先人工保存失败目录,再换一个新路径重试。
🧑💻 8. 项目也要迁移吗?
项目不在 CODEX_HOME 中。可单独移动到 D:\Projects,然后在桌面应用/IDE 中重新打开;检查 Git remote、虚拟环境、Node/Python 缓存、构建脚本和任何绝对路径。移动项目后 Windows 沙箱可能需要重新初始化。
📚 十、官方依据与最后提醒
- Codex 环境变量:
CODEX_HOME、CODEX_SQLITE_HOME、CODEX_INSTALL_DIR的公开定义和默认值。 - Codex 配置基础:用户配置默认位于
~/.codex/config.toml,CLI 与 IDE 扩展共享配置层。 - Codex 配置参考:配置、profile 和日志等路径与
$CODEX_HOME的关系。 - Windows 桌面应用:Windows 默认 Codex home、应用安装方式、原生/WSL 行为。
- Windows 沙箱:迁移或权限变化后的沙箱排错及敏感目录提醒。
关键原则只有三条:先复制并核验,再切换;旧目录先保留;任何异常优先回滚,不要边运行边搬动。
葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。
更多推荐



所有评论(0)