From 5c4adb66089dbeac0e44cabb79a3706709a052c5 Mon Sep 17 00:00:00 2001 From: protosphinx <133899485+protosphinx@users.noreply.github.com> Date: Sun, 17 May 2026 16:06:44 +0000 Subject: [PATCH] feat(checks): add empty-body rule Warns when a skill file has valid frontmatter but no body content. A body-less skill gives Claude nothing to act on at runtime, making the skill silently useless. The check runs after schema validation so it only fires on otherwise well-formed skills. Also registers the rule in the SARIF rule catalog so GitHub Code Scanning shows a stable description and severity for this rule ID. --- src/checks.ts | 15 +++++++++++++++ src/sarif.ts | 8 ++++++++ test/checks.test.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/checks.ts b/src/checks.ts index ff5c384..a516893 100644 --- a/src/checks.ts +++ b/src/checks.ts @@ -32,6 +32,7 @@ export function runChecks( diagnostics.push(...checkToolsOverloaded(v)); diagnostics.push(...checkDescriptionLength(v)); diagnostics.push(...checkNameDrift(v)); + diagnostics.push(...checkEmptyBody(v)); } diagnostics.push(...checkCollisions(validated)); @@ -144,6 +145,20 @@ function checkNameDrift(v: ValidatedSkill): Diagnostic[] { return []; } +function checkEmptyBody(v: ValidatedSkill): Diagnostic[] { + if (v.body.trim().length === 0) { + return [ + { + severity: "warn", + rule: "empty-body", + message: "skill body is empty; add instructions for Claude to follow", + file: v.file, + }, + ]; + } + return []; +} + function tokenize(s: string): Set { return new Set( s diff --git a/src/sarif.ts b/src/sarif.ts index 328c4d6..8ebc224 100644 --- a/src/sarif.ts +++ b/src/sarif.ts @@ -91,6 +91,14 @@ export const RULES: readonly SarifRule[] = [ helpUri: HELP_BASE, defaultLevel: "warning", }, + { + id: "empty-body", + name: "emptyBody", + shortDescription: + "Skill body is empty; the file has frontmatter but no instructions for Claude.", + helpUri: HELP_BASE, + defaultLevel: "warning", + }, ]; const SEVERITY_TO_LEVEL: Record = { diff --git a/test/checks.test.ts b/test/checks.test.ts index 20047aa..8c0258b 100644 --- a/test/checks.test.ts +++ b/test/checks.test.ts @@ -201,4 +201,37 @@ describe("runChecks", () => { const d = ds.find((d) => d.rule === "tools-overloaded"); expect(d?.message).toContain("11"); }); + + it("warns when skill body is empty", () => { + const s = mkSkill("/test/foo/foo.md", { + name: "foo", + description: "do the foo thing", + }, ""); + const ds = runChecks([s], config); + expect(ds.some((d) => d.rule === "empty-body")).toBe(true); + }); + + it("warns when skill body is whitespace only", () => { + const s = mkSkill("/test/foo/foo.md", { + name: "foo", + description: "do the foo thing", + }, " \n\n "); + const ds = runChecks([s], config); + expect(ds.some((d) => d.rule === "empty-body")).toBe(true); + }); + + it("does not warn on empty-body when body has content", () => { + const s = mkSkill("/test/foo/foo.md", { + name: "foo", + description: "do the foo thing", + }, "Use Read to read a file, then summarize it."); + const ds = runChecks([s], config); + expect(ds.find((d) => d.rule === "empty-body")).toBeUndefined(); + }); + + it("does not fire empty-body when frontmatter is invalid", () => { + const s = mkSkill("/test/foo/foo.md", { name: "foo" }, ""); + const ds = runChecks([s], config); + expect(ds.find((d) => d.rule === "empty-body")).toBeUndefined(); + }); });