kitbash / docs / format

Skill format (KSF)

A skill is a directory with a TOML manifest and a markdown body. This page documents every table, every field, and the constraints the compiler actually enforces.

KSF is specified in spec/SPEC.md with a machine-readable companion at spec/schema/skill.schema.json. The format is designed to outlive its reference implementation โ€” independent implementations are the point, so everything below is stated in terms of the spec first and the Kitbash CLI second.

Skill layout

A skill is a directory. Two files are required; the rest are optional.

<name>/
  skill.toml        # REQUIRED โ€” manifest
  SKILL.md          # REQUIRED โ€” instructions
  prompts/          # optional โ€” named prompt templates referenced from SKILL.md
  scripts/          # optional โ€” deterministic helpers (any executable)
  evals/            # optional, strongly encouraged โ€” *.eval.yaml
  fixtures/         # optional โ€” fixture material for evals

Directory names follow the same pattern as skill.name: [a-z][a-z0-9-]{1,40}.

Scripts are never executed at install time โ€” only when the agent invokes the skill, and only within the declared permissions. Gate-mode skills must route their verdict through a script exit code or a schema-validated artifact; a bare model opinion is not a gate verdict.

SKILL.md without a manifest. A folder containing only SKILL.md โ€” the convention used by Claude Skills and skills.sh โ€” still loads. Kitbash synthesizes a manifest from the YAML frontmatter (name, description) or the directory name, with permissive defaults: version 0.0.0, budget 6000, standing 250, lazy disclosure, and no permissions, artifacts, triggers, or dependencies. These skills are flagged as unmanifested at install and compile, and their budget and standing checks warn instead of failing โ€” they never declared those limits, so measuring them is informational.

The manifest

Required tables are [skill] and [context]. Every other table is optional and defaults to the most restrictive value: no permissions, no network, no writes, no artifacts, no lore access.

The schema and the CLI enforce different amounts. The CLI hard-fails a load on the value constraints for frozen fields โ€” name pattern, semver, description length, budget presence and integer range, standing integer range, a scalar given where an array is required, and a disclosure that is not lazy or eager. Unknown tables and the forward-compatible enum values (targets.requires, targets.mode, triggers.events, lore) produce lint warnings, not errors. Constraints below marked schema-only are enforced when you validate against the JSON Schema; the CLI does not reject them. See what gets enforced.

[skill]

Identity. Required.

FieldTypeRequiredDefaultConstraints
namestringyesโ€”^[a-z][a-z0-9-]{1,40}$ โ€” 2โ€“41 characters, lowercase, must start with a letter
versionstringyesโ€”^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$ โ€” semver with optional prerelease; build metadata is not accepted
descriptionstringyesโ€”10โ€“200 characters
licensestringnoโ€”free string; SPDX identifier by convention, not validated
authorsarray of stringsno[]schema-only; the reference CLI accepts and ignores it
homepagestringnoโ€”format: uri in the schema; not checked by the CLI

The name is the identity a skill carries everywhere โ€” the install path, the compiled filename on every adapter, the key other skills depend on. Version follows semver with a specific reading: instruction changes that alter behavior are minor at least, and permission or budget increases are major. Treat prompts like APIs.

[context]

Token accounting. Required.

FieldTypeRequiredDefaultConstraints
budgetintegeryesโ€”50โ€“20000; a fractional value is rejected
standingintegerno1000โ€“500; a fractional value is rejected
disclosurestringno"lazy"lazy | eager. Any other value is a hard error; omit the field to default to lazy

budget is the ceiling on the compiled output for the active state โ€” what the agent receives once the skill is invoked. Compilers must fail the build when the body exceeds it.

standing is the ceiling on what is present when the skill is not invoked: the trigger description, the frontmatter stub, the one line in an AGENTS.md index. This is the number that keeps thirty installed skills from silently eating the context window, and it is the field most authors under-think.

