{"record":{"id":"63666a36edae8c6c","repo":"Yeachan-Heo/oh-my-codex","slug":"frozen-transaction-staging-directory-is-not-an-own","errorCode":null,"errorMessage":"Frozen transaction staging directory is not an owner-only update stage.","messagePattern":"Frozen transaction staging directory is not an owner-only update stage\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/cli/update-worker.ts","lineNumber":93,"sourceCode":"    package_manager: ownership.manager,\n    updated_at: new Date().toISOString(),\n  }, omxUserInstallStampPath(ownership.environment.CODEX_HOME));\n}\n\nasync function main(): Promise<void> {\n  const payloadPath = process.argv[2];\n  const expectedDigest = process.argv[3];\n  let payload: DeferredUpdatePayload | null = null;\n\n  let stagedDirectory: string | null = null;\n  try {\n    const expectedWorkerDigest = process.argv[4];\n    if (!payloadPath || !expectedDigest || !expectedWorkerDigest) throw new Error('Frozen transaction payload is missing.');\n    const workerPath = await canonicalRegularFile(process.argv[1] ?? '', await realpath(join(process.argv[1] ?? '', '..')));\n    if (!workerPath || digest(await readFile(workerPath, 'utf-8')) !== expectedWorkerDigest) throw new Error('Frozen update worker identity changed before execution.');\n    const stage = await realpath(join(payloadPath, '..'));\n    if (!await ownerOnlyStage(stage) || !basename(stage).startsWith('omx-update-')) {\n      throw new Error('Frozen transaction staging directory is not an owner-only update stage.');\n    }\n    const stagedPayload = await canonicalRegularFile(payloadPath, stage);\n\n    if (!stagedPayload || stagedPayload !== join(stage, 'transaction.json')) {\n      throw new Error('Frozen transaction payload is not the canonical staged transaction file.');\n    }\n    const serialized = await readFile(stagedPayload, 'utf-8');\n    if (digest(serialized) !== expectedDigest) throw new Error('Frozen transaction payload fingerprint changed before execution.');\n    const parsedPayload: unknown = JSON.parse(serialized);\n    if (!isDeferredUpdatePayload(parsedPayload)) throw new Error('Frozen transaction payload is incomplete.');\n    payload = parsedPayload;\n    stagedDirectory = stage;\n    await waitForParent(payload.parentPid);\n    if (!await validatePackageManagerOwnership(payload.ownership)) throw new Error('Frozen manager, package root, or bin ownership validation failed before update.');\n    const result = payload.ownership.manager === 'npm'\n      ? runNpmCommand(payload.ownership.npmCommand, installArgs(payload.ownership), { ...installOptions, env: payload.ownership.environment })\n      : spawnSync(payload.ownership.bunCommand, installArgs(payload.ownership), { ...installOptions, env: payload.ownership.environment });\n    if (result.error || result.status !== 0) throw new Error(String(result.stderr || result.error?.message || 'controller install failed'));","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/update-worker.ts#L75-L111","documentation":"The deferred self-update worker verifies that the staging directory containing transaction.json is owned exclusively by the current user and follows the 'omx-update-' naming convention. This error means the stage directory failed the ownerOnlyStage check or the basename prefix check, so the worker refuses to execute a transaction staged in an unsafe location.","triggerScenarios":"Running the detached update-worker whose payload path resolves to a parent directory that is group/world accessible (mode bits & 0o077 set), owned by a different uid, or renamed to something not starting with 'omx-update-'.","commonSituations":"A security-hardened or restored-from-archive staging dir with widened permissions, a different user re-running the update, or manual tampering/renaming of the stage directory between staging and worker execution.","solutions":["Inspect the stage directory permissions: chmod 700 /path/to/omx-update-* and chown it to the current user","Re-run the update so a fresh 'omx-update-*' stage is created instead of reusing a modified one","Avoid running the update under sudo or a different account than the one that staged the transaction"],"exampleFix":"# before\nls -ld ~/.cache/omx-update-abc\ndrwxrwxr-x  root root ...\n# after\nchmod 700 ~/.cache/omx-update-abc\nchown $(id -u):$(id -g) ~/.cache/omx-update-abc","handlingStrategy":"validation","validationCode":"import { stat } from 'node:fs/promises';\nconst isOwnerOnlyStage = async (p: string) => {\n  const s = await stat(p);\n  return (s.mode & 0o077) === 0 && s.uid === process.getuid?.() && !s.isSymbolicLink();\n};","typeGuard":"const isSafeStage = (name: string, s: Stats) =>\n  name.startsWith('omx-update-') && !s.isSymbolicLink() && (s.mode & 0o077) === 0;","tryCatchPattern":"Catch around the update invocation and log message plus stage dir stat to identify which invariant (mode/uid/name) failed.","preventionTips":["Create stage dirs with mode 0o700 via fs.mkdir(dir, { mode: 0o700 })","Set umask 022 or tighter before starting updates","Run the updater as the same user that owns the install prefix"],"tags":["update-worker","permissions","staging","security"],"backgroundTag":"file-permission-validation-failed","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}