feat(codex): real-home hook installer trusted via the codex app-server grant client

With the real-home flag ON and the system-default selection, install Orca's
status hook into the user's real ~/.codex before any pane spawns:

- entry APPENDED LAST per managed event: codex hook trust keys are positional
  (source:event:group:handler), so appending keeps every user entry's position
  and trust record intact; user entries and unknown top-level hooks.json fields
  are preserved verbatim
- trust is granted exclusively through the codex app-server client
  (hooks/list + config/batchWrite, verified by re-list); Orca never writes
  [hooks.state] into the user's real config.toml itself
- if the grant lane is unavailable (old binary, unsupported RPC, verify
  failure), the appended entry is rolled back byte-exactly and the host keeps
  the managed-home lane end to end (PTY env, rate limits, commit messages)
  via a lane gate on the runtime-home service
- one-time pristine backup of the user's hooks.json under Orca's userData;
  a rolling .bak sits next to the file (existing atomic writer)
- hook opt-out sweeps Orca entries from the real home and drops Orca-owned
  trust records; flag-off downgrade re-arms the existing legacy system-home
  sweep, which removes the entry and its trust keys cleanly
- the legacy system-home sweep is suppressed only while the real-home lane
  owns ~/.codex/hooks.json, so managed installs cannot delete the entry
