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

artifact path must be relative

Error message

artifact path must be relative

What it means

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.

Source

Thrown at src/mcp/hermes-bridge.ts:502

    const child = (deps.spawnProcess ?? spawn)(command, launchArgs, {
      cwd,
      detached: true,
      stdio: "ignore",
      env: { ...bridgeEnv, OMX_HERMES_MCP_BRIDGE: "1" },
    }) as ChildProcess;
    child.unref();
    if (!child.pid) return failure("command_failed", "OMX session launcher did not report a pid");
    return jsonResult({ pid: child.pid, command, args: launchArgs, workingDirectory: cwd });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    if (message.includes("allow_mutation")) return failure("mutation_not_allowed", message);
    return failure("invalid_input", message);
  }
}

function normalizeArtifactRelativePath(pathValue: unknown): string {
  const raw = normalizeString(pathValue, "path", { required: true })!;
  if (isAbsolute(raw)) throw new Error("artifact path must be relative");
  const normalized = raw.replace(/\\/g, "/").replace(/^\.\//, "");
  if (normalized.includes("../") || normalized === ".." || normalized.includes("\0")) {
    throw new Error("artifact path must not traverse directories");
  }
  if (!SAFE_ARTIFACT_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
    throw new Error(`artifact path must be under ${SAFE_ARTIFACT_PREFIXES.join(", ")}`);
  }
  return normalized;
}

function isInsideDirectory(parent: string, candidate: string): boolean {
  const rel = relative(parent, candidate);
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
}

async function resolveSafeArtifactPath(cwd: string, rel: string): Promise<string> {
  const cwdRealPath = await realpath(cwd);
  const full = resolve(cwd, rel);

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Pass a relative path like "artifacts/build.log" instead of an absolute one
  2. Strip the working directory prefix before calling the tool (path.relative(cwd, p))
  3. Ensure the relative path starts with one of the SAFE_ARTIFACT_PREFIXES directories

Example fix

// before
await tool({ path: resolve(cwd, "artifacts/out.txt") });
// after
await tool({ path: "artifacts/out.txt" });
Defensive patterns

Strategy: validation

Validate before calling

const isRel = (p: string) => !path.isAbsolute(p);

Type guard

function isRelativeArtifactPath(p: unknown): p is string { return typeof p === 'string' && !path.isAbsolute(p.replace(/\\/g, '/')); }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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