{"record":{"id":"62feb0e60d38dfbc","repo":"Yeachan-Heo/oh-my-codex","slug":"artifact-path-must-be-under-safe-artifact-prefix","errorCode":null,"errorMessage":"artifact path must be under ${SAFE_ARTIFACT_PREFIXES.join(\", \")}","messagePattern":"artifact path must be under (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/mcp/hermes-bridge.ts","lineNumber":508,"sourceCode":"    child.unref();\n    if (!child.pid) return failure(\"command_failed\", \"OMX session launcher did not report a pid\");\n    return jsonResult({ pid: child.pid, command, args: launchArgs, workingDirectory: cwd });\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    if (message.includes(\"allow_mutation\")) return failure(\"mutation_not_allowed\", message);\n    return failure(\"invalid_input\", message);\n  }\n}\n\nfunction normalizeArtifactRelativePath(pathValue: unknown): string {\n  const raw = normalizeString(pathValue, \"path\", { required: true })!;\n  if (isAbsolute(raw)) throw new Error(\"artifact path must be relative\");\n  const normalized = raw.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n  if (normalized.includes(\"../\") || normalized === \"..\" || normalized.includes(\"\\0\")) {\n    throw new Error(\"artifact path must not traverse directories\");\n  }\n  if (!SAFE_ARTIFACT_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {\n    throw new Error(`artifact path must be under ${SAFE_ARTIFACT_PREFIXES.join(\", \")}`);\n  }\n  return normalized;\n}\n\nfunction isInsideDirectory(parent: string, candidate: string): boolean {\n  const rel = relative(parent, candidate);\n  return rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}\n\nasync function resolveSafeArtifactPath(cwd: string, rel: string): Promise<string> {\n  const cwdRealPath = await realpath(cwd);\n  const full = resolve(cwd, rel);\n  const relativeToCwd = relative(resolve(cwd), full);\n  if (relativeToCwd.startsWith(\"..\") || isAbsolute(relativeToCwd)) {\n    throw new Error(\"artifact resolved outside working directory\");\n  }\n\n  let artifactRealPath: string;","sourceCodeStart":490,"sourceCodeEnd":526,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/mcp/hermes-bridge.ts#L490-L526","documentation":"Thrown when a (valid, relative, non-traversing) artifact path does not start with any of the SAFE_ARTIFACT_PREFIXES whitelisted directory prefixes. Only artifacts stored under these known roots can be resolved, keeping file access predictable.","triggerScenarios":"path=\"docs/readme.md\" when allowed prefixes are e.g. [\"artifacts/\", \"logs/\"]; path=\"./dist/bundle.js\" after the leading ./ is stripped fails the prefix check.","commonSituations":"Assuming any file in the working directory is fetchable; version changes that alter the allowed prefix list; artifacts written to a new directory not yet whitelisted.","solutions":["Move/link the artifact under one of the allowed prefixes (e.g. artifacts/)","Check SAFE_ARTIFACT_PREFIXES in src/mcp/hermes-bridge.ts and align your output directories","If a new prefix is legitimately needed, extend SAFE_ARTIFACT_PREFIXES and rebuild"],"exampleFix":"// before\nawait tool({ path: \"out/report.md\" });\n// after\nawait tool({ path: \"artifacts/report.md\" });","handlingStrategy":"validation","validationCode":"const SAFE = ['artifacts/', 'logs/']; // mirror SAFE_ARTIFACT_PREFIXES\nif (!SAFE.some(pre => p.startsWith(pre))) throw new RangeError(`path must start with ${SAFE.join(', ')}`);","typeGuard":"function isAllowedArtifactPath(p: string, prefixes: string[]): p is string { return prefixes.some(x => p.startsWith(x)); }","tryCatchPattern":null,"preventionTips":["Write build outputs directly under the allowed prefixes","Check the prefix list in src/mcp/hermes-bridge.ts when upgrading"],"tags":["whitelist","path-validation","mcp","artifact"],"backgroundTag":"path-prefix-not-allowed","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}