This commit is contained in:
Brennan Benson
2026-07-14 22:54:37 -07:00
parent f52dd7952f
commit c3f37e5500
5 changed files with 573 additions and 6 deletions
@@ -200,10 +200,19 @@ export class CodexRuntimeHomeService {
return this.getRuntimeHomePath()
}
// Why: the real-home hook installer flips this gate off when the trust-grant
// client reports the host incapable, keeping that host byte-identical to the
// managed lane instead of shipping status-blind panes.
private realHomeLaneGate: () => boolean = () => true
setRealHomeLaneGate(gate: () => boolean): void {
this.realHomeLaneGate = gate
}
// Why: real-home routing applies only to the host system-default selection
// (no managed account chosen for host) with the staged flag ON. Managed host
// accounts keep the isolated runtime home for hot-swap and token persistence.
isHostSystemDefaultRealHome(): boolean {
isHostSystemDefaultRealHomeSelected(): boolean {
const settings = this.store.getSettings()
return (
isCodexSystemDefaultRealHomeEnabled(settings) &&
@@ -211,6 +220,10 @@ export class CodexRuntimeHomeService {
)
}
isHostSystemDefaultRealHome(): boolean {
return this.isHostSystemDefaultRealHomeSelected() && this.realHomeLaneGate()
}
syncActiveWslSelectionsBeforeRestart(): void {
if (process.platform !== 'win32') {
return
@@ -0,0 +1,208 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import type * as NodeOs from 'node:os'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { CodexManagedTrustGrantPlan } from './codex-hook-trust-grant'
const { homedirMock, grantMock } = vi.hoisted(() => ({
homedirMock: vi.fn<() => string>(),
grantMock: vi.fn()
}))
vi.mock('node:os', async () => {
const actual = await vi.importActual<typeof NodeOs>('node:os')
return { ...actual, homedir: homedirMock }
})
vi.mock('./codex-hook-trust-grant', () => ({
grantManagedCodexHookTrust: grantMock
}))
import {
ensureRealHomeCodexHookState,
getRealHomeCodexHookLane,
_internals
} from './codex-real-home-hook-install'
import { getCodexManagedHookInstallMaterial } from './hook-service'
let fakeHomeDir: string
let userDataDir: string
let previousUserDataPath: string | undefined
function getRealHooksJsonPath(): string {
return join(fakeHomeDir, '.codex', 'hooks.json')
}
function readRealHooksJson(): {
hooks?: Record<string, { hooks?: { command?: string }[] }[]>
[key: string]: unknown
} {
return JSON.parse(readFileSync(getRealHooksJsonPath(), 'utf-8'))
}
function grantSucceeds(): void {
grantMock.mockImplementation((plan: CodexManagedTrustGrantPlan) => ({
lane: 'rpc',
entries: plan.managedEntries.map((entry) => ({ ...entry, trustedHash: 'codex-hash' }))
}))
}
function grantUnavailable(): void {
grantMock.mockReturnValue({ lane: 'fallback', reason: 'unsupported' })
}
beforeEach(() => {
grantMock.mockReset()
fakeHomeDir = mkdtempSync(join(tmpdir(), 'orca-real-home-hooks-home-'))
userDataDir = mkdtempSync(join(tmpdir(), 'orca-real-home-hooks-user-data-'))
previousUserDataPath = process.env.ORCA_USER_DATA_PATH
process.env.ORCA_USER_DATA_PATH = userDataDir
homedirMock.mockReturnValue(fakeHomeDir)
mkdirSync(join(fakeHomeDir, '.codex'), { recursive: true })
_internals.setLaneForTesting('pending')
})
afterEach(() => {
rmSync(fakeHomeDir, { recursive: true, force: true })
rmSync(userDataDir, { recursive: true, force: true })
if (previousUserDataPath === undefined) {
delete process.env.ORCA_USER_DATA_PATH
} else {
process.env.ORCA_USER_DATA_PATH = previousUserDataPath
}
vi.clearAllMocks()
})
describe('ensureRealHomeCodexHookState (install)', () => {
it('creates hooks.json with the Orca entry in every managed event for a fresh home', () => {
grantSucceeds()
const lane = ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(lane).toBe('installed')
const material = getCodexManagedHookInstallMaterial()
const config = readRealHooksJson()
for (const eventName of material.events) {
const definitions = config.hooks?.[eventName]
expect(definitions).toHaveLength(1)
expect(definitions?.[0]?.hooks?.[0]?.command).toBe(material.command)
}
// The grant plan targeted the real home with append-position trust keys.
const plan = grantMock.mock.calls[0]![0] as CodexManagedTrustGrantPlan
expect(plan.runtimeHomePath).toBe(join(fakeHomeDir, '.codex'))
expect(plan.host).toEqual({ kind: 'native' })
expect(plan.managedEntries.every((entry) => entry.groupIndex === 0)).toBe(true)
})
it('appends LAST and preserves user entries, unknown fields, and trust positions', () => {
grantSucceeds()
const userConfig = {
hooks: {
Stop: [{ matcher: 'deploy-*', hooks: [{ type: 'command', command: 'my-stop-hook.sh' }] }],
PreCompact: [{ hooks: [{ type: 'command', command: 'my-compact-hook.sh' }] }]
},
_pluginManagerMetadata: { owner: 'someone-else' }
}
writeFileSync(getRealHooksJsonPath(), `${JSON.stringify(userConfig, null, 2)}\n`, 'utf-8')
const lane = ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(lane).toBe('installed')
const config = readRealHooksJson()
// User's Stop entry keeps position 0; Orca's entry is appended after it.
expect(config.hooks?.Stop).toHaveLength(2)
expect(config.hooks?.Stop?.[0]).toEqual(userConfig.hooks.Stop[0])
// Non-managed events Orca does not subscribe to stay untouched.
expect(config.hooks?.PreCompact).toEqual(userConfig.hooks.PreCompact)
// Unknown top-level metadata survives, unlike the managed-home writer.
expect(config._pluginManagerMetadata).toEqual(userConfig._pluginManagerMetadata)
// The appended entry's trust key uses its appended position.
const plan = grantMock.mock.calls[0]![0] as CodexManagedTrustGrantPlan
const stopEntry = plan.managedEntries.find((entry) => entry.eventLabel === 'stop')
expect(stopEntry?.groupIndex).toBe(1)
// Pristine pre-Orca backup lands under userData, not in ~/.codex.
expect(
readFileSync(join(userDataDir, 'codex-real-home-hooks', 'hooks.json.pre-orca'), 'utf-8')
).toBe(`${JSON.stringify(userConfig, null, 2)}\n`)
})
it('rolls the file back byte-exactly when the grant lane is unavailable', () => {
grantUnavailable()
const userRaw = `${JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'mine.sh' }] }] } }, null, 2)}\n`
writeFileSync(getRealHooksJsonPath(), userRaw, 'utf-8')
const lane = ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(lane).toBe('unavailable')
expect(getRealHomeCodexHookLane()).toBe('unavailable')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe(userRaw)
})
it('removes a freshly created hooks.json when the grant lane is unavailable', () => {
grantUnavailable()
const lane = ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(lane).toBe('unavailable')
expect(existsSync(getRealHooksJsonPath())).toBe(false)
})
it('leaves an unparseable hooks.json untouched and keeps the managed lane', () => {
writeFileSync(getRealHooksJsonPath(), '{not json', 'utf-8')
const lane = ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(lane).toBe('unavailable')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe('{not json')
expect(grantMock).not.toHaveBeenCalled()
})
it('is idempotent: a second ensure keeps a single appended entry per event', () => {
grantSucceeds()
ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
const firstRaw = readFileSync(getRealHooksJsonPath(), 'utf-8')
const lane = ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(lane).toBe('installed')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe(firstRaw)
})
})
describe('ensureRealHomeCodexHookState (opt-out sweep)', () => {
it('removes only Orca entries and reports the removed lane', () => {
grantSucceeds()
const userStop = {
matcher: 'deploy-*',
hooks: [{ type: 'command', command: 'my-stop-hook.sh' }]
}
writeFileSync(
getRealHooksJsonPath(),
`${JSON.stringify({ hooks: { Stop: [userStop] } }, null, 2)}\n`,
'utf-8'
)
ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(readRealHooksJson().hooks?.Stop).toHaveLength(2)
const lane = ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
expect(lane).toBe('removed')
const config = readRealHooksJson()
expect(config.hooks?.Stop).toEqual([userStop])
const material = getCodexManagedHookInstallMaterial()
for (const eventName of material.events) {
if (eventName === 'Stop') {
continue
}
expect(config.hooks?.[eventName]).toBeUndefined()
}
})
it('no-ops the sweep when the real home has no hooks.json', () => {
const lane = ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
expect(lane).toBe('removed')
expect(existsSync(getRealHooksJsonPath())).toBe(false)
})
})
@@ -0,0 +1,275 @@
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
renameSync,
unlinkSync,
writeFileSync
} from 'node:fs'
import { join } from 'node:path'
import {
buildManagedCommandHook,
createManagedCommandMatcher,
hookDefinitionHasManagedCommand,
MANAGED_HOOK_TIMEOUT_SECONDS,
readHooksJson,
removeManagedCommands,
writeHooksJson,
writeManagedScript,
type HookDefinition,
type HooksConfig
} from '../agent-hooks/installer-utils'
import { getSystemCodexHomePath } from './codex-home-paths'
import { getCodexManagedScriptFileName } from './codex-hook-identity'
import { grantManagedCodexHookTrust } from './codex-hook-trust-grant'
import { removeCodexTrustGrantLedgerHome } from './codex-trust-grant-ledger'
import { getCodexManagedHookInstallMaterial } from './hook-service'
import { computeTrustKey, removeHookTrustEntries, type CodexTrustEntry } from './config-toml-trust'
/**
* Real-home Codex hook lane for the system-default selection (flag ON).
*
* - 'pending': no attempt yet this process; routing may optimistically use the
* real home (reads are hook-free and the install runs before pane spawns).
* - 'installed': entry appended LAST in ~/.codex/hooks.json and trusted by
* codex itself through the app-server grant client.
* - 'unavailable': the grant lane could not trust the entry (old binary,
* unsupported RPC, verify failure). The entry is rolled back and the host
* stays on the managed-home lane.
* - 'removed': hooks are opted out; Orca entries are swept from the real home.
*/
export type RealHomeCodexHookLane = 'pending' | 'installed' | 'unavailable' | 'removed'
let currentLane: RealHomeCodexHookLane = 'pending'
export function getRealHomeCodexHookLane(): RealHomeCodexHookLane {
return currentLane
}
/**
* Routing gate consumed by CodexRuntimeHomeService: the real home is usable
* whenever hooks are opted out (nothing to install) or the grant lane has not
* proven incapable on this host.
*/
export function isRealHomeCodexHookLaneUsable(hooksEnabled: boolean): boolean {
return !hooksEnabled || currentLane !== 'unavailable'
}
function getRealHomeHooksJsonPath(): string {
return join(getSystemCodexHomePath(), 'hooks.json')
}
function getRealHomeConfigTomlPath(): string {
return join(getSystemCodexHomePath(), 'config.toml')
}
/** Orca-side state dir; nothing extra is ever written into the user's ~/.codex. */
function getRealHomeHookStateDir(userDataPath: string): string {
return join(userDataPath, 'codex-real-home-hooks')
}
/**
* Ensures the real-home hook state matches the settings: installs and trusts
* the Orca status hook when enabled, sweeps it when opted out. Idempotent and
* synchronous (launch prep); repeat calls are cheap — an unchanged hooks.json
* write no-ops and a valid grant ledger skips the RPC session entirely.
* Never throws: any failure logs and leaves the host on the managed lane.
*/
export function ensureRealHomeCodexHookState(args: {
hooksEnabled: boolean
userDataPath: string
}): RealHomeCodexHookLane {
try {
currentLane = args.hooksEnabled
? installRealHomeCodexHook(args.userDataPath)
: sweepRealHomeCodexHook()
} catch (error) {
console.warn('[codex-real-home-hooks] ensure failed; staying on managed lane:', error)
currentLane = args.hooksEnabled ? 'unavailable' : currentLane
}
return currentLane
}
function installRealHomeCodexHook(userDataPath: string): RealHomeCodexHookLane {
const material = getCodexManagedHookInstallMaterial()
const hooksJsonPath = getRealHomeHooksJsonPath()
const config = readHooksJson(hooksJsonPath)
if (!config) {
// Why: an unparseable user file must never be clobbered; without a hook
// entry the managed lane keeps status working for this host.
console.warn('[codex-real-home-hooks] could not parse', hooksJsonPath, '- managed lane kept')
return 'unavailable'
}
// Why: the same script the managed lane maintains; deploying here too keeps
// host-connect ordering independent of the managed installer loop.
writeManagedScript(material.scriptPath, material.script)
const isManagedCommand = createManagedCommandMatcher(getCodexManagedScriptFileName())
const nextHooks: Record<string, HookDefinition[]> = { ...config.hooks }
const managedEntries: CodexTrustEntry[] = []
for (const eventName of material.events) {
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : []
const cleaned = removeManagedCommands(current, isManagedCommand)
// Why: append LAST. Codex trust keys are positional
// (source:event:group:handler); prepending would shift every user entry
// and invalidate the user's own hook trust records.
nextHooks[eventName] = [...cleaned, { hooks: [buildManagedCommandHook(material.command)] }]
managedEntries.push({
sourcePath: hooksJsonPath,
eventLabel: material.eventLabel[eventName],
groupIndex: cleaned.length,
handlerIndex: 0,
command: material.command,
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
})
}
// Why: sweep stale Orca entries out of events the managed lane no longer
// subscribes to, mirroring the managed installer's upgrade behavior.
for (const [eventName, definitions] of Object.entries(nextHooks)) {
if ((material.events as readonly string[]).includes(eventName) || !Array.isArray(definitions)) {
continue
}
const cleaned = removeManagedCommands(definitions, isManagedCommand)
if (cleaned.length === 0) {
delete nextHooks[eventName]
} else {
nextHooks[eventName] = cleaned
}
}
const previousRaw = existsSync(hooksJsonPath) ? readFileSync(hooksJsonPath, 'utf-8') : null
backupRealHomeHooksJsonOnce(userDataPath, hooksJsonPath, previousRaw)
// Why: unknown top-level fields belong to the user (other managers'
// metadata); unlike the managed-home writer, preserve them verbatim.
writeHooksJson(hooksJsonPath, { ...config, hooks: nextHooks } as HooksConfig)
const grant = grantManagedCodexHookTrust({
runtimeHomePath: getSystemCodexHomePath(),
tomlPath: getRealHomeConfigTomlPath(),
managedCommand: material.command,
managedEntries,
host: { kind: 'native' }
})
if (grant.lane === 'rpc') {
return 'installed'
}
// Why: never leave an untrusted Orca entry in the user's real home — it
// would surface as "Hooks need review". Roll the file back to its prior
// bytes and keep this host on the managed-home lane; the grant client
// already logged the fallback reason.
restoreRealHomeHooksJson(hooksJsonPath, previousRaw)
console.warn(
`[codex-real-home-hooks] trust grant unavailable (${grant.reason}); entry rolled back, managed lane kept`
)
return 'unavailable'
}
function sweepRealHomeCodexHook(): RealHomeCodexHookLane {
const hooksJsonPath = getRealHomeHooksJsonPath()
const config = readHooksJson(hooksJsonPath)
if (!config?.hooks) {
return 'removed'
}
const isManagedCommand = createManagedCommandMatcher(getCodexManagedScriptFileName())
const material = getCodexManagedHookInstallMaterial()
const nextHooks: Record<string, HookDefinition[]> = { ...config.hooks }
const removedTrustKeys: string[] = []
let removedAny = false
for (const [eventName, definitions] of Object.entries(nextHooks)) {
if (!Array.isArray(definitions)) {
continue
}
definitions.forEach((definition, groupIndex) => {
if (!hookDefinitionHasManagedCommand(definition, isManagedCommand)) {
return
}
const eventLabel = material.eventLabel[eventName as (typeof material.events)[number]]
if (!eventLabel) {
return
}
removedTrustKeys.push(
computeTrustKey({
sourcePath: hooksJsonPath,
eventLabel,
groupIndex,
handlerIndex: 0,
command: material.command,
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
})
)
})
const cleaned = removeManagedCommands(definitions, isManagedCommand)
if (cleaned.length !== definitions.length) {
removedAny = true
}
if (cleaned.length === 0) {
delete nextHooks[eventName]
} else {
nextHooks[eventName] = cleaned
}
}
if (removedAny) {
writeHooksJson(hooksJsonPath, { ...config, hooks: nextHooks } as HooksConfig)
// Why: dead [hooks.state] blocks for a removed hook are Orca-owned records;
// dropping them keeps the user's config.toml from accumulating orphans.
// Their removal never shifts user trust keys because Orca appends last.
try {
removeHookTrustEntries(getRealHomeConfigTomlPath(), removedTrustKeys)
} catch (error) {
console.warn('[codex-real-home-hooks] failed to drop Orca trust entries:', error)
}
try {
removeCodexTrustGrantLedgerHome(getSystemCodexHomePath())
} catch {
// Ledger cleanup is bookkeeping only; the next grant rebuilds it.
}
}
return 'removed'
}
/** One-time pristine copy of the user's file, kept under Orca's userData. */
function backupRealHomeHooksJsonOnce(
userDataPath: string,
hooksJsonPath: string,
previousRaw: string | null
): void {
if (previousRaw === null) {
return
}
try {
const backupDir = getRealHomeHookStateDir(userDataPath)
const backupPath = join(backupDir, 'hooks.json.pre-orca')
if (existsSync(backupPath)) {
return
}
mkdirSync(backupDir, { recursive: true })
copyFileSync(hooksJsonPath, backupPath)
} catch (error) {
console.warn('[codex-real-home-hooks] failed to write pristine backup:', error)
}
}
function restoreRealHomeHooksJson(hooksJsonPath: string, previousRaw: string | null): void {
try {
if (previousRaw === null) {
if (existsSync(hooksJsonPath)) {
unlinkSync(hooksJsonPath)
}
return
}
const tmpPath = `${hooksJsonPath}.${process.pid}.rollback.tmp`
writeFileSync(tmpPath, previousRaw, 'utf-8')
renameSync(tmpPath, hooksJsonPath)
} catch (error) {
console.warn('[codex-real-home-hooks] failed to roll back hooks.json:', error)
}
}
export const _internals = {
setLaneForTesting(lane: RealHomeCodexHookLane): void {
currentLane = lane
}
}
+35
View File
@@ -142,6 +142,38 @@ function getManagedCommand(scriptPath: string): string {
: wrapPosixHookCommand(scriptPath)
}
export type CodexManagedHookInstallMaterial = {
events: readonly (typeof CODEX_EVENTS)[number][]
eventLabel: Record<(typeof CODEX_EVENTS)[number], CodexEventLabel>
scriptPath: string
command: string
script: string
}
// Why: the real-home installer must byte-match the managed lane's events,
// command, and script, or trust signatures diverge between the two homes.
export function getCodexManagedHookInstallMaterial(): CodexManagedHookInstallMaterial {
const scriptPath = getManagedScriptPath()
return {
events: CODEX_EVENTS,
eventLabel: CODEX_EVENT_LABEL,
scriptPath,
command: getManagedCommand(scriptPath),
script: getManagedScript()
}
}
// Why: when the real-home lane owns ~/.codex/hooks.json (system-default flag ON
// with hooks enabled), the legacy system-home sweep must stand down or every
// managed install would delete the entry the real-home installer just wrote.
// Injected as a gate because this module is bundled into plain-node CLI entries
// that have no settings store; the CLI default keeps the sweep active.
let systemCodexHomeHookSweepSuppressed: () => boolean = () => false
export function setSystemCodexHomeHookSweepSuppressed(gate: () => boolean): void {
systemCodexHomeHookSweepSuppressed = gate
}
export { createCodexWslRuntimeHookInstallPlan }
export type { CodexWslRuntimeHookInstallPlan }
@@ -539,6 +571,9 @@ function dedupeHookDefinitions(definitions: readonly HookDefinition[]): HookDefi
}
function cleanupLegacySystemManagedHooks(): void {
if (systemCodexHomeHookSweepSuppressed()) {
return
}
const legacyConfigPath = getSystemConfigPath()
const runtimeConfigPath = getConfigPath()
if (legacyConfigPath === runtimeConfigPath) {
+41 -5
View File
@@ -128,7 +128,11 @@ import {
type CodexAccountSelectionTarget
} from './codex-accounts/runtime-selection'
import { normalizeClaudeRuntimeSelection } from './claude-accounts/runtime-selection'
import { codexHookService } from './codex/hook-service'
import { codexHookService, setSystemCodexHomeHookSweepSuppressed } from './codex/hook-service'
import {
ensureRealHomeCodexHookState,
isRealHomeCodexHookLaneUsable
} from './codex/codex-real-home-hook-install'
import { setCodexTrustGrantTelemetry } from './codex/codex-hook-trust-grant'
import { getDefaultWslDistro } from './wsl'
import { ClaudeAccountService } from './claude-accounts/service'
@@ -734,12 +738,20 @@ function startTerminalRuntimeStartupServices(): Promise<void> {
}
function prepareCodexRuntimeHomeForLaunch(target?: CodexAccountSelectionTarget): string | null {
if (target?.runtime !== 'wsl' && codexRuntimeHome!.isHostSystemDefaultRealHomeSelected()) {
// Why (flag ON, system default): the hook entry must exist — appended last
// and trusted by codex's own app-server grant — in the real ~/.codex before
// the pane spawns. An incapable grant flips the lane gate so the launch
// below falls back to the managed home instead of a status-blind pane.
ensureRealHomeCodexHookState({
hooksEnabled: isAgentStatusHooksEnabled(store?.getSettings()),
userDataPath: app.getPath('userData')
})
}
const runtimeHomePath = codexRuntimeHome!.prepareForCodexLaunch(target)
if (runtimeHomePath === null && codexRuntimeHome!.isHostSystemDefaultRealHome()) {
// Why (flag ON, system default): Codex runs on the user's real ~/.codex, so
// the managed-home hook install below would target a home Codex never reads.
// The real-home hook installer (trust granted via the app-server client)
// owns hook install for this lane and lands with the trust plumbing.
// Why: Codex runs on the user's real ~/.codex; the managed-home hook
// install below would target a home Codex never reads on this lane.
return null
}
const hookTarget =
@@ -1766,6 +1778,21 @@ app.whenReady().then(async () => {
openCodeUsage = new OpenCodeUsageStore(store)
rateLimits = new RateLimitService()
codexRuntimeHome = new CodexRuntimeHomeService(store)
// Why: an incapable trust-grant host must fall back to the managed home for
// every consumer (PTY env, rate limits, commit messages) in one place.
codexRuntimeHome.setRealHomeLaneGate(() =>
isRealHomeCodexHookLaneUsable(isAgentStatusHooksEnabled(store?.getSettings()))
)
// Why: while the real-home lane owns ~/.codex/hooks.json, the legacy
// system-home sweep inside managed installs would delete the entry the
// real-home installer just appended. Flag OFF or hooks off re-arms the sweep,
// which is also what removes the entry again on downgrade or opt-out.
setSystemCodexHomeHookSweepSuppressed(
() =>
codexRuntimeHome !== null &&
codexRuntimeHome.isHostSystemDefaultRealHomeSelected() &&
isAgentStatusHooksEnabled(store?.getSettings())
)
codexAccounts = new CodexAccountService(store, rateLimits, codexRuntimeHome)
claudeRuntimeAuth = new ClaudeRuntimeAuthService(store)
claudeAccounts = new ClaudeAccountService(store, rateLimits, claudeRuntimeAuth)
@@ -1992,6 +2019,15 @@ app.whenReady().then(async () => {
removeManagedAgentHooks()
}
}
if (codexRuntimeHome.isHostSystemDefaultRealHomeSelected()) {
// Why: host-connect seam — install (or sweep, when opted out) the trusted
// real-home status hook before the first pane can spawn, so the very first
// launch already knows whether this host's grant lane is usable.
ensureRealHomeCodexHookState({
hooksEnabled: isAgentStatusHooksEnabled(store.getSettings()),
userDataPath: app.getPath('userData')
})
}
app.on('child-process-gone', (_event, details) => {
recordProcessGoneCrash('child', details.type, details.reason, details.exitCode ?? null, {