JuliusBrussee/caveman · error
SKILL.md missing under ${resolved}
Error message
SKILL.md missing under ${resolved} What it means
Thrown by importedSkillSource() in the caveman CLI when the import source is a DIRECTORY that does not contain a readable SKILL.md file (hasFile check fails). The directory itself resolved fine, but the required skill manifest file is absent, so the import aborts with exit code 2.
Source
Thrown at packages/cli/src/index.ts:1570
stop_condition: { status: string; value?: string };
prompt: { bytes: number; byte_budget: number; status: string };
resources: { scripts: string[]; references: string[]; assets: string[]; status: string };
transformations: { exact_duplicate_blocks_removed: number; decorative_separators_removed: number };
conflicts: { status: "needs_review"; declared: string[] };
benchmarks: { status: "required"; fixtures: string[] };
evidence_status: "unevaluated";
publication: { status: "blocked"; blockers: string[] };
};
function importedSkillSource(input: string): { root: string; file: string } {
let resolved: string;
try { resolved = realpathSync(resolve(process.cwd(), expandTilde(input))); } catch {
throw new Error(`source does not exist: ${input}`);
}
const stat = lstatSync(resolved);
const file = stat.isDirectory() ? join(resolved, SKILL_MD) : resolved;
if (!stat.isDirectory() && basename(resolved) !== SKILL_MD) throw new Error("source file must be named SKILL.md");
if (!hasFile(file)) throw new Error(`SKILL.md missing under ${resolved}`);
return { root: stat.isDirectory() ? resolved : dirname(resolved), file };
}
function cavemannifyImportedSkill(bytes: Buffer): { body: string; removedDuplicates: number; removedSeparators: number } {
if (bytes.length === 0 || bytes.length > 256 * 1024) throw new Error("SKILL.md must be 1..262144 bytes");
if (bytes.includes(0)) throw new Error("SKILL.md contains NUL bytes");
const text = bytes.toString("utf8");
if (!Buffer.from(text, "utf8").equals(bytes)) throw new Error("SKILL.md must be valid UTF-8");
if (/-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{16,}/i.test(text)) {
throw new Error("SKILL.md contains credential-shaped material");
}
const split = splitSkillMarkdown(bytes);
if (!split) throw new Error("SKILL.md needs closed YAML frontmatter");
const blocks = split.bodyText.trim().split(/\r?\n(?:[ \t]*\r?\n)+/);
const seen = new Set<string>();
const kept: string[] = [];
let removedDuplicates = 0;
let removedSeparators = 0;View on GitHub (pinned to 5184b3d11a)
Solutions
- cd into the source and locate the folder that actually holds SKILL.md (find . -name SKILL.md), then import that directory
- Fix the filename to exactly SKILL.md if it differs in case or spelling
- Repair dangling symlinks and ensure the file is readable (chmod +r)
Example fix
# before caveman skills import ./cloned-repo # SKILL.md is at ./cloned-repo/skills/foo/SKILL.md # after caveman skills import ./cloned-repo/skills/foo
Defensive patterns
Strategy: validation
Validate before calling
import { existsSync, statSync } from "node:fs";
import { join } from "node:path";
const hasSkillMd = (dir: string) => {
const f = join(dir, "SKILL.md");
return existsSync(f) && statSync(f).isFile();
}; Try / catch
try {
execFileSync("caveman", ["skills", "import", src]);
} catch (err) {
if (/SKILL\.md missing under/.test(err.stderr?.toString() ?? "")) {
// find the real skill dir: find src -name SKILL.md, then import that dir
}
} Prevention
- Import the deepest directory that directly contains SKILL.md, not a repo root
- Verify with find . -name SKILL.md before importing unfamiliar repos
- Check filename case and file readability on the source tree
When it happens
Trigger: `caveman skills import ./empty-dir`, a directory whose skill file is named skill.md/SKILL.MD instead of SKILL.md, a SKILL.md that is a dangling symlink, or one with no read permission (stat fails).
Common situations: Importing a cloned repo's root when the skill lives in a subdirectory (e.g. must import repo/skills/foo, not repo); case-mismatched filenames carried from case-insensitive filesystems; a partially synced/downloaded skill folder missing its main file.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- source does not exist: ${input}
- source file must be named SKILL.md
- SKILL.md must be 1..262144 bytes
- SKILL.md contains NUL bytes
- SKILL.md must be valid UTF-8
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18).
Data as JSON: /api/errors/ca93052b4022a3e8.
Report an issue: GitHub.