disclosure declares intent, not capability. Lazy targets load the body on invocation; eager targets carry it every session regardless. Declaring lazy does not make an eager adapter lazy โ€” it makes the degradation visible in the compile warning.

[triggers]

How the skill gets invoked. Optional.

FieldTypeRequiredDefaultConstraints
commandsarray of stringsno[]schema pattern ^/[a-z][a-z0-9-]*$; the CLI's lint fails any entry that does not start with /
autoarray of stringsno[]free text; natural-language hints, adapter-dependent
eventsarray of stringsno[]pre-push | pre-commit | ci | pr-open

commands are explicit invocations an adapter registers where it can. auto is a hint set โ€” some assistants match on it, some ignore it entirely, and nothing about it is guaranteed. events are hook points, meaningful for gate-mode skills; an adapter without hook support degrades visibly rather than pretending.

[permissions]

What the skill is allowed to do. Optional; omitting it means nothing is granted.

FieldTypeRequiredDefaultConstraints
toolsarray of stringsno[]free-form tool strings; the grammar is provisional
networkbooleannofalseโ€”
writebooleannofalsemay the skill direct file modifications

The table shape is stable; the tool-string grammar ("bash:git *") is provisional and advisory today. Adapters enforce permissions natively where the platform supports it and compile them into the instructions where it does not โ€” and an adapter must not claim enforcement it does not have. What the manifest reliably gives you is disclosure: this block is what kitbash install prints for your review before anything touches disk. See trust & review.

When a skill's permissions are non-default โ€” tools non-empty, or network or write set to true โ€” kitbash compile appends a short Declared permissions (kitbash) block to every target's compiled body, listing the permitted tools and whether network access and file writes are allowed. It is prose in the body, not native frontmatter, and no target enforces it โ€” but it travels with the generated file, so a teammate who pulls the compiled output without ever running install sees the same limits the installer reviewed.

[artifacts]

Structured JSON the skill exchanges with other skills. Optional.

FieldTypeRequiredDefaultConstraints
producesarray of stringsno[]each entry ^[a-z][a-z0-9-]*@\d+$ โ€” name@version, integer version
consumesarray of stringsno[]same shape; the skill must work without them

Refs are name@version with an integer version โ€” findings@1, not findings@1.0. Malformed refs are a lint failure. Artifacts live as JSON under .kitbash/artifacts/, named <type>.json or <type>/<id>.json, each with a versioned schema in spec/schema/artifacts/. v0.1 reserves plan@1, findings@1, verify@1, and benchmark@1. Consumed artifacts are inputs a skill may use if present, never inputs it may require.

[targets]

Capability requirements and execution mode. Optional.

FieldTypeRequiredDefaultConstraints
requiresarray of stringsno[]scripts | hooks | subagents | network; an unrecognized value warns (forward-compatible)
modestringno"skill"skill | gate. The CLI reads any value other than "gate" as "skill", and warns at test when the value is unrecognized

requires is the input to the compatibility matrix, which is derived and never hand-written: targets.requires crossed with each adapter's capability set yields full, degraded, or unsupported per assistant. A compiler missing a required capability must either emit a degraded variant with a visible warning or fail under --strict. Silent degradation is a conformance violation. In the reference compiler today every adapter's capability set is empty โ€” emit() writes instructions but does not yet copy scripts/, install a hook, or wire a subagent โ€” so any value in requires degrades on every one of the nine targets, Claude Code included.

mode = "gate" means the skill produces a verdict that can block โ€” a pre-push check, a CI step. Gates must ground that verdict in a script exit code or a schema-validated artifact. A gate-mode skill with no scripts/ directory and no artifacts.produces has nothing to produce a verdict from, and fails the gate-verdict check under lint and test.

[lore]

Access to the project's accumulated knowledge. Optional, and experimental.

FieldTypeRequiredDefaultConstraints
readsarray of stringsno[]decisions | conventions | invariants | map
writesarray of stringsno[]same enum; writes land only via human-reviewed proposals

