stablyai/orca · error · Error
pet.json must not be a symlink.
Error message
pet.json must not be a symlink.
What it means
Thrown by the pet:importPetBundle handler when isSymlink(manifestPath) returns true — lstat detected that pet.json is a symbolic link rather than a regular file. The check is a security guard: a symlinked manifest could point anywhere on the filesystem (or to a network path), enabling a malicious bundle to substitute content after the validation passes or to reference files outside the bundle.
Source
Thrown at src/main/ipc/pet.ts:268
try {
const pickedStat = await stat(picked)
bundleDir = pickedStat.isDirectory() ? picked : dirname(picked)
} catch {
throw new Error('Could not read the selected path.')
}
const manifestPath = join(bundleDir, 'pet.json')
let manifestStat: Awaited<ReturnType<typeof stat>>
try {
manifestStat = await stat(manifestPath)
} catch {
throw new Error('Bundle is missing pet.json.')
}
if (!manifestStat.isFile() || manifestStat.size > MAX_MANIFEST_BYTES) {
throw new Error('pet.json is invalid.')
}
if (await isSymlink(manifestPath)) {
throw new Error('pet.json must not be a symlink.')
}
let manifest: ResolvedPetManifest<PetManifest>
try {
const raw = await readFile(manifestPath, 'utf8')
// Why: defend against TOCTOU — the file may have grown between the stat check and this read.
if (Buffer.byteLength(raw, 'utf8') > MAX_MANIFEST_BYTES) {
throw new Error('pet.json exceeded the manifest size limit.')
}
manifest = applyCodexPetDefaults(PetManifestSchema.parse(JSON.parse(raw)))
} catch (error) {
throw new Error(`Invalid pet.json: ${error instanceof Error ? error.message : 'parse error'}`)
}
// Why: spritesheetPath is bundle-relative and attacker-controlled — reject absolute/escaping paths (and symlinks) so a bundle can't reach outside.
const normalizedSpritePath = manifest.spritesheetPath.replace(/[\\/]+/g, sep)
if (
isAbsolute(manifest.spritesheetPath) ||View on GitHub (pinned to 1136503c6a)
Solutions
- Replace the symlink with a real, regular pet.json file (cp the target content into place, remove the symlink).
- Re-extract the bundle with an option that materializes symlinks as regular files.
- On macOS/Linux run 'ls -l pet.json' — a leading 'l' in the mode confirms a symlink.
- Never ship pet.json as a symlink in a redistributable bundle.
Defensive patterns
Strategy: validation
Validate before calling
import { lstat } from 'node:fs/promises'
import { join } from 'node:path'
async function manifestIsSymlink(bundleDir: string): Promise<boolean> {
try {
return (await lstat(join(bundleDir, 'pet.json'))).isSymbolicLink()
} catch {
return false
}
}
if (await manifestIsSymlink(bundleDir)) {
notify('pet.json must be a real file, not a symlink.')
return
} Try / catch
try {
await ipcRenderer.invoke('pet:importPetBundle')
} catch (e) {
if (e instanceof Error && e.message === 'pet.json must not be a symlink.') {
notify('Replace the pet.json symlink with a real file and re-import.')
} else throw e
} Prevention
- Ship pet.json as a regular file in redistributable bundles.
- Extract bundles with an option that materializes symlinks as files.
- On macOS/Linux, check `ls -l pet.json` for a leading 'l'.
When it happens
Trigger: pet.json is a symlink (created via ln -s on macOS/Linux or mklink on Windows) pointing to another location, or a bundle-extraction tool preserved symlinks instead of materializing the target file.
Common situations: User authored the bundle using symlinks for convenience, an archive extractor preserved symlinks, or a malicious bundle intentionally symlinked pet.json to an external target.
Related errors
- Bundle is missing pet.json.
- pet.json is invalid.
- pet.json exceeded the manifest size limit.
- Path escaped upload root during staging: '${displayPath}'
- Symlink not allowed in '${entry.name}'
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/1812faac9359c886.
Report an issue: GitHub.