Extends
Cross-layer artifact inheritance via the extends field.
When two layers contribute artifacts with the same canonical ID, ingest rejects the collision by default. The higher-precedence artifact can declare extends: in its frontmatter to inherit from the lower-precedence one and refine specific fields instead of forking.
---
type: skill
name: pay-invoice
version: 2.0.0
extends: finance/ap/pay-invoice@1.x
description: Pay an approved invoice, with team-specific routing.
mcpServers:
- name: finance-warehouse
transport: stdio
command: npx
args: ["-y", "@team-foo/finance-warehouse-mcp"]
---
Team-specific addendum on top of the org-wide skill...At request time the registry folds the parent's frontmatter into the child's per the field-specific rules below. The child's prose body replaces the parent's; bodies are never concatenated.
Pinning
extends: accepts the same pin syntax as other artifact references:
| Syntax | Meaning |
|---|---|
<id> | Resolves to latest at the child's ingest time, then pins. |
<id>@<semver> | Exact version. |
<id>@<semver>.x | Minor or patch range (e.g., 1.2.x, 1.x). |
<id>@sha256:<hash> | Content-pinned. |
Parent version is resolved at the child's ingest time and stored as a hard pin in the ingested manifest's resolved form. Parent updates do not silently propagate. Re-ingesting the child's unchanged bytes is counted idempotent and changes nothing, so the child picks up a newer parent only when it is published at a new version:.
An unpinned <id> reference resolves to the most recently ingested non-deprecated version of the parent, which is the version with the latest ingest timestamp rather than the highest semver.
Deprecated parent versions
A child may not extend a deprecated parent version.
- An
extends:reference that names a deprecated version explicitly, by exact semver (<id>@1.2.0) or by content hash (<id>@sha256:<hash>), is rejected at ingest withingest.invalid_artifact. - A range (
<id>@1.x) or unpinned (<id>) reference selects among the parent's non-deprecated versions. Deprecated versions are removed from the candidate set before resolution, solatestfor such a reference is the most recently ingested non-deprecated version. - When the parent has stored versions and the filter leaves no candidate, ingest rejects the child with
ingest.invalid_artifactand reports that the parent's candidate versions are deprecated. The reference does not fall back to the deprecated versions.
Deprecation is per-version and a stored version's flag never changes. Deprecating a parent line means publishing a new parent version that carries deprecated: true, which adds a candidate the filter skips. A child that is already stored keeps the parent pin recorded at its own ingest, and a later deprecated parent version leaves that stored pin and its resolution on the read path unchanged.
The refusal therefore applies to the child version being ingested, and it never invalidates a child already stored against a parent version that was live when the child was ingested. A child runs the check again when it is published at a new version:. At that point a range or unpinned reference re-pins onto a non-deprecated candidate, and an exact or content-hash pin naming a deprecated version is refused. A pin that names a version already deprecated when the child is first ingested is rejected on that ingest and on every retry until the extends: reference names a live version.
Field merge semantics
When the child declares extends:, fields merge per the table below. Author-specified fields on the child override or combine with the parent's per the rule.
| Field | Merge rule |
|---|---|
description, name, release_notes | Scalar; child wins. |
tags | List; append unique. |
when_to_use | List; append. |
sensitivity | Scalar; most-restrictive (high > medium > low). |
mcpServers | List of objects; deep-merge by name. |
requiresApproval | List; append. |
runtime_requirements | Map; deep-merge with child wins. |
sandbox_profile | Scalar; most-restrictive. |
delegates_to | List; append. |
external_resources | List; append. |
license | Scalar; child wins (lint warning if changed across layers). |
search_visibility | Scalar; most-restrictive (direct-only > indexed). |
Omitted fields. If a child omits a frontmatter field, or sets an empty scalar, the parent's value is inherited unchanged. This holds for every frontmatter field, including the fields in the table above. When both the parent and the child declare a value, a field in the table merges per its row, and every other field takes the child's value: deprecated, replaced_by, effort_hint, model_class_hint, sbom, rule_mode, rule_globs, rule_description, hook_event, hook_action, server_identifier, target_harnesses, input, output, and any extension-type fields. The child's type: must match the parent's; ingest rejects an extends: chain that crosses types. The child's version: is independent; each artifact has its own version, and the parent version is pinned at the child's ingest time.
Extension types register their own merge semantics via TypeProvider.
The "most-restrictive" rules apply to security-relevant fields. A parent at sensitivity: medium cannot be relaxed to low by a child; a parent with sandbox_profile: read-only-fs cannot be widened to unrestricted.
Bundled files under extends
extends: merges frontmatter fields. Bundled files (everything under the artifact root other than the manifest, including scripts/, references/, assets/, schemas, and templates) do not merge:
- The materialized package carries the child's own bundled files. A file the parent ships is not inherited.
SKILL.mdfollows the same rule. The registry serves the child's authoredSKILL.mdverbatim.- A child that needs a file the parent ships copies that file into its own package.
The content hash the registry records covers the child's own package: its ARTIFACT.md, its SKILL.md, and its bundled files. The parent contributes no bytes to it.
Constraints
- Single inheritance.
extends:is a single scalar; no multiple inheritance. To compose from multiple parents, restructure the parents to chain (A extends B; B extends C). - Cycle detection. Cycles in the
extends:graph are detected at ingest time and rejected. - Parent reference. A child may extend a different canonical ID, inheriting from a separate artifact pinned to the version resolved at the child's ingest time. A child may also extend its own canonical ID, which overlays the artifact contributed by the next-lower-precedence layer; that form is what a cross-layer collision requires. In both forms the child's
type:must match the parent's. - Republishing required for parent updates. Parent version is pinned at the child's ingest time and is fixed for that version of the child. Bumping the parent does not retroactively update the child's resolved manifest, and re-ingesting the child's unchanged bytes is counted idempotent and changes nothing. Publish a new
version:of the child to pin the newer parent.
Replacing instead of extending
To replace a parent artifact entirely (rather than refine it), the lower-precedence layer must remove the parent first or rename the higher-precedence one. Silent shadowing is not permitted: ingest rejects same-ID collisions across layers when neither declares extends:.
This is the safety property that prevents an upper layer from quietly overwriting an org-wide artifact. Replacement is an explicit two-step operation: remove (or rename) the parent, then ingest the replacement.
Hidden parents
When a child manifest declares extends: <parent> and the requesting identity cannot see the layer that contributes the parent, the registry resolves and merges the parent server-side and serves the merged manifest. The parent's existence and ID are not surfaced to the requester.
This preserves layer privacy across the inheritance chain. A team-shared layer can extends: an org-internal layer that the team can see; a contractor with access to the team layer (but not the org layer) sees the merged result without learning about the org layer's contents.
What the registry refuses to serve
The registry enforces the guarantee on the merged manifest itself. It removes the child's extends: reference and then checks the assembled block for the parent's canonical ID. When a key or a value in the block stands as a reference to a chain parent, or the block cannot be rewritten at all, the merged manifest is refused. On a registry server the read fails with registry.invalid_argument and no manifest is served. In the filesystem-registry mode the same check fails the walk over the layer, so podium sync stops on that artifact and nothing materializes.
The check runs on every merged block either mode assembles, whether or not the requester can see the layer that contributes the parent, and the filesystem-registry mode applies it with no requester identity at all. The guarantee is a property of the served bytes rather than of the requester's visibility, so an author whose parent layer is fully visible gets the same refusal.
The check applies to what the child authored and to what it inherited on the same terms, and it covers the extension-type keys the merge restores as well as the fields in the merge table above. It is bounded to a scalar that resolves to a chain parent's ID once the version pin, together with the whitespace and the slashes around it, is removed, so shared/parent/ matches. A value that merely mentions the parent, such as a description quoting it or a path below it like shared/parent/CHARTER.md, resolves to no artifact and is served unchanged. An overlay of the same canonical ID is exempt, because the requester asked for that ID.
The parser resolves YAML merge keys and aliases, so a reference the child supplies through a merge key (<<: *base) is operative: ingest pins the parent, and the merged manifest is then refused on every load. The search descriptor for such a child is served with its frontmatter block omitted, because the descriptor path deletes the extends: key from the child's own authored block and re-reads it, and the merge key resolves the reference back into the block. The parent-ID scan described above runs over the merged block alone, so a descriptor is not held to it. An anchor on the extends: value that a sibling key aliases (extends: &p shared/parent@1.x with note: *p) is refused on the same terms. The registry reassembles the merged block and expands every alias in the values it restores, so such a reference lands in the served block as the parent's canonical ID and fails the check above. Write extends: as a literal top-level key. An anchor on the extends: value that no other key aliases is inert, because the reassembled block is written from the merged fields and carries neither the reference nor the anchor.
Examples
Refining a skill with team-specific MCP servers
The org-wide skill points at the org warehouse; the team-shared layer extends it to use a team-specific warehouse instead.
Org-wide layer:
# layers/org-defaults/finance/ap/pay-invoice/SKILL.md
---
name: pay-invoice
description: Pay an approved invoice. Use after invoice approval to submit payment to the vendor.
license: MIT
---
Validate the invoice against the warehouse, then submit payment...# layers/org-defaults/finance/ap/pay-invoice/ARTIFACT.md
---
type: skill
version: 1.0.0
sensitivity: medium
mcpServers:
- name: finance-warehouse
transport: stdio
command: npx
args: ["-y", "@company/finance-warehouse-mcp"]
---
<!-- Skill body lives in SKILL.md. -->Team-shared layer:
# layers/team-foo/finance/ap/pay-invoice/SKILL.md
---
name: pay-invoice
description: Pay an approved invoice (team Foo). Use after invoice approval to submit payment with the team cost-center code.
license: MIT
---
Team-specific addendum: also tag payments with the team cost-center
code...# layers/team-foo/finance/ap/pay-invoice/ARTIFACT.md
---
type: skill
version: 2.0.0
extends: finance/ap/pay-invoice@1.x
mcpServers:
- name: finance-warehouse
transport: stdio
command: npx
args: ["-y", "@team-foo/finance-warehouse-mcp"]
---
<!-- Skill body lives in SKILL.md. -->Merged result for a caller who can see both layers: child's description, version, prose body. mcpServers deep-merged by name, with the child's entry overriding the parent's (because the merge key matches). sensitivity: medium carried from the parent (most-restrictive between unset and medium).
Tightening sandbox profile
A child can tighten security-relevant fields in ARTIFACT.md without redeclaring the rest:
# layers/team-foo/platform/deploy-checks/ARTIFACT.md
---
type: skill
version: 2.0.0
extends: platform/deploy-checks@1.x
sandbox_profile: read-only-fs
---
<!-- Skill body lives in SKILL.md. -->The parent's sandbox_profile: unrestricted is overridden by the child's read-only-fs (most-restrictive wins). The rest of the parent (description, prose body, tags, etc.) is unchanged.
Lint behavior
- Unresolved parent at ingest time: ingest error (
ingest.invalid_artifact). The registry pins the parent version when the child is ingested, so the parent must already be ingested under the tenant. Ingest the parent's layer first. - Cycle detected: ingest error.
- Child declaring
extends:against a parent in a layer the child's layer cannot see: ingest succeeds; the parent resolves at request time per the visibility rules above. - Child declaring
extends:with a parent type that doesn't match the child's type: ingest error. - Child declaring
extends:with an exact or content-hash pin onto a deprecated parent version, or with a range or unpinned reference when every stored version of the parent is deprecated: ingest error (ingest.invalid_artifact).
Where to learn more
- Frontmatter reference for the field-by-field schema.