This ensures that chatgpt.com, chat.openai.com, ab.chatgpt.com, oaistatic.com, and oaiusercontent.com all go through the proxy first instead of falling through to GEOIP,CN,DIRECT.
However, if we manually update the subscription, those manual rules will be overwritten. So we added an extra manual auto-repair solution.
We wrote a PowerShell script and placed it at: C:\Users\Windows 10\.config\clash\codex-maintain-openai-rules.ps1
Its purpose is to:
Find the currently active Clash profile
Automatically check whether the OpenAI rules exist
If they are missing, reinsert them before GEOIP,CN,DIRECT
Generate the runtime configuration currently used for execution
Hot-reload Clash
Then we added the following PowerShell command to the PowerShell profile:
123456
# manually repair Clash OpenAI rules after updating subscriptionsfunctionFix-ClashOpenAIRules{powershell.exe-NoProfile-ExecutionPolicyBypass-File"$env:USERPROFILE\.config\clash\codex-maintain-openai-rules.ps1"}Set-Alias-Nameclash-fix-openai-ValueFix-ClashOpenAIRules
This way, after manually updating the Clash subscription, we can run the clash-fix-openai command to write those rules back in.
Troubleshooting
If reconnect shows up again later, how do you check whether the OpenAI rules are still there and whether the script is working?
Check in the following order:
Whether Clash is running and the local port 127.0.0.1:7890 is still listening
Whether the OpenAI rules are still present in the current profile
Whether the Clash logs show that the DomainSuffix rules were matched
$ErrorActionPreference="Stop"$clashRoot=Join-Path$env:USERPROFILE".config\clash"$profilesDir=Join-Path$clashRoot"profiles"$generalConfig=Join-Path$clashRoot"config.yaml"$profilesList=Join-Path$profilesDir"list.yml"$runtimeConfig=Join-Path$profilesDir"codex-active-runtime.yml"$utf8=[System.Text.UTF8Encoding]::new($false)$startMarker=" # codex-openai-rules-start"$endMarker=" # codex-openai-rules-end"functionGet-GeneralValue{param([string[]]$Lines,[string]$Key)foreach($linein$Lines){if($line-match"^\s*$([regex]::Escape($Key)):\s*(.+?)\s*$"){return$Matches[1]}}return$null}functionGet-MatchProxyName{param([System.Collections.Generic.List[string]]$Lines)foreach($linein$Lines){if($line-match'^\s*-\s*MATCH,(.+?)\s*$'){return$Matches[1]}}throw"Could not determine fallback proxy name from MATCH rule"}functionRead-Utf8Text{param([string]$Path)return[System.IO.File]::ReadAllText($Path,$utf8)}functionRead-Utf8Lines{param([string]$Path)return[System.IO.File]::ReadAllLines($Path,$utf8)}functionWrite-Utf8Lines{param([string]$Path,[string[]]$Lines)[System.IO.File]::WriteAllLines($Path,$Lines,$utf8)}functionUpdate-ProfileRules{param([string]$Path)$lines=[System.Collections.Generic.List[string]]::new()foreach($lineinRead-Utf8Lines-Path$Path){[void]$lines.Add($line)}$geoIndex=-1for($i=0;$i-lt$lines.Count;$i++){if($lines[$i]-match'^\s*-\s*GEOIP,CN,DIRECT\s*$'){$geoIndex=$ibreak}}if($geoIndex-lt0){return$false}$startIndex=$lines.IndexOf($startMarker)if($startIndex-ge0){$endIndex=$lines.IndexOf($endMarker)if($endIndex-ge$startIndex){$lines.RemoveRange($startIndex,$endIndex-$startIndex+1)if($startIndex-lt$geoIndex){$geoIndex-=($endIndex-$startIndex+1)}}}$proxyName=Get-MatchProxyName-Lines$lines$managedBlock=@($startMarker," - DOMAIN-SUFFIX,chatgpt.com,$proxyName"," - DOMAIN-SUFFIX,openai.com,$proxyName"," - DOMAIN-SUFFIX,oaistatic.com,$proxyName"," - DOMAIN-SUFFIX,oaiusercontent.com,$proxyName",$endMarker)for($i=0;$i-lt$managedBlock.Count;$i++){$lines.Insert($geoIndex+$i,$managedBlock[$i])}Write-Utf8Lines-Path$Path-Lines$linesreturn$true}functionGet-ActiveProfilePath{param([string]$ListPath,[string]$ProfilesRoot)$raw=Read-Utf8Text-Path$ListPath$indexMatch=[regex]::Match($raw,'(?m)^index:\s*(\d+)\s*$')if(-not$indexMatch.Success){throw"Could not find active profile index in $ListPath"}$selectedIndex=[int]$indexMatch.Groups[1].Value$entries=[regex]::Matches($raw,'(?m)^\s*(?:-\s*)?time:\s*(.+?)\s*$')|ForEach-Object{$_.Groups[1].Value}if($selectedIndex-ge$entries.Count){throw"Active profile index $selectedIndex is out of range"}returnJoin-Path$ProfilesRoot$entries[$selectedIndex]}functionBuild-RuntimeConfig{param([string]$SourcePath,[string]$DestinationPath,[string]$Controller,[string]$Secret)$lines=Read-Utf8Lines-Path$SourcePath$updated=foreach($linein$lines){if($line-match'^log-level:'){"log-level: info"}elseif($line-match'^external-controller:'){"external-controller: $Controller"}elseif($line-match'^secret:'){"secret: $Secret"}else{$line}}Write-Utf8Lines-Path$DestinationPath-Lines$updated}functionReload-ActiveConfig{param([string]$Controller,[string]$Secret,[string]$ConfigPath)$body=@{path=$ConfigPath}|ConvertTo-JsonInvoke-RestMethod`-Headers@{Authorization="Bearer $Secret"}`-Uri"http://$Controller/configs?force=true"`-MethodPut`-ContentType"application/json"`-Body$body|Out-Null}if(-not(Test-Path$generalConfig)){throw"Missing Clash general config: $generalConfig"}if(-not(Test-Path$profilesList)){throw"Missing Clash profile list: $profilesList"}$generalLines=Read-Utf8Lines-Path$generalConfig$controller=Get-GeneralValue-Lines$generalLines-Key"external-controller"$secret=Get-GeneralValue-Lines$generalLines-Key"secret"if(-not$controller){throw"Missing external-controller in $generalConfig"}if($null-eq$secret){$secret=""}$profileFiles=Get-ChildItem$profilesDir-Filter*.yml|Where-Object{$_.Name-notin@("list.yml","codex-active-runtime.yml")-and$_.Name-notlike"*.codex-merged.yml"}foreach($profilein$profileFiles){$raw=Read-Utf8Text-Path$profile.FullNameif($raw-match'(?m)^rules:\s*$'-and$raw-match'(?m)^\s*-\s*GEOIP,CN,DIRECT\s*$'){Update-ProfileRules-Path$profile.FullName|Out-Null}}$activeProfile=Get-ActiveProfilePath-ListPath$profilesList-ProfilesRoot$profilesDirBuild-RuntimeConfig-SourcePath$activeProfile-DestinationPath$runtimeConfig-Controller$controller-Secret$secretReload-ActiveConfig-Controller$controller-Secret$secret-ConfigPath$runtimeConfig