moeru-ai/airi · error
Extension entrypoint resolves outside the package folder
Error message
Extension entrypoint resolves outside the package folder: ${entrypoint} What it means
After the entrypoint path passes the lexical containment check, AIRI resolves the file's `realpath` (following symlinks) and verifies it again stays within the package folder. This error is thrown when the entrypoint file is a symlink (or hardlink chain) pointing outside the imported directory — a defense against symlink-based escapes from the package sandbox.
Solutions
- Replace the symlinked entrypoint with a real copy of the target file inside the package folder.
- Re-pack the extension ensuring no symlink entries escape the root (dereference symlinks when zipping, e.g. `zip -r --symlinks` alternatives or copy with `-L`).
- If the symlink target is legitimate, move that dependency into the package and update the manifest entrypoint.
Example fix
// before: package/main.js -> /home/user/lib/main.js (symlink) // after cp -L /home/user/lib/main.js package/main.js # real file inside the package
Defensive patterns
Strategy: validation
Validate before calling
import { realpath } from 'node:fs/promises'
async function assertNoSymlinkEscape(root, entrypoint) {
const real = await realpath(resolve(root, entrypoint))
if (real !== root && !real.startsWith(root + '/'))
throw new Error(`entrypoint symlink escapes package: ${entrypoint} -> ${real}`)
} Type guard
import { lstat } from 'node:fs/promises'
const isRealFileNotSymlink = async (p) => (await lstat(p)).isFile() && !(await lstat(p)).isSymbolicLink() Try / catch
try {
await importExtension(folder)
} catch (err) {
if (String(err.message).includes('resolves outside the package folder')) {
showHint('The entrypoint is a symlink pointing outside the package. Ship real files instead.')
} else throw err
} Prevention
- Do not symlink source files into the extension folder you plan to import; copy them instead.
- Repackage archives with symlinks dereferenced so no link targets escape the root.
- Run `find package -type l` before distribution to detect stray symlinks.
When it happens
Trigger: `inspectExtensionDirectory` calls `realpath(resolvedEntrypoint)` and the resulting real path is not contained by `sourceRealPath`, e.g. the entrypoint is `./main.js` but `main.js` is a symlink to `/home/user/lib/main.js` outside the package.
Common situations: Developers symlinking source files during development into the extension folder and then importing the folder as-is; package managers creating symlinks (e.g. pnpm/node_modules links) inside the extension; zips extracted with symlink entries preserved.
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
- Extension entrypoint escapes the package folder
- Extension packages cannot contain symbolic links
- Extension packages can contain only files and directories
- Extension source must be a regular directory
- must be a regular file
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/a495ff0a4565e155.
Report an issue: GitHub.
Appendix: source
Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:248
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`)
if (file.relativePath === manifestRelativePath) {
fingerprint.update(manifestContents)
}
else {
let bytesRead = 0
for await (const chunk of createReadStream(file.path)) {
const contents = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
bytesRead += contents.byteLength
if (bytesRead > file.size) {View on GitHub (pinned to 438a067dde)