Valibot in hr-skills
This skill covers how Valibot is used within the hr-skills monorepo specifically. It is not a general Valibot reference — see valibot.dev for the full API. The focus here is on the patterns, conventions, and pitfalls that appear in this codebase.
Where Valibot is used
Two files define schemas; all other packages consume them:
packages/hr-skills-ref/src/client/schema.ts—SkillPropertiesSchema, the generic Agent Skills shape used byreadProperties()inloader.tspackages/hr-skills-build/src/client/shared/schema.ts—MarketplaceJsonSchema(validates.claude-plugin/marketplace.json),SkillFrontmatterSchema(used byparseSkillFrontmatter()inparser.ts), andRegistrySchema(used byvalidateRegistryConsistency()invalidate-registry.tsto checkregistry/skills.jsonagainst its schema)
Import pattern used throughout the repo:
import * as v from 'valibot';
Never use named imports (import { object, string } from 'valibot') — the
wildcard * as v is the established convention here.
Supported tasks
- Write or extend a schema in
schema.tsfor a new frontmatter field - Add a
v.pipe(v.string(), v.trim())field to an existing object schema - Handle a
v.safeParse()result correctly inparser.tsorloader.ts - Infer the output type from a schema with
v.InferOutput<typeof Schema> - Add an optional field to
SkillPropertiesSchemaorSkillFrontmatterSchema - Validate marketplace.json shape against
MarketplaceJsonSchemain sync.ts - Add a
v.record()field for arbitrary string metadata - Debug a failed
v.safeParse()result usingv.summarize(result.issues) - Make a field optional without breaking the existing type contract
- Understand why
v.safeParseis used instead ofv.parsein this repo
Key prompts
Schema authoring
- "Add an optional [field] field to SkillPropertiesSchema that accepts a trimmed string."
- "Write the Valibot schema for a new marketplace plugin entry with fields [list]."
- "I need to validate a [shape] object in hr-skills-build. Write the schema."
- "Add a required string field [name] to SkillFrontmatterSchema with trim applied."
Parsing and results
- "Show me how parseSkillFrontmatter handles a v.safeParse failure in this repo."
- "When should I use v.safeParse vs v.parse in hr-skills-build?"
- "How does loader.ts throw a ValidationError from a failed v.safeParse result?"
- "Show me the correct way to check result.success and access result.output."
Type inference
- "How do I infer the output type from SkillPropertiesSchema?"
- "What is the difference between v.InferOutput and v.InferInput in this repo?"
- "How do I export both the schema and its type from schema.ts following repo conventions?"
- "Show me how SkillFrontmatter is inferred and used in parser.ts."
Optional and record fields
- "How is v.optional used in this repo for nullable frontmatter fields?"
- "Show me how v.record is used for the metadata field in SkillPropertiesSchema."
- "Add an optional field to SkillFrontmatterSchema that defaults to undefined."
- "How does toStringRecord convert unknown metadata values before passing to v.safeParse?"
Valibot vs Zod — critical differences
- "I accidentally wrote Zod-style chaining. Show me the correct Valibot equivalent."
- "How do I use v.pipe with v.trim and v.minLength like the existing schemas do?"
- "What is the Valibot equivalent of Zod's z.string().optional()?"
- "Show me how v.summarize works on result.issues for error reporting."
Tips
- Prefer
v.safeParsefor anything derived from user-authored content (parser.ts'sparseSkillFrontmatter,loader.ts) —parser.tsreturns an empty object on failure, andloader.tsthrows a typedValidationErrorusingv.summarize(result.issues). Throwing directly fromv.parsebypasses that error handling pattern. One deliberate exception:build/sync.ts#syncMarketplace()usesv.parsedirectly againstMarketplaceJsonSchema, since.claude-plugin/marketplace.jsonis a repo-committed file expected to always be valid — an unhandled throw there is the intended fail-fast behavior, not a bug to fix. - Apply
v.pipe(v.string(), v.trim())to every string field that comes from user-authored YAML — frontmatter values frequently have trailing whitespace or newlines that would silently fail downstream string comparisons. - Use
v.optional()wrapping, not chained methods —v.optional(v.string())notv.string().optional(). Valibot does not chain; it composes. - Infer types with
v.InferOutput<typeof Schema>and export the type alongside the schema in the same file. See howSkillPropertiesandSkillFrontmatterare exported in their respectiveschema.tsfiles as the pattern to follow. - Never add a new schema file — both packages already have one
schema.tseach. Add fields to the existing schemas unless there is a genuinely separate data shape that doesn't belong in either existing file. - When a
v.safeParseresult fails, logv.summarize(result.issues)for a human-readable summary — that is whatValidationErrorinloader.tsuses and what@clack/promptssurfaces to the terminal.
Common mistakes
- Using Zod-style chaining such as
v.string().trim().email()— Valibot has no method chaining. Usev.pipe(v.string(), v.trim(), v.email())instead. - Calling
v.parse(Schema, data)on user-authored content (SKILL.md frontmatter, etc.) instead ofv.safeParse— this throws a rawValiErrorthat bypasses the typed error hierarchy inerrors.ts. Usev.safeParseand handleresult.successexplicitly. (sync.ts'sv.parseagainst a repo-committed marketplace.json is the one intentional exception — see Tips.) - Forgetting
v.trim()in the pipeline for string fields parsed from YAML — without it, fields with trailing newlines or spaces will pass validation but cause subtle mismatch bugs in downstream string comparisons like author name normalization. - Adding
v.InferInputinstead ofv.InferOutputwhen the schema has no transforms — in this repo all schemas usev.trim()which counts as a transform, so input and output types differ slightly. PreferInferOutput.