moeru-ai/airi · error · Error
Extension packages cannot contain symbolic links
Error message
Extension packages cannot contain symbolic links: ${relativePath} What it means
Every entry is checked with lstat; if any entry (at any depth) is a symbolic link the walk aborts. Symbolic links are forbidden because they can point outside the package root, breaking the containment guarantee for path traversal, size accounting, and staged copying.
Solutions
- Replace symlinks with real copies of the target files/directories (cp -rL or rsync -L).
- Import a flattened/resolved copy of the folder rather than the original tree.
- Remove .bin or tool-generated symlink directories from the package.
- If the link target is needed, vendor the actual files into the extension folder.
Example fix
// before
await staged('my-ext/') // contains my-ext/assets -> ../shared/assets (symlink) -> throws
// after: dereference links into real files
// cp -rL my-ext my-ext-flat
await staged('my-ext-flat/') Defensive patterns
Strategy: validation
Validate before calling
import { readdir, lstat } from 'node:fs/promises'
import { join } from 'node:path'
async function hasSymlinks(dir: string): Promise<boolean> {
for (const entry of await readdir(dir, { withFileTypes: true })) {
if (entry.isSymbolicLink()) return true
if (entry.isDirectory() && await hasSymlinks(join(dir, entry.name))) return true
}
return false
} Try / catch
try {
await staged(folder)
} catch (error) {
if (error instanceof Error && error.message.includes('symbolic links')) {
showUserError(`Replace symlink with a real copy: ${error.message}`)
return
}
throw error
} Prevention
- Package extensions with dereferenced copies (`cp -rL`, `rsync -L`) so no links ship.
- Add a packaging step that fails CI if the bundle contains symlinks.
- Exclude tool-generated symlink directories like node_modules/.bin from packages.
When it happens
Trigger: Calling inspectExtensionDirectory on a folder that contains any symlink — internal ones (entry -> ../shared) or absolute ones (entry -> /etc) — discovered during the recursive walk.
Common situations: macOS/Linux developers committing symlinks into the package (shared assets, node_modules .bin links); unpacking an archive containing symlinks; build tools creating .bin symlink directories inside the output.
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 resolves outside the package folder
- Extension packages can contain only files and directories
- Extension source must be a regular directory
- must be a regular file
- Extension entrypoint escapes the package folder
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/354a3aa4a2e7cd2a.
Report an issue: GitHub.
Appendix: source
Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:183
let entryCount = 0
let totalBytes = 0
while (pendingDirectories.length > 0) {
const directory = pendingDirectories.pop()
if (!directory) {
continue
}
const entries = await opendir(directory)
for await (const entry of entries) {
entryCount += 1
if (entryCount > extensionPackageLimits.entries) {
throw new Error(`Extension package exceeds the ${extensionPackageLimits.entries} entry limit.`)
}
const path = join(directory, entry.name)
const stats = await lstat(path)
const relativePath = relative(sourceRealPath, path)
if (stats.isSymbolicLink()) {
throw new Error(`Extension packages cannot contain symbolic links: ${relativePath}`)
}
if (stats.isDirectory()) {
directories.push(relativePath)
pendingDirectories.push(path)
continue
}
if (!stats.isFile()) {
throw new Error(`Extension packages can contain only files and directories: ${relativePath}`)
}
totalBytes += stats.size
if (totalBytes > extensionPackageLimits.totalBytes) {
throw new Error('Extension package exceeds the 512 MiB size limit.')
}
files.push({ path, relativePath, size: stats.size })
}
}
directories.sort()View on GitHub (pinned to 438a067dde)