Extending stacks (extends:)
A stack (catalog/stacks/<name>/stack.yaml) composes recipes, services, and a handful of
policy fields (permissions, instructions, state, credential forwarding). Once a few stacks
share the same base — the same house recipes, the same permission mode, the same identity text —
copy-paste starts to rot: you fix a base recipe list in one stack and forget the other four.
extends: names a base stack; the child is the base merged with its own manifest.
# catalog/stacks/team-base/stack.yaml
name: team-base
recipes: [repowise, gsd-core]
services: [ping]
permissions: auto
instructions: |
You are a team agent working in a shared repo.
# catalog/stacks/team-web/stack.yaml
name: team-web
extends: team-base
recipes: [playwright] # → repowise, gsd-core, playwright
services: [] # → ping (base's services still apply)
permissions: yolo # → yolo (child wins)
team-web resolves to recipes: [repowise, gsd-core, playwright], services: [ping],
permissions: yolo, and the base’s instructions.
Resolution
extends:is a single stack name, not a list. No multiple inheritance, no diamond merge.- The base is resolved in the child’s own catalog root first (so a fixture tree or a self-contained overlay resolves within itself), then the normal catalog search (user overlay first, then the repo catalog). This is the point of the feature — a stack in your overlay extends a base the repo ships.
- Chains are allowed (
aextendsbextendsc), merged base-first. Cycles are a hard error, named in the message. name:is never inherited. It must still equal the stack’s directory name.- Unknown stack fields are rejected with a suggestion. This is deliberate: before the feature
shipped, unknown keys were silently ignored, so
extends:written in a manifest did nothing for months while looking accepted.
Merge rules, field by field
| Field | Rule |
|---|---|
name | Never inherited. Must match the stack’s directory name. |
recipes | Base’s list first (in base order), then the child’s. Duplicates collapse to their first occurrence. |
services | Same as recipes. |
harnesses | Same as recipes. |
ssh_keys | Same as recipes — union, base’s entries first, de-duped. See the security note below. |
permissions | Child wins when set; otherwise the base’s. |
instructions | Child replaces the base’s text when set (no concatenation — two identity blocks read as a contradiction). |
forward_git_credentials, forward_aws_sso | Inherited; the child may set either back to false. |
hatago | Child wins whole block when set; otherwise inherited. |
state | Child wins whole block when set; otherwise inherited. (Replace, not per-key merge.) |
There is no way to remove a recipe or service the base declares. If you need the base minus something, the base is wrong — split it.
Security: ssh_keys and credential forwarding
ssh_keys unions exactly like recipes and services — the base’s key names appear first, then
the child’s. However, the launcher enforces that private-key mounts are honored only from the
user-overlay catalog (~/.config/harnessed/catalog). A repo-catalog base that lists ssh_keys
will carry those names into the merged manifest, but the launcher will drop them at launch time
because the stack does not originate from your overlay. Declare ssh_keys in the stack you actually
launch, in your own overlay, so the key owner’s intent is explicit and not inherited from a
base you didn’t write.
forward_git_credentials and forward_aws_sso are inherited normally. A child may always set
either to false to opt out of what the base turned on.
Rebuilds: editing a base affects its children
A built profile is a pure function of its catalog inputs, and harnessed hashes those inputs into a
staleness stamp. Under extends:, the inputs of a child stack include every stack.yaml in its
chain plus every recipe dir the merged list references. Edit team-base, and team-web reports
stale on the next harnessed list / launch and rebuilds — the same as if you had edited
team-web’s own manifest.
See also
- Composing stacks — the stack manifest itself, and the build/run lifecycle.
- Recipe authoring — recipes and recipe varieties (
<family>/<variety>). src/harnessed/schema.py— the typedStackmodel and_resolve_stack_extends.tests/test_stack_extends.py— merge semantics and error cases.