JuliusBrussee/caveman · error
SKILL.md contains NUL bytes
Error message
SKILL.md contains NUL bytes
What it means
During skill import, cavemannifyImportedSkill reads SKILL.md as raw bytes and rejects any file containing NUL (0x00) bytes. Markdown skills must be clean text; NUL bytes indicate binary corruption or a wrong file. The check runs before UTF-8 decoding, so the file is rejected early.
Solutions
- Re-encode the file as UTF-8 without BOM (e.g. iconv -f UTF-16 -t UTF-8 SKILL.md) and re-import.
- Re-download or re-copy the SKILL.md from its original source to fix corruption.
- Verify with `grep -c $'\0' SKILL.md` or `file SKILL.md` that the file is plain text before importing.
Example fix
// before (UTF-16 encoded file) $ cave skill import ./skill-dir Error: SKILL.md contains NUL bytes // after $ iconv -f UTF-16 -t UTF-8 SKILL.md > SKILL.md.utf8 && mv SKILL.md.utf8 SKILL.md $ cave skill import ./skill-dir
Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync } from 'node:fs';
const bytes = readFileSync('SKILL.md');
if (bytes.includes(0)) throw new Error('file contains NUL bytes; re-encode as UTF-8'); Type guard
const isCleanText = (b: Buffer) => b.length > 0 && !b.includes(0);
Prevention
- Always save SKILL.md as UTF-8 (never UTF-16) in your editor
- Check `file SKILL.md` shows ASCII/UTF-8 text before importing
- Re-download skill files rather than copying from archives you didn't create
When it happens
Trigger: Calling the skill import command with a SKILL.md whose bytes include 0x00 — e.g. a file produced by a binary write, a truncated download, or a UTF-16 encoded file (which interleaves NUL bytes).
Common situations: Importing a skill file saved by a text editor in UTF-16; copying a skill out of a zip/tar with corruption; accidentally pointing the importer at a binary file named SKILL.md.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- at least one report path and the canonical span fixture…
- at least one sink id is required
- base and head must be full Git object IDs
- cave_live_eval_sandbox_profile_invalid
- caveman agent: entry must export default agent()
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/46817eaed0ede2cb.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:1586
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;
for (const block of blocks) {
const clean = block.trim();
if (/^(?:---+|\*\*\*+|___+)$/.test(clean)) {
removedSeparators++;
continue;
}View on GitHub (pinned to 3ee70a1026)