Yeachan-Heo/oh-my-codex · error · Error

Native hook transaction rollback preserved ${artifact.path}

Error message

Native hook transaction rollback preserved ${artifact.path} for manual recovery because ${artifact.label} no longer matches the transaction-owned version.

What it means

During rollback of a native hook transaction, setup re-captures the artifact on disk and compares it to the snapshot taken when the change was applied. If they differ, the file was modified by something else after the transaction applied it, so rollback refuses to overwrite it and preserves it for manual recovery.

Source

Thrown at src/cli/setup.ts:1793

		if (!validation.ok) {
			throw new Error(
				`Native hook transaction wrote invalid hooks.json: ${validation.error.message}`,
			);
		}
	}
	if (artifact.kind === "config") TOML.parse(content);
}

async function restoreNativeHookTransactionArtifact(
	applied: AppliedNativeHookTransactionArtifact,
	ancestorPrecondition: NativeHookTransactionAncestorPrecondition,
	tracker: RegularFileDurabilityTracker,
	assertRollbackState: () => Promise<void>,
): Promise<void> {
	const { artifact } = applied;
	const current = await captureNativeHookTransactionArtifact(artifact.path, artifact.label);
	if (!nativeHookTransactionSnapshotsEqual(current, applied.appliedSnapshot)) {
		throw new Error(
			`Native hook transaction rollback preserved ${artifact.path} for manual recovery because ${artifact.label} no longer matches the transaction-owned version.`,
		);
	}
	if (artifact.before.bytes === null) {
		injectNativeHookTransactionFailure("before_rollback", artifact);
		injectNativeHookTransactionFailure("before_rollback_remove", artifact);
		await assertNativeHookTransactionArtifactSnapshot(
			artifact.path,
			artifact.label,
			applied.appliedSnapshot,
			"rollback",
		);
		await assertNativeHookTransactionAncestorPrecondition(ancestorPrecondition);
		await assertRollbackState();
		injectNativeHookTransactionFailure("after_final_restore_validation", artifact);
		const claimPath = nativeHookTransactionClaimPath(artifact.path);

		await rename(artifact.path, claimPath);

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Diff the preserved file against the expected transaction-owned version and reconcile edits manually
  2. Ensure only one setup/transaction process runs at a time (lock file)
  3. Re-run setup after manual reconciliation so it starts from a known state
  4. Remove test fault-injection points that mutate files mid-transaction
Defensive patterns

Strategy: try-catch

Validate before calling

const snapshot = await captureNativeHookTransactionArtifact(path, label);
if (!nativeHookTransactionSnapshotsEqual(snapshot, expected)) {
  console.warn(`${path} changed externally; aborting before apply`);
}

Try / catch

try { await tx.rollback(); } catch (e) { if (e instanceof Error && e.message.includes("no longer matches the transaction-owned version")) { /* diff preserved file vs expected, reconcile manually */ } else throw e; }

Prevention

When it happens

Trigger: Rolling back a transaction after another process (editor, second setup run, user edit) modified hooks.json or config.toml between apply and rollback.

Common situations: Concurrent setup invocations, a hook-editing tool or editor autosave racing the transaction, or delayed fault-injection in tests.

Related errors


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