moeru-ai/airi · error

Imported Extension entrypoints must be relative paths

Error message

Imported Extension entrypoints must be relative paths: ${entrypoint}

What it means

When importing an unpacked Extension folder, AIRI inspects the manifest and validates every entrypoint listed in it. Entry points must be relative paths inside the package directory so they can be resolved and sandboxed against the imported folder's real path. This error is thrown by `inspectExtensionDirectory` when a manifest entrypoint is an absolute path (e.g. starts with `/` or a drive letter), which would bypass the package containment check.

Solutions

  1. Open the extension's manifest and rewrite each entrypoint as a path relative to the package root (e.g. `./index.js`, `src/main.js`).
  2. If a build tool generates the manifest, configure it to emit relative paths (or strip the package root prefix before writing entrypoints).
  3. Re-select the folder in AIRI after fixing the manifest so inspection re-runs.

Example fix

// before (manifest)
{ "entrypoints": { "main": "/Users/me/Downloads/my-ext/main.js" } }
// after
{ "entrypoints": { "main": "./main.js" } }
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, isAbsolute } from 'node:path'
function validateManifestEntrypoints(manifest, packageRoot) {
  for (const [name, entrypoint] of Object.entries(manifest.entrypoints ?? {})) {
    if (!entrypoint) continue
    if (isAbsolute(entrypoint))
      throw new Error(`entrypoint '${name}' must be relative: ${entrypoint}`)
    if (!resolve(packageRoot, entrypoint).startsWith(packageRoot))
      throw new Error(`entrypoint '${name}' escapes package root: ${entrypoint}`)
  }
}

Type guard

const isRelativeEntrypoint = (p) => typeof p === 'string' && p.length > 0 && !isAbsolute(p)

Try / catch

try {
  await importExtension(folder)
} catch (err) {
  if (String(err.message).includes('must be relative paths')) {
    showHint('Fix manifest entrypoints to be relative to the package root, then re-select the folder.')
  } else throw err
}

Prevention

When it happens

Trigger: Call `inspected`/`staged` on a directory whose manifest `entrypoints` values contain an absolute path (e.g. `"entrypoints": { "main": "/home/user/extension/main.js" }` or `"C:\\ext\\index.js"`). The manifest parses fine, but `isAbsolute(entrypoint)` is true for one of the values.

Common situations: Authors generating the manifest programmatically and accidentally writing an absolute filesystem path; hand-editing a manifest copied from another machine; build tooling resolving entrypoints to absolute paths before emitting the manifest; copying a manifest from a zip extraction log that recorded full paths.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17). Data as JSON: /api/errors/03b06d266dba79d9. Report an issue: GitHub.

Appendix: source

Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:239

  }
  catch (error) {
    if (error instanceof SyntaxError) {
      throw new Error(`Extension manifest is not valid JSON: ${error.message}`)
    }
    throw error
  }

  const parsedManifest = parseExtensionManifest(rawManifest)
  if (!parsedManifest.success) {
    throw new Error(`Extension manifest is invalid: ${formatManifestDiagnostics(parsedManifest.diagnostics)}`)
  }

  for (const entrypoint of Object.values(parsedManifest.manifest.entrypoints)) {
    if (!entrypoint) {
      continue
    }
    if (isAbsolute(entrypoint)) {
      throw new Error(`Imported Extension entrypoints must be relative paths: ${entrypoint}`)
    }
    const resolvedEntrypoint = resolve(sourceRealPath, entrypoint)
    if (!isContainedPath(sourceRealPath, resolvedEntrypoint)) {
      throw new Error(`Extension entrypoint escapes the package folder: ${entrypoint}`)
    }
    await assertRegularFile(resolvedEntrypoint, 'Extension entrypoint')
    const entrypointRealPath = await realpath(resolvedEntrypoint)
    if (!isContainedPath(sourceRealPath, entrypointRealPath)) {
      throw new Error(`Extension entrypoint resolves outside the package folder: ${entrypoint}`)
    }
  }

  const fingerprint = createHash('sha256')
  for (const directory of directories) {
    fingerprint.update(`directory\0${directory}\0`)
  }
  for (const file of files) {
    fingerprint.update(`file\0${file.relativePath}\0${file.size}\0`)

View on GitHub (pinned to 438a067dde)