Yeachan-Heo/oh-my-codex · critical

Frozen transaction staging directory is not an owner-only up

Error message

Frozen transaction staging directory is not an owner-only update stage.

What it means

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.

Source

Thrown at src/cli/update-worker.ts:93

    package_manager: ownership.manager,
    updated_at: new Date().toISOString(),
  }, omxUserInstallStampPath(ownership.environment.CODEX_HOME));
}

async function main(): Promise<void> {
  const payloadPath = process.argv[2];
  const expectedDigest = process.argv[3];
  let payload: DeferredUpdatePayload | null = null;

  let stagedDirectory: string | null = null;
  try {
    const expectedWorkerDigest = process.argv[4];
    if (!payloadPath || !expectedDigest || !expectedWorkerDigest) throw new Error('Frozen transaction payload is missing.');
    const workerPath = await canonicalRegularFile(process.argv[1] ?? '', await realpath(join(process.argv[1] ?? '', '..')));
    if (!workerPath || digest(await readFile(workerPath, 'utf-8')) !== expectedWorkerDigest) throw new Error('Frozen update worker identity changed before execution.');
    const stage = await realpath(join(payloadPath, '..'));
    if (!await ownerOnlyStage(stage) || !basename(stage).startsWith('omx-update-')) {
      throw new Error('Frozen transaction staging directory is not an owner-only update stage.');
    }
    const stagedPayload = await canonicalRegularFile(payloadPath, stage);

    if (!stagedPayload || stagedPayload !== join(stage, 'transaction.json')) {
      throw new Error('Frozen transaction payload is not the canonical staged transaction file.');
    }
    const serialized = await readFile(stagedPayload, 'utf-8');
    if (digest(serialized) !== expectedDigest) throw new Error('Frozen transaction payload fingerprint changed before execution.');
    const parsedPayload: unknown = JSON.parse(serialized);
    if (!isDeferredUpdatePayload(parsedPayload)) throw new Error('Frozen transaction payload is incomplete.');
    payload = parsedPayload;
    stagedDirectory = stage;
    await waitForParent(payload.parentPid);
    if (!await validatePackageManagerOwnership(payload.ownership)) throw new Error('Frozen manager, package root, or bin ownership validation failed before update.');
    const result = payload.ownership.manager === 'npm'
      ? runNpmCommand(payload.ownership.npmCommand, installArgs(payload.ownership), { ...installOptions, env: payload.ownership.environment })
      : spawnSync(payload.ownership.bunCommand, installArgs(payload.ownership), { ...installOptions, env: payload.ownership.environment });
    if (result.error || result.status !== 0) throw new Error(String(result.stderr || result.error?.message || 'controller install failed'));

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Inspect the stage directory permissions: chmod 700 /path/to/omx-update-* and chown it to the current user
  2. Re-run the update so a fresh 'omx-update-*' stage is created instead of reusing a modified one
  3. Avoid running the update under sudo or a different account than the one that staged the transaction

Example fix

# before
ls -ld ~/.cache/omx-update-abc
drwxrwxr-x  root root ...
# after
chmod 700 ~/.cache/omx-update-abc
chown $(id -u):$(id -g) ~/.cache/omx-update-abc
Defensive patterns

Strategy: validation

Validate before calling

import { stat } from 'node:fs/promises';
const isOwnerOnlyStage = async (p: string) => {
  const s = await stat(p);
  return (s.mode & 0o077) === 0 && s.uid === process.getuid?.() && !s.isSymbolicLink();
};

Type guard

const isSafeStage = (name: string, s: Stats) =>
  name.startsWith('omx-update-') && !s.isSymbolicLink() && (s.mode & 0o077) === 0;

Try / catch

Catch around the update invocation and log message plus stage dir stat to identify which invariant (mode/uid/name) failed.

Prevention

When it happens

Trigger: 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-'.

Common situations: 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.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/63666a36edae8c6c. Report an issue: GitHub.