Skip to content

extending stacks

Mike Crowe edited this page Jul 31, 2026 · 3 revisions

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 (a extends b extends c), 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

Clone this wiki locally