n8n-io/n8n · error · InvalidRuntimeSkillError

Invalid skill at

Error message

Invalid skill at ${sourceDirectory}: ${errors.join('; ')}

What it means

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.

Solutions

  1. Replace the symlinked skill folder with real files (cp -rL to dereference), or move the actual folder into the skills tree.
  2. If a single file inside the folder is a symlink, copy the target file in place.
  3. Ensure SKILL.md is a regular file (not a symlink, not a directory/pipe).
  4. Re-run; the message lists every offending path so you can resolve them one by one.

Example fix

# before — symlinked skill
ln -s /shared/skills/billing /app/skills/billing

# after — dereference into real files
cp -rL /shared/skills/billing /app/skills/billing
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync, readdirSync } from 'fs';
import { join } from 'path';

function assertNoSymlinks(skillDir: string): void {
  const check = (abs: string) => {
    const st = lstatSync(abs);
    if (st.isSymbolicLink()) throw new Error(`Refusing symlink in skill: ${abs}`);
    if (st.isDirectory()) for (const e of readdirSync(abs)) check(join(abs, e));
  };
  check(skillDir);
}

assertNoSymlinks(skillDir); // before loadRuntimeSkillSourceFromDirectory

Type guard

import { lstatSync } from 'fs';

function isRegularSkillFile(skillFile: string): boolean {
  const st = lstatSync(skillFile);
  return !st.isSymbolicLink() && st.isFile();
}

Try / catch

try {
  loadRuntimeSkillSourceFromDirectory(rootDir);
} catch (err) {
  if (err instanceof InvalidRuntimeSkillError && /must not be a symlink|regular file/i.test(err.message)) {
    // replace symlinks with real files (cp -rL), then reload
    throw err;
  }
  throw err;
}

Prevention

When it happens

Trigger: `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.

Common situations: 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.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/4468fe477a8100e2. Report an issue: GitHub.

Appendix: source

Thrown at packages/@n8n/agents/src/skills/registry.ts:279

	if (lstatSync(skillDir).isSymbolicLink()) {
		errors.push(`Skill folder must not be a symlink: ${sourceDirectory}`);
	}

	const skillFileStat = lstatSync(skillFile);
	if (skillFileStat.isSymbolicLink()) {
		errors.push(`${RUNTIME_SKILL_FILE_NAME} must not be a symlink`);
	}
	if (!skillFileStat.isFile()) {
		errors.push(`${RUNTIME_SKILL_FILE_NAME} must be a regular file`);
	}

	const symlinks = collectSymlinks(skillDir);
	if (symlinks.length > 0) {
		errors.push(`Skill files must not include symlinks: ${symlinks.join(', ')}`);
	}

	if (errors.length > 0) {
		throw new InvalidRuntimeSkillError(`Invalid skill at ${sourceDirectory}: ${errors.join('; ')}`);
	}
}

function collectSkillFiles(rootDir: string): string[] {
	const out: string[] = [];
	walkSkillDirectories(rootDir, out);
	return out.sort();
}

function walkSkillDirectories(dir: string, out: string[]) {
	for (const entry of readdirSync(dir).sort()) {
		if (shouldIgnoreDirectory(entry)) continue;

		const absolutePath = join(dir, entry);
		const stat = lstatSync(absolutePath);
		if (!stat.isDirectory() || stat.isSymbolicLink()) continue;

		const skillFile = join(absolutePath, RUNTIME_SKILL_FILE_NAME);

View on GitHub (pinned to 5ac6606e81)