{"record":{"id":"4468fe477a8100e2","repo":"n8n-io/n8n","slug":"invalid-skill-at-sourcedirectory-errors-join","errorCode":null,"errorMessage":"Invalid skill at ${sourceDirectory}: ${errors.join('; ')}","messagePattern":"Invalid skill at (.+?): (.+?)","errorType":"exception","errorClass":"InvalidRuntimeSkillError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/skills/registry.ts","lineNumber":279,"sourceCode":"\tif (lstatSync(skillDir).isSymbolicLink()) {\n\t\terrors.push(`Skill folder must not be a symlink: ${sourceDirectory}`);\n\t}\n\n\tconst skillFileStat = lstatSync(skillFile);\n\tif (skillFileStat.isSymbolicLink()) {\n\t\terrors.push(`${RUNTIME_SKILL_FILE_NAME} must not be a symlink`);\n\t}\n\tif (!skillFileStat.isFile()) {\n\t\terrors.push(`${RUNTIME_SKILL_FILE_NAME} must be a regular file`);\n\t}\n\n\tconst symlinks = collectSymlinks(skillDir);\n\tif (symlinks.length > 0) {\n\t\terrors.push(`Skill files must not include symlinks: ${symlinks.join(', ')}`);\n\t}\n\n\tif (errors.length > 0) {\n\t\tthrow new InvalidRuntimeSkillError(`Invalid skill at ${sourceDirectory}: ${errors.join('; ')}`);\n\t}\n}\n\nfunction collectSkillFiles(rootDir: string): string[] {\n\tconst out: string[] = [];\n\twalkSkillDirectories(rootDir, out);\n\treturn out.sort();\n}\n\nfunction walkSkillDirectories(dir: string, out: string[]) {\n\tfor (const entry of readdirSync(dir).sort()) {\n\t\tif (shouldIgnoreDirectory(entry)) continue;\n\n\t\tconst absolutePath = join(dir, entry);\n\t\tconst stat = lstatSync(absolutePath);\n\t\tif (!stat.isDirectory() || stat.isSymbolicLink()) continue;\n\n\t\tconst skillFile = join(absolutePath, RUNTIME_SKILL_FILE_NAME);","sourceCodeStart":261,"sourceCodeEnd":297,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/skills/registry.ts#L261-L297","documentation":"Thrown by validateRuntimeSkillFolder during directory-level structural checks before a skill is parsed. It collects filesystem errors and joins them: the skill folder is a symlink, SKILL.md is a symlink, SKILL.md is not a regular file, or any file under the skill folder is a symlink. These are hard rejections (not warnings) because symlinks break content hashing and can escape the skill root. Wrapped as InvalidRuntimeSkillError.","triggerScenarios":"`ln -s` linking a skill folder into the skills tree; a SKILL.md that is itself a symlink to a template; editor/IDE creating symlinks for assets; a deploy that symlinks shared resources (references/, scripts/) into a skill folder; a SKILL.md replaced by a pipe/socket/non-file.","commonSituations":"Monorepo symlinking a shared package's skills into the runtime skills dir; `pnpm`/`npm link` style dev setups that introduce symlinks; CI copying via `cp -a` preserving symlinks; a build tool generating symlinked assets.","solutions":["Replace the symlinked skill folder with real files (cp -rL to dereference), or move the actual folder into the skills tree.","If a single file inside the folder is a symlink, copy the target file in place.","Ensure SKILL.md is a regular file (not a symlink, not a directory/pipe).","Re-run; the message lists every offending path so you can resolve them one by one."],"exampleFix":"# before — symlinked skill\nln -s /shared/skills/billing /app/skills/billing\n\n# after — dereference into real files\ncp -rL /shared/skills/billing /app/skills/billing","handlingStrategy":"validation","validationCode":"import { lstatSync, readdirSync } from 'fs';\nimport { join } from 'path';\n\nfunction assertNoSymlinks(skillDir: string): void {\n  const check = (abs: string) => {\n    const st = lstatSync(abs);\n    if (st.isSymbolicLink()) throw new Error(`Refusing symlink in skill: ${abs}`);\n    if (st.isDirectory()) for (const e of readdirSync(abs)) check(join(abs, e));\n  };\n  check(skillDir);\n}\n\nassertNoSymlinks(skillDir); // before loadRuntimeSkillSourceFromDirectory","typeGuard":"import { lstatSync } from 'fs';\n\nfunction isRegularSkillFile(skillFile: string): boolean {\n  const st = lstatSync(skillFile);\n  return !st.isSymbolicLink() && st.isFile();\n}","tryCatchPattern":"try {\n  loadRuntimeSkillSourceFromDirectory(rootDir);\n} catch (err) {\n  if (err instanceof InvalidRuntimeSkillError && /must not be a symlink|regular file/i.test(err.message)) {\n    // replace symlinks with real files (cp -rL), then reload\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Never symlink skill folders into the skills tree; copy real files (cp -rL dereferences).","In your deploy/build step, resolve symlinks before staging skills.","Add a CI check that fails if any file under the skills root is a symlink (lstatSync().isSymbolicLink())."],"tags":["skills","filesystem","symlink","security","validation"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}