Declaring a collection in reads is what makes {{lore.<collection>}} meaningful in the body. [lore] is marked experimental in the spec and is separable from core KSF โ€” the surface may move to a @kitbash/lore package. Build on it knowing that.

[dependencies]

Other skills this one builds on. Optional. Unlike the tables above, the keys are not fixed โ€” each key is a skill name and each value a semver range.

FieldTypeRequiredDefaultConstraints
<skill-name>stringnoโ€”key matches ^[a-z][a-z0-9-]{1,40}$; value is a semver range such as "^1.0". Non-string values are dropped by the CLI

The resolver installs the transitive closure and pins every node in kitbash.lock. Cycles are an install-time error. A dependency's produces artifacts become available to the dependent's consumes โ€” and that is the only thing that crosses the edge. Permissions and budgets never flow across a dependency; each manifest stands alone. The table is marked provisional: resolution works, but conflict and private-index behavior is unproven.

A complete skill.toml

Every table, valid against both the schema and the CLI:

[skill]
name = "prereview"
version = "0.1.0"
description = "Review the working diff against this team's real standards before it ships"
license = "Apache-2.0"
authors = ["Kitbash contributors"]
homepage = "https://github.com/singhharsh1708/kitbash"

[triggers]
commands = ["/prereview"]
events = ["pre-push"]

[context]
budget = 1500
standing = 60
disclosure = "lazy"

[permissions]
tools = ["read", "grep", "bash:git diff *", "bash:git log *"]
network = false
write = false

[artifacts]
produces = ["findings@1"]
consumes = ["plan@1"]

[targets]
requires = []
mode = "gate"

[lore]
reads = ["conventions", "invariants"]

That is the manifest of the prereview example skill in the repo. Linting it:

$ kitbash lint examples/skills/prereview
โœ“ prereview
    ยท manifest โ€” prereview@0.1.0
    ยท references
    ยท budget โ€” body ~464 tok / budget 1500
    ยท standing โ€” stub ~40 tok / limit 60
    ยท artifacts โ€” produces 1, consumes 1

linted 1 skill(s) ยท 0 failure(s) ยท 0 warning(s)

SKILL.md

Plain markdown. No required structure beyond one rule: the first real paragraph is the standing stub, and everything after it loads on invocation.

The standing stub

The stub is what an agent sees when the skill is installed but not invoked โ€” the "what this does and when to reach for it" line. It must fit context.standing.

