{"record":{"id":"46817eaed0ede2cb","repo":"JuliusBrussee/caveman","slug":"skill-md-contains-nul-bytes","errorCode":null,"errorMessage":"SKILL.md contains NUL bytes","messagePattern":"SKILL\\.md contains NUL bytes","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":1576,"sourceCode":"\tevidence_status: \"unevaluated\";\n\tpublication: { status: \"blocked\"; blockers: string[] };\n};\n\nfunction importedSkillSource(input: string): { root: string; file: string } {\n\tlet resolved: string;\n\ttry { resolved = realpathSync(resolve(process.cwd(), expandTilde(input))); } catch {\n\t\tthrow new Error(`source does not exist: ${input}`);\n\t}\n\tconst stat = lstatSync(resolved);\n\tconst file = stat.isDirectory() ? join(resolved, SKILL_MD) : resolved;\n\tif (!stat.isDirectory() && basename(resolved) !== SKILL_MD) throw new Error(\"source file must be named SKILL.md\");\n\tif (!hasFile(file)) throw new Error(`SKILL.md missing under ${resolved}`);\n\treturn { root: stat.isDirectory() ? resolved : dirname(resolved), file };\n}\n\nfunction cavemannifyImportedSkill(bytes: Buffer): { body: string; removedDuplicates: number; removedSeparators: number } {\n\tif (bytes.length === 0 || bytes.length > 256 * 1024) throw new Error(\"SKILL.md must be 1..262144 bytes\");\n\tif (bytes.includes(0)) throw new Error(\"SKILL.md contains NUL bytes\");\n\tconst text = bytes.toString(\"utf8\");\n\tif (!Buffer.from(text, \"utf8\").equals(bytes)) throw new Error(\"SKILL.md must be valid UTF-8\");\n\tif (/-----BEGIN [A-Z ]*PRIVATE KEY-----|\\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{16,}/i.test(text)) {\n\t\tthrow new Error(\"SKILL.md contains credential-shaped material\");\n\t}\n\tconst split = splitSkillMarkdown(bytes);\n\tif (!split) throw new Error(\"SKILL.md needs closed YAML frontmatter\");\n\tconst blocks = split.bodyText.trim().split(/\\r?\\n(?:[ \\t]*\\r?\\n)+/);\n\tconst seen = new Set<string>();\n\tconst kept: string[] = [];\n\tlet removedDuplicates = 0;\n\tlet removedSeparators = 0;\n\tfor (const block of blocks) {\n\t\tconst clean = block.trim();\n\t\tif (/^(?:---+|\\*\\*\\*+|___+)$/.test(clean)) {\n\t\t\tremovedSeparators++;\n\t\t\tcontinue;\n\t\t}","sourceCodeStart":1558,"sourceCodeEnd":1594,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L1558-L1594","documentation":"Thrown by cavemannifyImportedSkill() in the caveman CLI during `caveman skills import`: the raw bytes of SKILL.md must not contain any NUL (0x00) byte. NUL bytes indicate binary content (or a binary file with a .md extension), which cannot be a valid skill document; the CLI exits with code 2 before any parsing.","triggerScenarios":"A SKILL.md that is actually a binary saved with the wrong extension; a file that got a NUL from a terminal copy-paste or a truncated UTF-16 export (UTF-16LE ASCII has NUL bytes interleaved); output of a tool that pads or corrupts writes.","commonSituations":"Windows editors saving UTF-16; a gzip/zip artifact renamed to SKILL.md; files transferred through a channel that introduces stray NULs; generated skills from a buggy script writing Buffer(0)-padded content.","solutions":["Detect: LC_ALL=C grep -q $'\\x00' SKILL.md && echo binary","Re-save the file as UTF-8 text (from UTF-16: iconv -f UTF-16 -t UTF-8)","If the file is truly binary, it is not a skill — restore the real markdown source"],"exampleFix":"# before: UTF-16LE file (NUL bytes)\nfile SKILL.md   # → UTF-16 Unicode text\n\n# after\niconv -f UTF-16LE -t UTF-8 SKILL.md > SKILL.utf8.md && mv SKILL.utf8.md SKILL.md\ncaveman skills import ./skills/foo","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\n\nconst bytes = readFileSync(file);\nif (bytes.includes(0)) throw new Error(\"SKILL.md contains NUL bytes — re-export as UTF-8 text\");","typeGuard":"const isNulFree = (bytes: Buffer) => !bytes.includes(0);","tryCatchPattern":"try {\n  execFileSync(\"caveman\", [\"skills\", \"import\", src]);\n} catch (err) {\n  if (/NUL bytes/.test(err.stderr?.toString() ?? \"\")) {\n    // re-encode the source (iconv -f UTF-16 -t UTF-8) and retry once\n  }\n}","preventionTips":["Save skill files as UTF-8 without BOM; avoid UTF-16 exports","Never rename binary artifacts to SKILL.md","Screen generated skills with a NUL-byte check before import"],"tags":["cli","validation","encoding","binary","skills-import","caveman"],"backgroundTag":"binary-content-rejected","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}