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

Refusing to back up ${artifact.path} outside controlled back

Error message

Refusing to back up ${artifact.path} outside controlled backup root.

What it means

Before writing a transaction backup, setup computes the backup path relative to the controlled backup root and rejects any path that escapes it (absolute relative path, '..', or '..' prefix). This is a path-traversal guard ensuring backups never land outside the managed backup root.

Source

Thrown at src/cli/setup.ts:1974

}

async function ensureSnapshotBackup(
	artifact: NativeHookTransactionArtifact,
	backupContext: SetupBackupContext,
	options: Pick<SetupOptions, "dryRun" | "verbose">,
	tracker: RegularFileDurabilityTracker,
): Promise<boolean> {
	const bytes = artifact.before.bytes;
	if (bytes === null) return false;
	const backupPath = nativeHookTransactionBackupPath(artifact.path, backupContext);
	if (!options.dryRun) {
		const relativeParent = relative(backupContext.baseRoot, dirname(backupPath));
		if (
			isAbsolute(relativeParent) ||
			relativeParent === ".." ||
			relativeParent.startsWith(`..${sep}`)
		) {
			throw new Error(`Refusing to back up ${artifact.path} outside controlled backup root.`);
		}
		let currentPath = backupContext.baseRoot;
		for (const component of relativeParent.split(sep).filter(Boolean)) {
			currentPath = join(currentPath, component);
			try {
				const currentStat = await lstat(currentPath);
				if (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {
					throw new Error(`Refusing to use unsafe backup ancestor ${currentPath}.`);
				}
			} catch (error) {
				if (!isMissingPathError(error)) throw error;
				await mkdir(currentPath);
				const createdStat = await lstat(currentPath);
				if (createdStat.isSymbolicLink() || !createdStat.isDirectory()) {
					throw new Error(`Refusing to use unsafe created backup ancestor ${currentPath}.`);
				}
			}
		}

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Sanitize artifact.path: strip .., absolute prefixes, and symlinked segments before constructing the backup path
  2. Verify backupContext.baseRoot is the intended controlled root
  3. Log artifact.path and the computed backupPath to spot the escaping segment
  4. Treat this as a security signal: audit where artifact.path originates

Example fix

// before
const backupPath = join(backupContext.baseRoot, artifact.path);
// after
const safeRel = artifact.path.split(/[\\/]/).filter((p) => p && p !== "." && p !== "..").join(sep);
const backupPath = join(backupContext.baseRoot, safeRel);
Defensive patterns

Strategy: validation

Validate before calling

const isSafeRelative = (p: string) =>
  !p.includes("..") && !isAbsolute(p) && p.split(/[\\/]/).every((s) => s && s !== ".");
if (!isSafeRelative(artifact.path)) throw new Error(`unsafe artifact path: ${artifact.path}`);

Type guard

const isSafeArtifactPath = (p: string): p is `${string}.${string}` =>
  typeof p === "string" && !isAbsolute(p) && !p.split(/[\\/]/).includes("..");

Try / catch

try { await createBackup(artifact); } catch (e) { if (e instanceof Error && e.message.startsWith("Refusing to back up")) { /* sanitize artifact.path and retry */ } else throw e; }

Prevention

When it happens

Trigger: An artifact path containing traversal segments (../) or an absolute path that, when joined under backupContext.baseRoot, resolves outside it.

Common situations: Malicious or malformed artifact paths, tampered backup context, or bugs constructing backupPath from user-supplied hook file names.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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