Kitbash computes it by splitting the body on blank lines and walking the blocks in order. Within each block, lines that are markdown ATX headings (# through ######) are dropped; the first block with anything left is the stub. Headings are skipped deliberately โ€” otherwise a # Title line would be read as the stub, and a bloated description sitting underneath it would slip past the standing check entirely.

So both of these produce the same stub โ€” the paragraph, never the title line:

# Prereview

Review the current diff against this team's conventions
and invariants before it ships.

## Procedure
...
Review the current diff against this team's conventions
and invariants before it ships.

## Procedure
...

Token counts are estimated at roughly four characters per token โ€” deliberately rough, and good enough for budget enforcement. kitbash lint reports the measured stub and body size against the declared limits.

Template variables

Double-brace variables resolve at compile time. The accepted form is {{namespace.name}}, where the namespace is lowercase letters and the name is lowercase alphanumerics and hyphens. Whitespace inside the braces is allowed; whitespace around the dot is not.

VariableResolves to
{{artifact.<type>}}A path reference: .kitbash/artifacts/<type>.json, which the agent reads at invocation time.
{{lore.<collection>}}A pointer to the project knowledge under .kitbash/lore/<collection>/, with an explicit instruction to skip silently if it is absent.
{{prompt.<name>}}The contents of prompts/<name>.md from the skill directory, inlined. A missing file is a hard error.

Anything else fails the compile. An unknown namespace reports the whole variable:

โœ— demo
    ยท manifest โ€” demo@1.0.0
    โœ— references โ€” demo: unknown template variable {{config.thing}}

And anything still in brace form after resolution โ€” a malformed name, spaces around the dot, an underscore namespace โ€” is reported rather than leaked into the compiled output:

demo: unresolved template variable "{{ lore . conventions }}" โ€”
  unknown namespace or malformed (spec ยง3)

Declare before you reference. {{lore.conventions}} resolves whether or not you listed conventions in [lore] reads โ€” resolution is textual. The manifest declaration is what makes the access disclosed at install review. Reference a collection you did not declare and you have written a skill that reads project knowledge the reviewer was never told about.

What gets enforced, and when

Three tiers, and the distinction matters if you are writing a skill that has to work on more than one implementation.

Hard errors. The manifest fails to load, and every command that touches the skill fails with it. The value constraints on frozen fields are all here: the name pattern, semver, description length, budget presence and range, standing range. So is a budget or standing that is fractional rather than an integer, a scalar given where the schema types an array (commands = "/deploy" would otherwise silently coerce to [] โ€” a slash command that never registers), and a disclosure value that is neither lazy nor eager (a frozen enum must not silently fall back to lazy).

skill.toml: invalid manifest
  - skill.name "Bad_Name" must match /^[a-z][a-z0-9-]{1,40}$/
  - skill.version "1.0" is not semver
  - skill.description must be 10โ€“200 characters
  - context.budget is required and must be 50โ€“20000

Lint failures. Surfaced by kitbash lint and kitbash test as failures with a nonzero exit: unresolvable template variables, missing prompt files, artifact refs that are not name@version, trigger commands without a leading slash, a body over its declared budget, and a stub over its declared standing limit.

Warnings. Unknown tables and unrecognized values in the forward-compatible enums โ€” targets.requires, targets.mode, triggers.events, and the lore collections โ€” warn instead of failing, because a newer skill running on an older compiler should not hard-error on a field that compiler has never heard of. (context.disclosure is the exception: it is a frozen enum, so an unrecognized value there is a hard error, not a warning.) Pass --strict to turn warnings into a nonzero exit.

โš  demo
    ยท manifest โ€” demo@1.0.0
    โš  schema โ€” unknown table [widgets] โ€” typo or unsupported, ignored;
      targets.requires "gpu" is not one of scripts, hooks, subagents, network

The JSON Schema is stricter than the CLI on purpose. It sets additionalProperties: false on the manifest root and on every fixed table, so an unknown key inside a known table is a schema violation โ€” while the CLI simply ignores keys it does not read. [dependencies] is the exception: its keys are skill names, so they are constrained by pattern rather than enumerated. It also enforces the command pattern, the artifact-ref pattern, the dependency-key pattern, and format: uri on homepage. Validate against the schema in CI if you want the tight reading.

Stability policy

Per RFC 0002, the core manifest fields are frozen and evolve additive-only within a major version. Fields marked provisional or experimental may still change.

TableStatus
[skill], [context], [triggers], [targets], [artifacts]stable โ€” frozen at 1.0; additive changes only within a major
[permissions]stable shape; the tool-string grammar is provisional and advisory today
[dependencies]provisional โ€” resolution works, but conflict and private-index behavior is unproven
[lore]experimental โ€” separable from core KSF; the surface may move to @kitbash/lore

The compatibility rule that follows from this is worth stating plainly: within a major version, existing fields never change meaning or type, new fields may be added, and unknown fields are ignored with a lint warning rather than a hard error. That is what lets a skill written against a newer spec still compile on an older toolchain โ€” degraded, and visibly so, but not broken.

An adapter is conformant when it passes the conformance suite shipping with v1.0: budget enforcement, standing-stub limits, degradation visibility, generated-file headers, permission honesty. At spec 1.0 KSF moves to its own repository under open governance. Changes land via RFCs.