The maleta.json file follows a single, explicit schema, currently version 1. Selection fields live at the top level: skills, plugins, targetTool and targetOs do not live inside a nested object. Every field is read by parseMaleta in packages/core/src/maleta.ts.
#Complete schema 1
The document below is the canonical valid example, with the GitHub source expanded across multiple lines as produced by serializeMaleta:
{
"schemaVersion": 1,
"id": "shared-fixture",
"slug": "shared-fixture",
"name": "Shared fixture",
"description": "Builtin and GitHub selection used by site and CLI contract tests.",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"skills": [
{
"id": "builtin:design-taste-frontend",
"name": "design-taste-frontend",
"source": { "type": "builtin", "name": "design-taste-frontend" }
},
{
"id": "github:acme%2Fskills:skills%2Freviewer%2FSKILL.md@v1.2.0",
"name": "reviewer",
"source": { "type": "github", "repo": "acme/skills", "path": "skills/reviewer/SKILL.md", "ref": "v1.2.0" },
"description": "Review code before shipping."
}
],
"plugins": ["frontend-design@claude-plugins-official"],
"targetTool": "claude",
"targetOs": "unix"
}#Root fields
schemaVersionLiteral
1. Any other value is rejected withFormato de Maleta não suportado— there is no migration between versions.idMaleta identifier, up to 128 characters. It is not derived from
slugand does not need to be unique across files.slugUp to 96 characters, in the format
palavraorpalavra-palavra: Unicode letters and numbers only, with segments separated by a single hyphen.nameHuman-readable Maleta name, up to 80 characters. This is not the skill name: each skill has its own
name, which defines the destination directory.descriptionFree-form text up to 600 characters. It accepts line breaks; the other text fields do not.
createdAtA string accepted by
Date.parse, up to 64 characters. Usually ISO 8601.updatedAtThe same rule as
createdAt.maleta initwrites the same instant to both fields.skillsUp to 500 entries, with no duplicate
idvalues. Each entry is a{ id, name, source, description? }object or a string, which is expanded into a builtin source with the same name.pluginsUp to 200 identifiers, each up to 200 characters, with no duplicates. Use
[]for an empty list; the field is required in the input file.targetToolThe selection's default target tool. This value is used when
--toolis omitted at runtime.targetOsTarget operating system. It must match the host when running
installandsync.
#source: union discriminated by type
Each item in skills[] points to a source. The id field provided in the file is discarded and recalculated by skillId(source): builtin:<name> or github:<repo>:<path>[@<ref>], with percent-encoded components.
#{ "type": "builtin", "name": "…" }
typeA skill bundled with the local assets. This is the form used by a bare string in
skills[], which is expanded into a builtin source with the same text.nameIt must be identical to the skill's
name; any mismatch producesFonte da skill "<x>" inconsistente. It is also the destination directory name and must match[A-Za-z0-9._-]+.
#{ "type": "github", "repo": "…", "path": "…", "ref": "…" }
typeA skill resolved from a public repository at the declared revision.
repoAccepts
owner/nameor anhttps://github.com/owner/name[.git]URL, which is normalized toowner/name. The result must match[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+.pathRelative path inside the repository, up to 240 characters. Rejects
\, a leading slash,//, the segments.and.., empty segments, and any character outside[A-Za-z0-9._-]. A path ending inSKILL.mdis reduced to its parent directory.refBranch, tag, or SHA, up to 160 characters. Accepts
[A-Za-z0-9._/-]and rejects\,//, and... When omitted, the GitHub API resolves the repository's default branch.
#Normalization
- Skills are sorted by
idand plugins alphabetically; duplicateidvalues and plugin identifiers are discarded. - All text fields are passed through
trim; fields containing control characters are rejected. Onlydescriptionaccepts line breaks. - Unknown top-level keys are silently discarded; a root
selectionkey is an explicit error. - The file is read only as valid JSON up to 1 MiB in UTF-8 bytes. Above that limit,
Arquivo de Maleta muito grande. serializeMaletare-emits the document withJSON.stringify(doc, null, 2)and a final newline, soparseMaleta(serializeMaleta(m))preserves the fields and canonical order.
#Core validation messages
For maleta validate, the error is printed as [error] <caminho>: <mensagem> and the process exits with code 1. These are the possible messages:
| Message | Cause |
|---|---|
JSON de Maleta inválido | The file content is not parseable JSON. |
Arquivo de Maleta muito grande | The input string exceeds 1 MiB in UTF-8 bytes. |
Formato de Maleta não suportado | The object is not a record or schemaVersion is not 1. There is no migration between versions. |
Formato de Maleta inválido: os campos da seleção devem ficar no nível principal | A selection key exists at the document root. |
Maleta precisa expor skills, plugins, targetTool e targetOs no nível principal | At least one of slug, skills, plugins, targetTool, and targetOs is missing from the root. |
Caminho da skill inválido | source.path violates the safe relative path rules. |
Referência da skill inválida | ref contains a forbidden character or exceeds 160 characters. |
Repositório GitHub inválido | repo does not normalize to owner/name. |
Fonte da skill "<x>" inconsistente | The name of a builtin source does not match the skill's name. |
Ferramenta de destino inválida | The targetTool value is outside the four-value enum. |
Sistema operacional inválido | The targetOs value is neither windows nor unix. |
Slug da Maleta inválido | The slug field does not match hyphen-separated letters and numbers, or exceeds 96 characters. |