Created
July 27, 2026 22:12
-
-
Save arikon/1f5af782bfb42e919b41bb2bc2c86d76 to your computer and use it in GitHub Desktop.
oh-my-codex #3321: macOS /var path canonicalization patch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| From 435e778f4a17bda4a0eee070536a4086261659f0 Mon Sep 17 00:00:00 2001 | |
| From: Sergey Belov <peimei@ya.ru> | |
| Date: Tue, 28 Jul 2026 00:12:30 +0200 | |
| Subject: [PATCH] fix(hook): canonicalize macOS var paths | |
| --- | |
| .../__tests__/codex-native-hook.test.ts | 7 +- | |
| src/scripts/codex-native-hook.ts | 89 +++++++++++++------ | |
| src/utils/__tests__/paths.test.ts | 22 +++++ | |
| src/utils/paths.ts | 18 ++-- | |
| 4 files changed, 101 insertions(+), 35 deletions(-) | |
| diff --git a/src/scripts/__tests__/codex-native-hook.test.ts b/src/scripts/__tests__/codex-native-hook.test.ts | |
| index 05f96ed2..0cc3debd 100644 | |
| --- a/src/scripts/__tests__/codex-native-hook.test.ts | |
| +++ b/src/scripts/__tests__/codex-native-hook.test.ts | |
| @@ -64,6 +64,7 @@ import { | |
| import { getBaseStateDir } from "../../state/paths.js"; | |
| import { maybeNudgeLeaderForAllowedWorkerStop } from "../notify-hook/team-worker-stop.js"; | |
| import { MAX_NATIVE_STDIN_JSON_BYTES } from "../hook-payload-guard.js"; | |
| +import { sameFilePath } from "../../utils/paths.js"; | |
| const ARGUMENT_PRODUCING_RUNTIME_DENIAL_COMMANDS = [ | |
| @@ -5048,10 +5049,10 @@ PY`, | |
| workingDirectory: cwd, | |
| }); | |
| assert.notEqual(terminalWrite.isError, true); | |
| - assert.equal( | |
| - (terminalWrite.payload as { path?: string }).path, | |
| + assert.equal(sameFilePath( | |
| + String((terminalWrite.payload as { path?: string }).path ?? ""), | |
| join(stateDir, "sessions", canonicalSessionId, "deep-interview-state.json"), | |
| - ); | |
| + ), true); | |
| assert.equal(existsSync(join(stateDir, "sessions", ownerSessionId, "deep-interview-state.json")), false); | |
| const stop = await dispatchCodexNativeHook( | |
| diff --git a/src/scripts/codex-native-hook.ts b/src/scripts/codex-native-hook.ts | |
| index 9a13f337..e81eeecc 100644 | |
| --- a/src/scripts/codex-native-hook.ts | |
| +++ b/src/scripts/codex-native-hook.ts | |
| @@ -74,7 +74,12 @@ import { | |
| writeTeamPhase, | |
| } from "../team/state.js"; | |
| import { parseTeamNoticeLedgerPrompt, reconcileTeamNoticeLedger } from "../team/notice-ledger.js"; | |
| -import { omxNotepadPath, resolveProjectMemoryPath } from "../utils/paths.js"; | |
| +import { | |
| + canonicalizeComparablePath, | |
| + omxNotepadPath, | |
| + resolveProjectMemoryPath, | |
| + sameFilePath, | |
| +} from "../utils/paths.js"; | |
| import { findGitLayout } from "../utils/git-layout.js"; | |
| import { | |
| getAuthoritativeActiveStatePaths, | |
| @@ -2804,14 +2809,14 @@ async function hasAuthoritativeTeamWorkerContext(cwd: string): Promise<boolean> | |
| const config = await readJsonIfExists(join(teamRoot, "config.json")); | |
| if (!identity || !manifest || !config) return false; | |
| - const canonicalStateRoot = resolve(stateRoot); | |
| - const canonicalCwd = resolve(cwd); | |
| - const canonicalLeaderCwd = resolve(safeString(process.env.OMX_TEAM_LEADER_CWD).trim() || cwd); | |
| + const canonicalStateRoot = canonicalizeComparablePath(stateRoot); | |
| + const canonicalCwd = canonicalizeComparablePath(cwd); | |
| + const canonicalLeaderCwd = canonicalizeComparablePath(safeString(process.env.OMX_TEAM_LEADER_CWD).trim() || cwd); | |
| const pathMatches = (value: unknown, expected: string): boolean => { | |
| const candidate = safeString(value).trim(); | |
| if (!candidate) return false; | |
| try { | |
| - return resolve(candidate) === expected; | |
| + return sameFilePath(candidate, expected); | |
| } catch { | |
| return false; | |
| } | |
| @@ -3748,7 +3753,7 @@ function hookCancelObject(bytes: Buffer): Record<string, unknown> | null { | |
| function hookCancelString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } | |
| function hookCancelCwdMatches(value: Record<string, unknown>, canonicalCwd: string): boolean { | |
| const recorded = hookCancelString(value.cwd) || hookCancelString(value.workingDirectory); | |
| - try { return !recorded || resolve(recorded) === canonicalCwd; } catch { return false; } | |
| + try { return !recorded || sameFilePath(recorded, canonicalCwd); } catch { return false; } | |
| } | |
| function hookCancelSessionMatches(value: Record<string, unknown>, sessionId: string, cwd: string): boolean { | |
| const recorded = hookCancelString(value.session_id); | |
| @@ -4094,7 +4099,7 @@ function isFreshNativeSubagentCapacityBlocker( | |
| const blockerCwd = safeString(blocker.cwd).trim(); | |
| if (blockerCwd) { | |
| try { | |
| - if (resolve(blockerCwd) !== resolve(cwd)) return false; | |
| + if (!sameFilePath(blockerCwd, cwd)) return false; | |
| } catch { | |
| return false; | |
| } | |
| @@ -4336,10 +4341,7 @@ function normalizePlanningArtifactRelativePath(cwd: string, rawPath: string): st | |
| const trimmed = rawPath.trim(); | |
| if (!trimmed || trimmed.includes("\0")) return null; | |
| try { | |
| - const absolute = resolve(cwd, trimmed); | |
| - const relativePath = relative(cwd, absolute).replace(/\\/g, "/"); | |
| - if (!relativePath || relativePath.startsWith("..") || relativePath.startsWith("/")) return null; | |
| - return relativePath; | |
| + return normalizePolicyRelativePath(cwd, resolve(cwd, trimmed)); | |
| } catch { | |
| return null; | |
| } | |
| @@ -8884,7 +8886,7 @@ function isStandaloneParsedOmxStateWriteTransport(cwd: string, command: string, | |
| || safeString(payload.session_id).trim() !== authoritativeSessionId | |
| || !suppliedSessionAliasesMatch(payload, authoritativeSessionId) | |
| || safeString(payload.workingDirectory).trim() === "" | |
| - || resolve(safeString(payload.workingDirectory)) !== resolve(cwd)) return false; | |
| + || !sameFilePath(safeString(payload.workingDirectory), cwd)) return false; | |
| const usesInputFile = readStateWriteFlagValue(stateWriteOperation.args, "--input-file") !== undefined; | |
| if (usesInputFile && splitStateScanSegments(canonicalCommand).length !== 1) return false; | |
| if (extractDeepInterviewCommandRedirectTargets(command).length > 0) return false; | |
| @@ -9033,7 +9035,7 @@ export function evaluateDeepInterviewRalplanHandoffCommand( | |
| if (!suppliedSessionAliasesMatch(payload, authoritativeSessionId)) return rejectDeepInterviewRalplanHandoff("session_alias_conflict"); | |
| const workingDirectory = safeString(payload.workingDirectory).trim(); | |
| if (!workingDirectory) return rejectDeepInterviewRalplanHandoff("working_directory_missing"); | |
| - if (resolve(workingDirectory) !== resolve(cwd)) return rejectDeepInterviewRalplanHandoff("working_directory_mismatch"); | |
| + if (!sameFilePath(workingDirectory, cwd)) return rejectDeepInterviewRalplanHandoff("working_directory_mismatch"); | |
| const targets = extractDeepInterviewCommandWriteTargets(command); | |
| if (targets.length === 0) { | |
| if (hasPriorExecutableCommand(stateWriteOperation.prefix)) return rejectDeepInterviewRalplanHandoff("unsafe_transport"); | |
| @@ -9086,7 +9088,7 @@ function isCompleteRalplanTerminalWritePayload( | |
| ).trim(); | |
| if (payloadSessionId !== sessionId) return false; | |
| if (!suppliedSessionAliasesMatch(payload, sessionId)) return false; | |
| - if (safeString(payload.workingDirectory).trim() === "" || resolve(safeString(payload.workingDirectory)) !== resolve(cwd)) return false; | |
| + if (safeString(payload.workingDirectory).trim() === "" || !sameFilePath(safeString(payload.workingDirectory), cwd)) return false; | |
| if (activeSessionId && payloadSessionId !== activeSessionId) return false; | |
| return true; | |
| } | |
| @@ -9154,7 +9156,7 @@ function isCompleteDeepInterviewTerminalWritePayload( | |
| const phase = safeString(payload.current_phase ?? payload.currentPhase).trim().toLowerCase(); | |
| const payloadSessionId = safeString(payload.session_id).trim(); | |
| if (!hasOnlyFinishedExplicitOutcomes(payload)) return false; | |
| - if (safeString(payload.workingDirectory).trim() === "" || resolve(safeString(payload.workingDirectory)) !== resolve(cwd)) return false; | |
| + if (safeString(payload.workingDirectory).trim() === "" || !sameFilePath(safeString(payload.workingDirectory), cwd)) return false; | |
| return mode === "deep-interview" | |
| && payload.active === false | |
| && phase === "complete" | |
| @@ -9540,7 +9542,7 @@ function isAllowedDeepInterviewBashWrite( | |
| || safeString(payload.session_id).trim() !== sessionId | |
| || !suppliedSessionAliasesMatch(payload, sessionId) | |
| || safeString(payload.workingDirectory).trim() === "" | |
| - || resolve(safeString(payload.workingDirectory)) !== resolve(cwd)) return false; | |
| + || !sameFilePath(safeString(payload.workingDirectory), cwd)) return false; | |
| } | |
| if (commandHasUntargetedPlanningForbiddenIntent(command)) return false; | |
| if (firstPlanningTmpScriptExecutionTarget(cwd, command)) return false; | |
| @@ -10505,12 +10507,45 @@ function normalizeRepoRelativePath(cwd: string, rawPath: string): string | null | |
| const candidate = rawPath.trim(); | |
| if (!candidate || isUnresolvedVariableTarget(candidate)) return null; | |
| const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(cwd, candidate); | |
| - let relativePath = relative(cwd, absolute).replace(/\\/g, "/"); | |
| - if (!relativePath || relativePath === ".") return null; | |
| - if (relativePath.startsWith("../") || relativePath === "..") { | |
| - relativePath = candidate.replace(/\\/g, "/"); | |
| + const canonicalRelative = normalizePolicyRelativePath(cwd, absolute); | |
| + if (canonicalRelative) return canonicalRelative; | |
| + | |
| + let lexicalRelative = relative(cwd, absolute).replace(/\\/g, "/"); | |
| + if (!lexicalRelative || lexicalRelative === ".") return null; | |
| + if (lexicalRelative.startsWith("../") || lexicalRelative === "..") { | |
| + lexicalRelative = candidate.replace(/\\/g, "/"); | |
| + } | |
| + return lexicalRelative.replace(/^\.\//, ""); | |
| +} | |
| + | |
| +function normalizePolicyRelativePath(policyRoot: string, candidatePath: string): string | null { | |
| + const lexicalRoot = resolve(policyRoot); | |
| + const lexicalCandidate = resolve(candidatePath); | |
| + const lexicalRelative = relative(lexicalRoot, lexicalCandidate).replace(/\\/g, "/"); | |
| + if ( | |
| + lexicalRelative | |
| + && lexicalRelative !== "." | |
| + && !lexicalRelative.startsWith("../") | |
| + && lexicalRelative !== ".." | |
| + && conductorPathTraversesLink(lexicalRoot, lexicalRelative) | |
| + ) { | |
| + return null; | |
| + } | |
| + | |
| + const canonicalRoot = canonicalizeComparablePath(lexicalRoot); | |
| + const canonicalCandidate = canonicalizeComparablePath(lexicalCandidate); | |
| + const canonicalRelative = relative(canonicalRoot, canonicalCandidate).replace(/\\/g, "/"); | |
| + if ( | |
| + !canonicalRelative | |
| + || canonicalRelative === "." | |
| + || canonicalRelative.startsWith("../") | |
| + || canonicalRelative === ".." | |
| + || canonicalRelative.startsWith("/") | |
| + ) { | |
| + return null; | |
| } | |
| - return relativePath.replace(/^\.\//, ""); | |
| + if (conductorPathTraversesLink(canonicalRoot, canonicalRelative)) return null; | |
| + return canonicalRelative.replace(/^\.\//, ""); | |
| } | |
| function conductorPathTraversesLink(cwd: string, relativePath: string): boolean { | |
| @@ -17758,7 +17793,7 @@ function scanConductorShellSegment( | |
| } | |
| const omxGjcPathIsSafeForStaticOrchestration = trustedOmxGjcPackageCliPath; | |
| const omxGjcStateWriteDestinationIsCanonical = activeState.effectiveCwd !== null | |
| - && resolve(activeState.effectiveCwd) === resolve(rootCwd) | |
| + && sameFilePath(activeState.effectiveCwd, rootCwd) | |
| && !conductorInvocationUsesEnvCwdChangingWrapper(words, commandStartIndex, commandIndex); | |
| const mainRootStructuredStateWrite = omxGjcInheritedRootsAreCanonical | |
| && omxGjcPrefixEnvironmentIsSafe | |
| @@ -18865,7 +18900,7 @@ function conductorStateWriteTransportIsBoundToActiveSession( | |
| || safeString(statePayload.session_id).trim() !== authoritativeSessionId | |
| || !suppliedSessionAliasesMatch(statePayload, authoritativeSessionId) | |
| || safeString(statePayload.workingDirectory).trim() === "" | |
| - || resolve(safeString(statePayload.workingDirectory)) !== resolve(cwd) | |
| + || !sameFilePath(safeString(statePayload.workingDirectory), cwd) | |
| ) return false; | |
| // Inherited hook environment is process-authenticated; model-controlled shell | |
| // assignments must match the session resolved for this PreToolUse payload. | |
| @@ -19158,12 +19193,12 @@ async function directConductorStateWritePayloadHasExactSchema( | |
| while (nestedState) { | |
| if (!suppliedSessionAliasesMatch(nestedState, canonicalSessionId)) return false; | |
| const nestedWorkingDirectory = safeString(nestedState.workingDirectory).trim(); | |
| - if (nestedWorkingDirectory && resolve(nestedWorkingDirectory) !== resolve(policyCwd)) return false; | |
| + if (nestedWorkingDirectory && !sameFilePath(nestedWorkingDirectory, policyCwd)) return false; | |
| const nestedMode = safeString(nestedState.mode).trim(); | |
| if (nestedMode && nestedMode !== safeString(input.mode).trim()) return false; | |
| nestedState = nestedState.state === undefined ? null : safeObject(nestedState.state); | |
| } | |
| - if (safeString(input.workingDirectory).trim() === "" || resolve(safeString(input.workingDirectory)) !== resolve(policyCwd)) return false; | |
| + if (safeString(input.workingDirectory).trim() === "" || !sameFilePath(safeString(input.workingDirectory), policyCwd)) return false; | |
| return true; | |
| } | |
| @@ -19462,7 +19497,7 @@ function modeStateMatchesSkillStopContext( | |
| ).trim(); | |
| if (stateCwd) { | |
| try { | |
| - if (resolve(stateCwd) !== resolve(cwd)) return false; | |
| + if (!sameFilePath(stateCwd, cwd)) return false; | |
| } catch { | |
| return false; | |
| } | |
| @@ -19481,7 +19516,7 @@ function modeStateHasExplicitMatchingCwd(state: Record<string, unknown>, cwd: st | |
| if (!stateCwd) return false; | |
| try { | |
| - return resolve(stateCwd) === resolve(cwd); | |
| + return sameFilePath(stateCwd, cwd); | |
| } catch { | |
| return false; | |
| } | |
| diff --git a/src/utils/__tests__/paths.test.ts b/src/utils/__tests__/paths.test.ts | |
| index dd8025ae..14af1f4f 100644 | |
| --- a/src/utils/__tests__/paths.test.ts | |
| +++ b/src/utils/__tests__/paths.test.ts | |
| @@ -25,6 +25,7 @@ import { | |
| omxLogsDir, | |
| packageRoot, | |
| canonicalizeComparablePath, | |
| + sameFilePath, | |
| OMX_ENTRY_PATH_ENV, | |
| OMX_STARTUP_CWD_ENV, | |
| rememberOmxLaunchContext, | |
| @@ -32,6 +33,27 @@ import { | |
| resolveOmxEntryPath, | |
| } from "../paths.js"; | |
| +describe("comparable paths", () => { | |
| + it("leaves ordinary paths under a canonical root unchanged", async () => { | |
| + const root = await realpath(await mkdtemp(join(tmpdir(), "omx-comparable-path-"))); | |
| + try { | |
| + const missingChild = join(root, "missing", "child.json"); | |
| + assert.equal(canonicalizeComparablePath(missingChild), missingChild); | |
| + assert.equal(sameFilePath(missingChild, missingChild), true); | |
| + } finally { | |
| + await rm(root, { recursive: true, force: true }); | |
| + } | |
| + }); | |
| + | |
| + it("treats the macOS /var alias as the same path for missing descendants", { | |
| + skip: process.platform !== "darwin", | |
| + }, () => { | |
| + const aliasPath = join("/var", "tmp", "omx-comparable-missing", "state.json"); | |
| + const canonicalPath = join("/private", "var", "tmp", "omx-comparable-missing", "state.json"); | |
| + assert.equal(sameFilePath(aliasPath, canonicalPath), true); | |
| + }); | |
| +}); | |
| + | |
| describe("codexHome", () => { | |
| let originalCodexHome: string | undefined; | |
| let originalUserProfile: string | undefined; | |
| diff --git a/src/utils/paths.ts b/src/utils/paths.ts | |
| index a4a2b781..71713676 100644 | |
| --- a/src/utils/paths.ts | |
| +++ b/src/utils/paths.ts | |
| @@ -6,7 +6,7 @@ | |
| import { createHash } from "crypto"; | |
| import { existsSync, realpathSync } from "fs"; | |
| import { readdir, readFile, realpath } from "fs/promises"; | |
| -import { dirname, isAbsolute, join, resolve } from "path"; | |
| +import { basename, dirname, isAbsolute, join, resolve } from "path"; | |
| import { homedir } from "os"; | |
| import { fileURLToPath } from "url"; | |
| @@ -49,11 +49,19 @@ function resolveLauncherPath(rawPath: string, baseCwd: string): string { | |
| export function canonicalizeComparablePath(rawPath: string): string { | |
| const absolutePath = resolve(rawPath); | |
| - if (!existsSync(absolutePath)) return absolutePath; | |
| + let existingPath = absolutePath; | |
| + const missingSegments: string[] = []; | |
| + while (!existsSync(existingPath)) { | |
| + const parent = dirname(existingPath); | |
| + if (parent === existingPath) return absolutePath; | |
| + missingSegments.unshift(basename(existingPath)); | |
| + existingPath = parent; | |
| + } | |
| try { | |
| - return typeof realpathSync.native === "function" | |
| - ? realpathSync.native(absolutePath) | |
| - : realpathSync(absolutePath); | |
| + const canonicalExistingPath = typeof realpathSync.native === "function" | |
| + ? realpathSync.native(existingPath) | |
| + : realpathSync(existingPath); | |
| + return resolve(canonicalExistingPath, ...missingSegments); | |
| } catch { | |
| return absolutePath; | |
| } | |
| -- | |
| 2.50.1 (Apple Git-155) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment