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
- Construct the journal path with join(root, name) using the same root you pass to the API.
- Never pass absolute paths unless produced from the root itself.
- 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
- Always build journal paths with join(root, name)
- Pass the same root object/variable to both arguments
- Reject user-supplied '..' components in filenames
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
- [native-assets] cache path escapes configured root
- [ask] invalid --agent-prompt role "${role}". Expected lowerc
- Unknown hooks subcommand: ${subcommand}
- hooks validation failed (${failed} plugin${failed === 1 ? ''
- run directory escapes the authorized runs root
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/318ef36529a0b8e9.
Report an issue: GitHub.