{"record":{"id":"3d0e61ca572c7409","repo":"Yeachan-Heo/oh-my-codex","slug":"artifact-path-must-be-relative","errorCode":null,"errorMessage":"artifact path must be relative","messagePattern":"artifact path must be relative","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/mcp/hermes-bridge.ts","lineNumber":502,"sourceCode":"    const child = (deps.spawnProcess ?? spawn)(command, launchArgs, {\n      cwd,\n      detached: true,\n      stdio: \"ignore\",\n      env: { ...bridgeEnv, OMX_HERMES_MCP_BRIDGE: \"1\" },\n    }) as ChildProcess;\n    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);","sourceCodeStart":484,"sourceCodeEnd":520,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/mcp/hermes-bridge.ts#L484-L520","documentation":"Thrown by normalizeArtifactRelativePath in the Hermes MCP bridge when an artifact path argument is an absolute path (starts with / or a Windows drive). The bridge only serves artifacts via relative paths so it can confine reads to the session working directory and its whitelisted artifact prefixes.","triggerScenarios":"Calling an artifact-related MCP tool with path=\"/tmp/report.md\" or \"C:\\\\logs\\\\x.txt\" instead of \"reports/report.md\". isAbsolute(raw) is true on the normalized string input.","commonSituations":"Clients forwarding OS-native absolute paths from a local script; tools that resolve a path with path.resolve before sending; Windows users pasting backslash paths.","solutions":["Pass a relative path like \"artifacts/build.log\" instead of an absolute one","Strip the working directory prefix before calling the tool (path.relative(cwd, p))","Ensure the relative path starts with one of the SAFE_ARTIFACT_PREFIXES directories"],"exampleFix":"// before\nawait tool({ path: resolve(cwd, \"artifacts/out.txt\") });\n// after\nawait tool({ path: \"artifacts/out.txt\" });","handlingStrategy":"validation","validationCode":"const isRel = (p: string) => !path.isAbsolute(p);","typeGuard":"function isRelativeArtifactPath(p: unknown): p is string { return typeof p === 'string' && !path.isAbsolute(p.replace(/\\\\/g, '/')); }","tryCatchPattern":null,"preventionTips":["Always build artifact paths with path.relative(cwd, absolutePath) before sending","Reject absolute paths in your client wrapper before invoking the tool"],"tags":["path-validation","mcp","artifact","security"],"backgroundTag":"absolute-path-rejected","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}