fix: add strict YAML validation for plugin frontmatter (#399)
This commit is contained in:
@@ -44,7 +44,7 @@ async function loadPersonalSkills(skillsDir: string): Promise<ClaudeSkill[]> {
|
||||
let data: Record<string, unknown> = {}
|
||||
try {
|
||||
const raw = await fs.readFile(skillPath, "utf8")
|
||||
data = parseFrontmatter(raw).data
|
||||
data = parseFrontmatter(raw, skillPath).data
|
||||
} catch {
|
||||
// Keep syncing the skill even if frontmatter is malformed.
|
||||
}
|
||||
@@ -87,7 +87,7 @@ async function loadPersonalCommands(commandsDir: string): Promise<ClaudeCommand[
|
||||
const commands: ClaudeCommand[] = []
|
||||
for (const file of files) {
|
||||
const raw = await fs.readFile(file, "utf8")
|
||||
const { data, body } = parseFrontmatter(raw)
|
||||
const { data, body } = parseFrontmatter(raw, file)
|
||||
commands.push({
|
||||
name: typeof data.name === "string" ? data.name : deriveCommandName(commandsDir, file),
|
||||
description: data.description as string | undefined,
|
||||
|
||||
@@ -60,7 +60,7 @@ async function loadAgents(agentsDirs: string[]): Promise<ClaudeAgent[]> {
|
||||
const agents: ClaudeAgent[] = []
|
||||
for (const file of files) {
|
||||
const raw = await readText(file)
|
||||
const { data, body } = parseFrontmatter(raw)
|
||||
const { data, body } = parseFrontmatter(raw, file)
|
||||
const name = (data.name as string) ?? path.basename(file, ".md")
|
||||
agents.push({
|
||||
name,
|
||||
@@ -80,7 +80,7 @@ async function loadCommands(commandsDirs: string[]): Promise<ClaudeCommand[]> {
|
||||
const commands: ClaudeCommand[] = []
|
||||
for (const file of files) {
|
||||
const raw = await readText(file)
|
||||
const { data, body } = parseFrontmatter(raw)
|
||||
const { data, body } = parseFrontmatter(raw, file)
|
||||
const name = (data.name as string) ?? path.basename(file, ".md")
|
||||
const allowedTools = parseAllowedTools(data["allowed-tools"])
|
||||
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
||||
@@ -104,7 +104,7 @@ async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
|
||||
const skills: ClaudeSkill[] = []
|
||||
for (const file of skillFiles) {
|
||||
const raw = await readText(file)
|
||||
const { data } = parseFrontmatter(raw)
|
||||
const { data } = parseFrontmatter(raw, file)
|
||||
const name = (data.name as string) ?? path.basename(path.dirname(file))
|
||||
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
||||
skills.push({
|
||||
|
||||
@@ -5,7 +5,7 @@ export type FrontmatterResult = {
|
||||
body: string
|
||||
}
|
||||
|
||||
export function parseFrontmatter(raw: string): FrontmatterResult {
|
||||
export function parseFrontmatter(raw: string, sourcePath?: string): FrontmatterResult {
|
||||
const lines = raw.split(/\r?\n/)
|
||||
if (lines.length === 0 || lines[0].trim() !== "---") {
|
||||
return { data: {}, body: raw }
|
||||
@@ -25,9 +25,15 @@ export function parseFrontmatter(raw: string): FrontmatterResult {
|
||||
|
||||
const yamlText = lines.slice(1, endIndex).join("\n")
|
||||
const body = lines.slice(endIndex + 1).join("\n")
|
||||
const parsed = load(yamlText)
|
||||
const data = (parsed && typeof parsed === "object") ? (parsed as Record<string, unknown>) : {}
|
||||
return { data, body }
|
||||
try {
|
||||
const parsed = load(yamlText)
|
||||
const data = (parsed && typeof parsed === "object") ? (parsed as Record<string, unknown>) : {}
|
||||
return { data, body }
|
||||
} catch (err) {
|
||||
const location = sourcePath ? ` in ${sourcePath}` : ""
|
||||
const hint = "Tip: quote frontmatter values containing colons (e.g. description: 'Use for X: Y')"
|
||||
throw new Error(`Invalid YAML frontmatter${location}: ${err instanceof Error ? err.message : err}\n${hint}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatFrontmatter(data: Record<string, unknown>, body: string): string {
|
||||
|
||||
Reference in New Issue
Block a user