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 a valid example with builtin, GitHub, and inline sources. serializeMaleta writes the normalized form shown here:
{
"schemaVersion": 1,
"id": "shared-fixture",
"slug": "shared-fixture",
"name": "Shared fixture",
"description": "Builtin, GitHub, and inline 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."
},
{
"id": "inline:release-checklist",
"name": "release-checklist",
"source": {
"type": "inline",
"name": "release-checklist",
"files": [
{ "path": "SKILL.md", "content": "# Release checklist\n\nRun tests before shipping.\n" }
]
}
}
],
"plugins": ["frontend-design@claude-plugins-official"],
"targetTool": "claude",
"targetOs": "unix"
}#Root fields
schemaVersionLiteral
1. Any other value is rejected withUnsupported Maleta format— 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. A skill'snamemust match[A-Za-z0-9._-]+and must not be.,.., or contain..— for the string shorthand too. Two skills that would land in the same destination directory are rejected withSkill destination collision '<name>' between '<id>' and '<id>'.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>, inline:<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 producesSource for skill "<x>" does not match its name. It is also the destination directory name and must match[A-Za-z0-9._-]+, must not be.or.., and must not contain...
#{ "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.
#{ "type": "inline", "name": "…", "files": […] }
typeA skill whose file content travels inside
maleta.json. The CLI writes the content as opaque UTF-8 data and never executes it.nameIt must be identical to the skill's
name; a mismatch producesSource for skill "<name>" does not match its name. The source identity isinline:<name>.filesContains 1 to 200 entries. Paths are safe relative POSIX paths of at most 240 characters: no leading slash, backslash, empty segment,
.or..segment, and only[A-Za-z0-9._-]in each segment. Paths must be unique case-insensitively, and an exact rootSKILL.mdis required. Normalization sorts entries deterministically bypath.files[].contentPreserved without trimming. Tabs, line feeds, and carriage returns are allowed; other ASCII control characters and DEL are rejected.
#Normalization
- Skills are sorted by
idand plugins alphabetically; duplicateidvalues and plugin identifiers are discarded. - Text fields except inline file content are passed through
trim; fields containing forbidden control characters are rejected.descriptionand inline content may contain 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,
Maleta file is too large. 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 |
|---|---|
Invalid Maleta JSON | The file content is not parseable JSON. |
Maleta file is too large | The input string exceeds 1 MiB in UTF-8 bytes. |
Unsupported Maleta format | The object is not a record or schemaVersion is not 1. There is no migration between versions. |
Invalid Maleta format: selection fields must be at the top level | A selection key exists at the document root. |
Maleta must expose skills, plugins, targetTool, and targetOs at the top level | At least one of slug, skills, plugins, targetTool, and targetOs is missing from the root. |
Invalid skill path | source.path violates the safe relative path rules. |
Invalid skill reference | ref contains a forbidden character or exceeds 160 characters. |
Invalid GitHub repository | repo does not normalize to owner/name. |
Source for skill "<name>" does not match its name | The name of a builtin or inline source does not match the skill's name. |
Invalid source for skill "<name>" | The source is not an object or its type is not builtin, github, or inline. |
Invalid source name for "<name>" | An inline or builtin source.name is missing, empty, over 120 characters, or contains a control character. |
Invalid files for skill "<name>" | source.files is not an array or contains fewer than 1 or more than 200 entries. |
Invalid file for skill "<name>" | An item in source.files is not an object. |
Invalid file path for skill "<name>" | An inline file path violates the safe relative POSIX path rules or exceeds 240 characters. |
Invalid file content for skill "<name>" | An inline file's content is not a string or contains a forbidden control character. |
Duplicate file path '<path>' in skill "<name>" | Two inline paths are equal case-insensitively. |
Skill "<name>" is missing a root SKILL.md file | No inline file has the exact root path SKILL.md. |
Invalid target tool | The targetTool value is outside the four-value enum. |
Invalid operating system | The targetOs value is neither windows nor unix. |
Invalid Maleta slug | The slug field does not match hyphen-separated letters and numbers, or exceeds 96 characters. |
Invalid skill name '<x>' | A skill name (or source.name) fails [A-Za-z0-9._-]+ or is ./../contains ... The string shorthand is validated the same way. |
Invalid skill name '<x>' for Windows | Under targetOs: "windows", the name ends with a dot or space, or is a DOS device name such as CON or NUL. |
Skill destination collision '<name>' between '<id>' and '<id>' | Two skills would be written to the same destination directory. The comparison is case-insensitive on Windows. |
file is UTF-16; save it as UTF-8 | The --file bytes carry a UTF-16 BOM or a UTF-16 NUL pattern. A UTF-8 BOM is stripped and accepted. |