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

Native hook claim journal path is outside controlled root: $

Error message

Native hook claim journal path is outside controlled root: ${path}

What it means

The native hook claim journal refuses a path that is not strictly inside its controlled root: the path equals the root, resolves above it via '..', or is absolute relative to it. This containment guard prevents the journal (used to record and recover cross-process hook claims) from reading/writing outside its directory.

Source

Thrown at src/cli/native-hook-claim-journal.ts:41

	canonicalPath: string;
	claimPath: string;
	beforeHash: string;
	afterHash: string | null;
}

function digest(bytes: Buffer): string {
	return createHash("sha256").update(bytes).digest("hex");
}

function isMissing(error: unknown): boolean {
	return typeof error === "object" && error !== null && "code" in error &&
		(error as { code?: unknown }).code === "ENOENT";
}

function assertControlledPath(root: string, path: string): void {
	const rel = relative(root, path);
	if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`) || rel === "") {
		throw new Error(`Native hook claim journal path is outside controlled root: ${path}`);
	}
}

function processIsAlive(pid: number): boolean {
	try {
		process.kill(pid, 0);
		return true;
	} catch (error) {
		return typeof error === "object" && error !== null && "code" in error &&
			(error as { code?: unknown }).code === "EPERM";
	}
}

type NativeHookClaimJournalOpen = (
	path: string,
	flags: "r",
) => Promise<Pick<FileHandle, "sync" | "close">>;

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Construct the journal path with join(root, name) using the same root you pass to the API.
  2. Never pass absolute paths unless produced from the root itself.
  3. Re-check both arguments after refactors that move the journal root.

Example fix

// before
journalPath = resolve(process.cwd(), 'claims.json'); // root may be elsewhere
// after
journalPath = join(root, 'claims.json');
Defensive patterns

Strategy: validation

Validate before calling

import { isAbsolute, relative, sep } from 'node:path';
function isControlledJournalPath(root: string, p: string): boolean {
  const rel = relative(root, p);
  return !isAbsolute(rel) && rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`);
}

Type guard

function isSafeJournalPath(root: string, p: string): boolean { try { assertControlledPathCompat(root, p); return true; } catch { return false; } }

Try / catch

try { persistNativeHookClaimJournal(root, path); } catch (e) { if (/outside controlled root/.test(String(e))) { /* rebuild path with join(root, name) */ } throw e; }

Prevention

When it happens

Trigger: persistNativeHookClaimJournal or recoverNativeHookClaimJournal called with a journal path built outside the root — wrong root argument, absolute path not derived from the root, or path traversal components in the filename.

Common situations: Mismatch between the configured journal root and the path passed by callers after config changes; tests constructing paths with resolve() against a different CWD; user-supplied filenames containing '..'.

Related errors


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