{"record":{"id":"318ef36529a0b8e9","repo":"Yeachan-Heo/oh-my-codex","slug":"native-hook-claim-journal-path-is-outside-controll","errorCode":null,"errorMessage":"Native hook claim journal path is outside controlled root: ${path}","messagePattern":"Native hook claim journal path is outside controlled root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/native-hook-claim-journal.ts","lineNumber":41,"sourceCode":"\tcanonicalPath: string;\n\tclaimPath: string;\n\tbeforeHash: string;\n\tafterHash: string | null;\n}\n\nfunction digest(bytes: Buffer): string {\n\treturn createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n\nfunction isMissing(error: unknown): boolean {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error &&\n\t\t(error as { code?: unknown }).code === \"ENOENT\";\n}\n\nfunction assertControlledPath(root: string, path: string): void {\n\tconst rel = relative(root, path);\n\tif (isAbsolute(rel) || rel === \"..\" || rel.startsWith(`..${sep}`) || rel === \"\") {\n\t\tthrow new Error(`Native hook claim journal path is outside controlled root: ${path}`);\n\t}\n}\n\nfunction processIsAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn typeof error === \"object\" && error !== null && \"code\" in error &&\n\t\t\t(error as { code?: unknown }).code === \"EPERM\";\n\t}\n}\n\ntype NativeHookClaimJournalOpen = (\n\tpath: string,\n\tflags: \"r\",\n) => Promise<Pick<FileHandle, \"sync\" | \"close\">>;\n","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/native-hook-claim-journal.ts#L23-L59","documentation":"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.","triggerScenarios":"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.","commonSituations":"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 '..'.","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."],"exampleFix":"// before\njournalPath = resolve(process.cwd(), 'claims.json'); // root may be elsewhere\n// after\njournalPath = join(root, 'claims.json');","handlingStrategy":"validation","validationCode":"import { isAbsolute, relative, sep } from 'node:path';\nfunction isControlledJournalPath(root: string, p: string): boolean {\n  const rel = relative(root, p);\n  return !isAbsolute(rel) && rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`);\n}","typeGuard":"function isSafeJournalPath(root: string, p: string): boolean { try { assertControlledPathCompat(root, p); return true; } catch { return false; } }","tryCatchPattern":"try { persistNativeHookClaimJournal(root, path); } catch (e) { if (/outside controlled root/.test(String(e))) { /* rebuild path with join(root, name) */ } throw e; }","preventionTips":["Always build journal paths with join(root, name)","Pass the same root object/variable to both arguments","Reject user-supplied '..' components in filenames"],"tags":["claim-journal","path-traversal","filesystem-security","hooks"],"backgroundTag":"path-escapes-root","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}