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
- 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
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
- 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
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
- Frozen transaction payload is not the canonical staged trans
- invalid auth slot name: use 1-64 letters, numbers, '.', '_'
- agents-init target must stay inside the current working dire
- [api] failed to launch native binary: executable is blocked
- failed to launch codex login: executable is blocked (${error
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/63666a36edae8c6c.
Report an issue: GitHub.