Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Exit codes:
| `description-collision` | warn | Two skills' descriptions have Jaccard ≥ 0.6 |
| `tools-overloaded` | warn | `tools:` lists 10 or more entries; narrow the list to what this skill actually needs |
| `duplicate-name` | warn | Two or more skills share the same `name:` value; resolution is ambiguous |
| `empty-body` | warn | Skill body has no instructions; Claude has nothing to follow |
| `model-unknown` | warn | `model:` value is not a recognized Claude model ID; likely a typo |
| `parse` | error | The file doesn't have valid frontmatter / YAML |

The MCP and built-in tool checks read `~/.claude/settings.json` and
Expand Down
32 changes: 32 additions & 0 deletions src/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ const MAX_DESCRIPTION_CHARS = 500;
const COLLISION_THRESHOLD = 0.6;
const MAX_TOOLS_COUNT = 10;

// Known Claude model IDs. Severity is warn (not error) because the list
// evolves with each Anthropic release; update this set when new models ship.
const KNOWN_CLAUDE_MODELS: ReadonlySet<string> = new Set([
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept documented model aliases and inherit

This whitelist makes valid skill frontmatter produce model-unknown warnings: I checked the Claude Code skills frontmatter docs, which say model accepts the same values as /model, or inherit, and the CLI reference documents /model aliases such as sonnet and opus. A skill using model: inherit or model: sonnet will now be reported as a typo, and in --strict CI that false positive becomes a failing run, so the accepted set should include the documented non-ID values (or delegate validation to Claude Code's model parser).

Useful? React with 👍 / 👎.

"claude-3-haiku-20240307",
"claude-3-opus-20240229",
"claude-3-opus-latest",
"claude-3-sonnet-20240229",
"claude-3-5-haiku-20241022",
"claude-3-5-haiku-latest",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
"claude-haiku-4-5-20251001",
"claude-opus-4-8",
"claude-sonnet-4-6",
]);

export function runChecks(
parsed: ParsedSkill[],
config: SkillcheckConfig,
Expand All @@ -33,6 +49,7 @@ export function runChecks(
diagnostics.push(...checkDescriptionLength(v));
diagnostics.push(...checkNameDrift(v));
diagnostics.push(...checkEmptyBody(v));
diagnostics.push(...checkModelUnknown(v));
}

diagnostics.push(...checkCollisions(validated));
Expand Down Expand Up @@ -154,6 +171,21 @@ function checkNameDrift(v: ValidatedSkill): Diagnostic[] {
];
}

function checkModelUnknown(v: ValidatedSkill): Diagnostic[] {
const model = v.frontmatter.model;
if (model === undefined) return [];
if (typeof model !== "string" || model.length === 0) return [];
if (KNOWN_CLAUDE_MODELS.has(model)) return [];
return [
{
severity: "warn",
rule: "model-unknown",
message: `model '${model}' is not a recognized Claude model; check for typos or update skillcheck`,
file: v.file,
},
];
}

function checkEmptyBody(v: ValidatedSkill): Diagnostic[] {
if (v.body.trim().length === 0) {
return [
Expand Down
50 changes: 50 additions & 0 deletions test/checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,54 @@ describe("runChecks", () => {
const ds = runChecks([s], config);
expect(ds.some((d) => d.rule === "name-drift")).toBe(true);
});

it("warns on an unrecognized model name", () => {
const s = mkSkill("/test/foo/foo.md", {
name: "foo",
description: "do the foo thing",
model: "claude-sonet-4-6",
});
const ds = runChecks([s], config);
expect(ds.some((d) => d.rule === "model-unknown")).toBe(true);
});

it("does not warn on a recognized model", () => {
const s = mkSkill("/test/foo/foo.md", {
name: "foo",
description: "do the foo thing",
model: "claude-sonnet-4-6",
});
const ds = runChecks([s], config);
expect(ds.find((d) => d.rule === "model-unknown")).toBeUndefined();
});

it("does not warn when model field is absent", () => {
const s = mkSkill("/test/foo/foo.md", {
name: "foo",
description: "do the foo thing",
});
const ds = runChecks([s], config);
expect(ds.find((d) => d.rule === "model-unknown")).toBeUndefined();
});

it("model-unknown message includes the offending model name", () => {
const s = mkSkill("/test/foo/foo.md", {
name: "foo",
description: "do the foo thing",
model: "gpt-4o",
});
const ds = runChecks([s], config);
const d = ds.find((d) => d.rule === "model-unknown");
expect(d?.message).toContain("gpt-4o");
});

it("warns on model-unknown for each model in the known-3.x series", () => {
const s = mkSkill("/test/foo/foo.md", {
name: "foo",
description: "do the foo thing",
model: "claude-3-5-sonnet-20241022",
});
const ds = runChecks([s], config);
expect(ds.find((d) => d.rule === "model-unknown")).toBeUndefined();
});
});