feat(codex): opt-in to use default ~/.codex config dir, stopping auth revocation war

Fixes #5370.

Orca injects a private managed CODEX_HOME into Codex processes. For a single
OpenAI/ChatGPT account, OAuth enforces one active session, so two independently
refreshing homes (Orca's managed home and the user's ~/.codex) revoke each
other's single-use refresh tokens — silently breaking non-Orca Codex usage.
The refresh is driven both by interactive Codex and by Orca's BACKGROUND quota
poller, which spawns the real codex binary against the managed home.

Adds an opt-in "Use default Codex config directory" setting (Settings >
Accounts > Codex). When enabled:
- PTY launches inject no CODEX_HOME (all three buildPtyHostEnv sites OR in
  isCodexDefaultHomeEnabled), so terminals resolve ~/.codex.
- The background quota poller and host Codex launches resolve the system
  ~/.codex home instead of spawning codex against the managed home — closing
  the revocation gap that persisted even after the ~/.zshenv workaround.

Supersedes community PR #5594: re-derives the locale changes (drops the
out-of-scope "Create PR" status-message block, keeps only the new Codex keys),
closes the background-revocation gap left unaddressed by the original PR, and
sharpens the setting description to state it also disables the managed-home
quota refresh.

Co-authored-by: Eilon Goldberg <eilon@onyx.security>
This commit is contained in:
Neil
2026-06-27 23:24:53 -07:00
co-authored by Eilon Goldberg
parent b45cb545c5
commit dfab4a89c9
14 changed files with 207 additions and 19 deletions
@@ -77,6 +77,17 @@ export function isAgentStatusHooksEnabled(
return settings?.agentStatusHooksEnabled !== false
}
/**
* Why: opt-in setting that makes Codex use its default config home (~/.codex)
* by skipping the managed CODEX_HOME injection. Default off (undefined/false)
* keeps Orca's managed runtime home and its account hot-swap behavior.
*/
export function isCodexDefaultHomeEnabled(
settings: Pick<GlobalSettings, 'codexUseDefaultConfigDir'> | null | undefined
): boolean {
return settings?.codexUseDefaultConfigDir === true
}
export function installManagedAgentHooks(): void {
for (const [agent, install] of MANAGED_AGENT_HOOK_INSTALLERS) {
try {
@@ -736,6 +736,47 @@ describe('CodexRuntimeHomeService', () => {
expect(existsSync(getRuntimeCodexHomePath())).toBe(true)
})
it('targets the default ~/.codex home for launch and quota fetch when the opt-in is on', async () => {
// Why: closes the background revocation gap — when codexUseDefaultConfigDir
// is on, the quota poller and any host launch must resolve null (the system
// ~/.codex home) instead of spawning Codex against the managed runtime home,
// which would refresh and revoke the user's single-OAuth-session token.
const managedHomePath = createManagedAuth(
testState.userDataDir,
'account-1',
createCodexAuthJson('one@example.com', 'acct-1', 'one')
)
const store = createStore(
createSettings({
codexUseDefaultConfigDir: true,
codexManagedAccounts: [
{
id: 'account-1',
email: 'one@example.com',
managedHomePath,
providerAccountId: 'acct-1',
workspaceLabel: null,
workspaceAccountId: 'acct-1',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: 'account-1',
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
})
)
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
// Why: null is how callers (pty env build, rate-limit fetcher, commit-msg
// agent) resolve the system ~/.codex home instead of an Orca-managed
// CODEX_HOME, so the background poller never spawns Codex against the
// managed home and never refreshes/revokes the ~/.codex OAuth session.
expect(service.prepareForRateLimitFetch()).toBeNull()
expect(service.prepareForCodexLaunch()).toBeNull()
})
it('uses the same host CODEX_HOME after switching managed Codex accounts', async () => {
const runtimeAuthPath = getRuntimeCodexAuthPath()
const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one')
@@ -136,6 +136,14 @@ export class CodexRuntimeHomeService {
this.getWslSystemCodexHomePath(wslTarget)
)
}
// Why: with the default-home opt-in, every host Codex launch (terminals,
// commit-message agent) must resolve ~/.codex. Returning null tells callers
// to inject no CODEX_HOME, and skipping the managed-home sync means Orca
// never spawns Codex against the managed home to drive a token refresh that
// would revoke the user's single-OAuth-session ~/.codex login.
if (this.isDefaultConfigDirEnabled()) {
return null
}
this.syncForCurrentSelection()
syncSystemCodexResourcesIntoManagedHome()
syncSystemConfigIntoManagedCodexHome()
@@ -189,12 +197,24 @@ export class CodexRuntimeHomeService {
this.getWslSystemCodexHomePath(wslTarget)
)
}
// Why: when the user opts into the default ~/.codex home, the background
// quota poller must NOT spawn Codex against the managed home — doing so
// refreshes the managed home's single-use OAuth token and revokes the
// ~/.codex session. Return null so the fetcher targets the system home and
// skip the managed-home sync entirely (no auth.json read-back to mutate).
if (this.isDefaultConfigDirEnabled()) {
return null
}
this.syncForCurrentSelection()
syncSystemCodexResourcesIntoManagedHome()
syncSystemConfigIntoManagedCodexHome()
return this.getRuntimeHomePath()
}
private isDefaultConfigDirEnabled(): boolean {
return this.store.getSettings().codexUseDefaultConfigDir === true
}
syncForCurrentSelection(target?: CodexAccountSelectionTarget): void {
if (target?.runtime === 'wsl') {
this.syncWslRuntimeForCurrentSelection(target)
+29
View File
@@ -547,6 +547,7 @@ describe('registerPtyHandlers', () => {
getSettings?: () => {
enableGitHubAttribution?: boolean
agentStatusHooksEnabled?: boolean
codexUseDefaultConfigDir?: boolean
httpProxyUrl?: string
httpProxyBypassRules?: string
},
@@ -697,6 +698,34 @@ describe('registerPtyHandlers', () => {
expect(env.ORCA_CODEX_HOME).toBe(TEST_CODEX_HOME)
})
it('skips the managed Codex home when codexUseDefaultConfigDir is enabled', async () => {
// Why: the opt-in setting must force Codex back to its default ~/.codex
// home by injecting NO CODEX_HOME, even though a managed home is selected
// (getSelectedCodexHomePath returns TEST_CODEX_HOME here). Any ambient
// CODEX_HOME/ORCA_CODEX_HOME must also be stripped, not left to leak in.
const env = await spawnAndGetEnv(
{ CODEX_HOME: '/tmp/ambient-codex-home', ORCA_CODEX_HOME: '/tmp/ambient-orca-codex-home' },
undefined,
() => TEST_CODEX_HOME,
() => ({ codexUseDefaultConfigDir: true })
)
expect(env.CODEX_HOME).toBeUndefined()
expect(env.ORCA_CODEX_HOME).toBeUndefined()
})
it('still injects the managed Codex home when codexUseDefaultConfigDir is off', async () => {
// Why: guards the OR condition wired into skipCodexHomeEnv — a false (or
// absent) setting must preserve the existing managed-home injection.
const env = await spawnAndGetEnv(
undefined,
undefined,
() => TEST_CODEX_HOME,
() => ({ codexUseDefaultConfigDir: false })
)
expect(env.CODEX_HOME).toBe(TEST_CODEX_HOME)
expect(env.ORCA_CODEX_HOME).toBe(TEST_CODEX_HOME)
})
it('injects the OpenCode hook env into Orca terminal PTYs', async () => {
// Why: clear any ambient OPENCODE_CONFIG_DIR so the mock's value is used
const env = await spawnAndGetEnv(undefined, { OPENCODE_CONFIG_DIR: undefined })
+15 -8
View File
@@ -30,7 +30,10 @@ import {
getFirstCommandToken
} from '../../shared/command-token-scanner'
import { agentHookServer } from '../agent-hooks/server'
import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls'
import {
isAgentStatusHooksEnabled,
isCodexDefaultHomeEnabled
} from '../agent-hooks/managed-agent-hook-controls'
import { piTitlebarExtensionService } from '../pi/titlebar-extension-service'
import { detectPiAgentKindFromCommand, type PiAgentKind } from '../../shared/pi-agent-kind'
import { isPwshAvailable } from '../pwsh'
@@ -1222,7 +1225,9 @@ export function registerPtyHandlers(
isPackaged: app.isPackaged,
userDataPath: app.getPath('userData'),
selectedCodexHomePath,
skipCodexHomeEnv: ctx?.isWsl === true && !selectedCodexHomePath,
skipCodexHomeEnv:
isCodexDefaultHomeEnabled(getSettings?.()) ||
(ctx?.isWsl === true && !selectedCodexHomePath),
githubAttributionEnabled: getSettings?.()?.enableGitHubAttribution ?? false,
launchCommand: ctx?.command,
shellPath: ctx?.shellPath,
@@ -1833,9 +1838,10 @@ export function registerPtyHandlers(
)
: null
const skipCodexHomeEnv =
isDaemonHostSpawn &&
shouldSkipCodexHomeEnvForWindowsShell(daemonShellOverride, args.cwd) &&
!selectedCodexHomePath
isCodexDefaultHomeEnabled(getSettings?.()) ||
(isDaemonHostSpawn &&
shouldSkipCodexHomeEnvForWindowsShell(daemonShellOverride, args.cwd) &&
!selectedCodexHomePath)
if (isDaemonHostSpawn && sessionId) {
if (!isSafePtySessionId(sessionId, app.getPath('userData'))) {
throw new Error('Invalid PTY session id')
@@ -2475,9 +2481,10 @@ export function registerPtyHandlers(
)
: null
const skipCodexHomeEnv =
isDaemonHostSpawn &&
shouldSkipCodexHomeEnvForWindowsShell(effectiveShellOverride, args.cwd) &&
!selectedCodexHomePath
isCodexDefaultHomeEnabled(getSettings?.()) ||
(isDaemonHostSpawn &&
shouldSkipCodexHomeEnvForWindowsShell(effectiveShellOverride, args.cwd) &&
!selectedCodexHomePath)
if (isDaemonHostSpawn) {
if (effectiveSessionId === undefined) {
// Should be unreachable: the expression above returns a string when
@@ -28,7 +28,7 @@ import {
getAccountsPaneSearchEntries
} from './accounts-search'
import { SearchableSetting } from './SearchableSetting'
import { SettingsRow, SettingsSegmentedControl } from './SettingsFormControls'
import { SettingsRow, SettingsSegmentedControl, SettingsSwitchRow } from './SettingsFormControls'
import { matchesSettingsSearch } from './settings-search'
import { markLiveCodexSessionsForRestart } from '@/lib/codex-session-restart'
import {
@@ -1109,6 +1109,40 @@ export function AccountsPane({
)}
</div>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.2eb471df34',
'Use default Codex config directory'
)}
description={translate(
'auto.components.settings.AccountsPane.e321640e44',
"Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync."
)}
keywords={['codex', 'config', 'directory', 'home', 'managed', 'default', 'account']}
className="space-y-3 py-2"
>
<SettingsSwitchRow
label={translate(
'auto.components.settings.AccountsPane.2eb471df34',
'Use default Codex config directory'
)}
description={translate(
'auto.components.settings.AccountsPane.e321640e44',
"Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync."
)}
checked={settings.codexUseDefaultConfigDir === true}
onChange={() =>
updateSettings({
codexUseDefaultConfigDir: settings.codexUseDefaultConfigDir !== true
})
}
ariaLabel={translate(
'auto.components.settings.AccountsPane.2eb471df34',
'Use default Codex config directory'
)}
/>
</SearchableSetting>
</section>
) : null,
matchesSettingsSearch(searchQuery, getAccountsGeminiSearchEntries()) ? (
@@ -93,6 +93,24 @@ export const getAccountsCodexSearchEntries = createLocalizedCatalog(() => [
...translateSearchKeyword('auto.components.settings.accounts.search.f2d666a886', 'optional'),
...translateSearchKeyword('auto.components.settings.accounts.search.35b461d817', 'sign in')
]
},
{
title: translate(
'auto.components.settings.accounts.search.d6347a2658',
'Default Codex config directory'
),
description: translate(
'auto.components.settings.accounts.search.edd9a67f8d',
"Run Codex with your system ~/.codex config instead of Orca's managed home."
),
keywords: [
...translateSearchKeyword('auto.components.settings.accounts.search.70d1b8def5', 'codex'),
...translateSearchKeyword('auto.components.settings.accounts.search.e85a7f3385', 'default'),
...translateSearchKeyword('auto.components.settings.accounts.search.397ba215c5', 'config'),
...translateSearchKeyword('auto.components.settings.accounts.search.67a07ebd35', 'directory'),
...translateSearchKeyword('auto.components.settings.accounts.search.5d23eca2a7', 'home'),
...translateSearchKeyword('auto.components.settings.accounts.search.c919fa7390', 'managed')
]
}
])
+6 -2
View File
@@ -4459,7 +4459,9 @@
"75ca9b718e": "Codex reported that the active account needs a fresh sign-in. Re-authenticate it before starting new Codex sessions.",
"b11078a9c2": "wsl",
"350b2a1aa7": "Use your current",
"e05d0ff737": "Use your current {{value0}} Claude login."
"e05d0ff737": "Use your current {{value0}} Claude login.",
"2eb471df34": "Use default Codex config directory",
"e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync."
},
"AdvancedPane": {
"40b29e0bf3": "Restart",
@@ -6573,7 +6575,9 @@
"bdbd1e668e": "windows",
"593720c17f": "location",
"b84a5b0c8a": "Choose whether provider accounts are inspected and added on this device or in WSL.",
"d09fb5ca92": "Account Location"
"d09fb5ca92": "Account Location",
"d6347a2658": "Default Codex config directory",
"edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home."
}
},
"advanced": {
+6 -2
View File
@@ -4459,7 +4459,9 @@
"75ca9b718e": "Codex informó que la cuenta activa necesita un nuevo inicio de sesión. Vuelva a autenticarlo antes de iniciar nuevas sesiones del Codex.",
"b11078a9c2": "wsl",
"350b2a1aa7": "Usa tu actual",
"e05d0ff737": "Utilice su inicio de sesión actual de {{value0}} Claude."
"e05d0ff737": "Utilice su inicio de sesión actual de {{value0}} Claude.",
"2eb471df34": "Use default Codex config directory",
"e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync."
},
"AdvancedPane": {
"40b29e0bf3": "Reanudar",
@@ -6536,7 +6538,9 @@
"bdbd1e668e": "ventanas",
"593720c17f": "ubicación",
"b84a5b0c8a": "Elija si las cuentas de proveedor se inspeccionan y agregan en este dispositivo o en WSL.",
"d09fb5ca92": "Ubicación de la cuenta"
"d09fb5ca92": "Ubicación de la cuenta",
"d6347a2658": "Default Codex config directory",
"edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home."
}
},
"advanced": {
+6 -2
View File
@@ -4444,7 +4444,9 @@
"75ca9b718e": "Codex は、アクティブなアカウントには新たにサインインする必要があると報告しました。新規 Codex セッションを開始する前に、再認証してください。",
"b11078a9c2": "wsl",
"350b2a1aa7": "Use your current",
"e05d0ff737": "現在の {{value0}} Claude ログインを使用します。"
"e05d0ff737": "現在の {{value0}} Claude ログインを使用します。",
"2eb471df34": "Use default Codex config directory",
"e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync."
},
"AdvancedPane": {
"40b29e0bf3": "再起動",
@@ -6558,7 +6560,9 @@
"bdbd1e668e": "窓",
"593720c17f": "場所",
"b84a5b0c8a": "プロバイダー アカウントをこのデバイスまたは WSL で検査および追加するかどうかを選択します。",
"d09fb5ca92": "アカウントの場所"
"d09fb5ca92": "アカウントの場所",
"d6347a2658": "Default Codex config directory",
"edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home."
}
},
"advanced": {
+6 -2
View File
@@ -4444,7 +4444,9 @@
"75ca9b718e": "Codex는 활성 계정에 새로 로그인해야 한다고 보고했습니다. 새 Codex 세션을 시작하기 전에 다시 인증하세요.",
"b11078a9c2": "wsl",
"350b2a1aa7": "현재 사용 중인",
"e05d0ff737": "현재 {{value0}} Claude 로그인을 사용하세요."
"e05d0ff737": "현재 {{value0}} Claude 로그인을 사용하세요.",
"2eb471df34": "Use default Codex config directory",
"e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync."
},
"AdvancedPane": {
"40b29e0bf3": "다시 시작",
@@ -6521,7 +6523,9 @@
"bdbd1e668e": "Windows",
"593720c17f": "위치",
"b84a5b0c8a": "이 장치 또는 WSL에서 공급자 계정을 검사하고 추가할지 선택합니다.",
"d09fb5ca92": "계정 위치"
"d09fb5ca92": "계정 위치",
"d6347a2658": "Default Codex config directory",
"edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home."
}
},
"advanced": {
+6 -2
View File
@@ -4444,7 +4444,9 @@
"75ca9b718e": "Codex 报告活动账户需要重新登录。在开始新的 Codex 会话之前重新对其进行身份验证。",
"b11078a9c2": "wsl",
"350b2a1aa7": "用你已有的",
"e05d0ff737": "使用您当前 {{value0}} Claude 登录名。"
"e05d0ff737": "使用您当前 {{value0}} Claude 登录名。",
"2eb471df34": "Use default Codex config directory",
"e321640e44": "Run Codex with your system ~/.codex config instead of Orca's managed home. Also stops Orca's background quota poller from refreshing the managed home, so it no longer revokes your ~/.codex session. Disables Orca's Codex account hot-swap and auth sync."
},
"AdvancedPane": {
"40b29e0bf3": "重新启动",
@@ -6521,7 +6523,9 @@
"bdbd1e668e": "视窗",
"593720c17f": "位置",
"b84a5b0c8a": "选择是否在此设备上或 WSL 中检查和添加提供商账户。",
"d09fb5ca92": "账户位置"
"d09fb5ca92": "账户位置",
"d6347a2658": "Default Codex config directory",
"edd9a67f8d": "Run Codex with your system ~/.codex config instead of Orca's managed home."
}
},
"advanced": {
+1
View File
@@ -317,6 +317,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
agentDefaultEnv: { ...DEFAULT_TUI_AGENT_ENV },
agentYoloDefaultsMigrated: true,
agentStatusHooksEnabled: true,
codexUseDefaultConfigDir: false,
tabAutoGenerateTitle: false,
confirmClosePinnedTab: true,
keepComputerAwakeWhileAgentsRun: false,
+7
View File
@@ -2692,6 +2692,13 @@ export type GlobalSettings = {
/** Why: disabling must persist so startup does not reinstall global agent
* hook entries right after the user removes them from Settings or CLI. */
agentStatusHooksEnabled: boolean
/** Why: opt-in escape hatch so Codex launched inside Orca terminals reads the
* user's real ~/.codex config instead of Orca's managed runtime home. When
* true, Orca injects no CODEX_HOME and the background quota poller refreshes
* ~/.codex instead of the managed home — stopping the single-OAuth-session
* refresh-token revocation war with non-Orca Codex. Disables Codex account
* hot-swap and auth sync. Default false preserves the managed-home behavior. */
codexUseDefaultConfigDir?: boolean
/** Why: generated tab titles are semantic but subjective, so they stay opt-in
* and manual renames remain the stronger user intent. */
tabAutoGenerateTitle: boolean