Skip to content
By Wayne Sun, Greg Allen, Ralph Bean
Picture of Wayne Sun
Wayne Sun
Picture of Greg Allen
Greg Allen
Picture of Ralph Bean
Ralph Bean

Layered Configuration Reference

This guide documents how fullsend resolves per-repo configuration fields through the layered config system introduced by ADR 0069 Decision 2.

For initial setup instructions, see Configuring GitHub. For advanced installation variants, see Advanced Setup.

Overview

Per-repo configuration is stored in two files inside the target repository's .fullsend/ directory:

FileRoleWritable?
config.yamlOverlay — repo-specific customizationYes
config.base.yamlBase — vendor preset or shared baselineRead-through only

When an accessor reads a config field, it checks the overlay first, then the base layer, then compiled-in code defaults. Only the overlay (config.yaml) is writable — the base layer and code defaults are read-through layers that the overlay inherits from.

config.yaml (overlay)
    ↓ fallthrough when unset
config.base.yaml (base)
    ↓ fallthrough when unset
code defaults (compiled into fullsend)

Existing installations without config.base.yaml are unaffected — the overlay falls through directly to code defaults.

fullsend github setup --config commits the preset as config.base.yaml and writes only explicitly passed persistent setup flags into config.yaml. Required values such as inference.project may come from the preset alone; CLI flags override the same keys without rewriting the preset file.

Marshal behavior

Marshal (and any serialization path) emits only values explicitly set on the local layer. Values resolved through a parent layer are never written back. This means:

  • Round-tripping a config through parse → marshal preserves only locally-set fields. For slice fields (roles, allowed_remote_resources), the nil-vs-empty distinction is also preserved: an explicitly empty list (e.g., roles: []) survives the roundtrip and does not collapse to nil, which would cause unwanted fallthrough to parent defaults.
  • Upgrading a base layer (e.g., refreshing config.base.yaml from a new preset) does not require editing config.yaml — the overlay inherits new defaults automatically for any field it does not override.

Unset detection

Each field type uses a zero-value convention to distinguish "unset (fall through to parent)" from "explicitly set":

Go typeUnset (falls through)Set (uses local value)
string"" (empty string)Any non-empty string
[]T (slice)nil (key omitted from YAML)Non-nil, including [] (empty slice)
*T (pointer)nilNon-nil (including pointer to zero value)

This distinction matters most for slice and pointer fields, where the difference between "not specified" and "explicitly empty" drives different merge behavior.

Per-field merge rules

The table below documents how each per-repo config field resolves through the overlay → base → code defaults chain.

FieldTypeMerge ruleCode default
versionstringScalar override"1"
runtimestringScalar override"claude"
kill_switch*boolScalar overridefalse (inactive)
keep_history*boolScalar overridetrue (history appended)
roles[]stringReplace if setPerRepoDefaultRoles()
agents[]AgentEntryKeyed merge by DerivedName()nil (none)
allowed_remote_resources[]stringUnion with deny-allDefaultAllowedRemoteResources()
forgestringScalar override"" (GitHub)
trackerstringScalar override"" (none)
mint_urlstringScalar overrideDefaultPerRepoMintURL (hosted public mint)
inference.providerstring (nested)Scalar override"vertex"
inference.projectstring (nested)Scalar override"" (empty)
inference.regionstring (nested)Scalar override"global"
inference.wif_providerstring (nested)Scalar override"" (empty)
inference.openai.audiencestring (nested)Scalar override"" (empty)
inference.openai.identity_provider_idstring (nested)Scalar override"" (empty)
inference.openai.service_account_idstring (nested)Scalar override"" (empty)
models.aliasesmap[string]string (nested)Per-key mergenil (fleet defaults)
create_issues*CreateIssuesConfigReplace whole object if setnil
status_notifications*StatusNotificationConfigReplace whole object if setnil

Per-agent runtime, model, effort, subagents on agents: entries

An agents: entry may set runtime, model, effort and subagents for that agent. The ref field records the branch or tag that was resolved when the agent was adopted via agent add; agent update re-resolves against this ref instead of the default branch when it is present (empty for SHA-pinned or legacy entries).

An enabled entry without source: is an override-only entry that tunes a built-in agent by name (or, in an overlay, a custom agent registered in the base layer). The keyed merge by DerivedName() carries the four settings field by field: for runtime, model and effort the overlay's non-empty value wins and an empty value inherits the parent's. subagents uses per-key merge: overlay entries override or tombstone (~) individual persona keys while unstated keys inherit from the parent. There is no way to unset a parent's scalar value from the overlay short of restating the entry.

yaml
# config.base.yaml (preset)
agents:
  - source: harness/lint.yaml   # name derived from the file: lint
    model: opus
# config.yaml (overlay)
agents:
  - name: lint          # merges onto the base entry: source kept, effort added
    effort: medium
  - name: code          # built-in agent tuned by name
    runtime: claude

Precedence at run time: flag > env var > the agent's entry > repo-wide runtime: / harness default.

Scalar override fields

version, runtime, and kill_switch use simple scalar override semantics: if the overlay sets the field, that value is used. If unset, the accessor falls through to the base layer, then to code defaults.

  • version: Schema version string. Unset ("") falls through to parent. Code default is "1".
  • runtime: Agent runtime identifier. Unset ("") falls through to parent. Code default is "claude". Valid values: claude, pi, codex, dummy, dummy-playback.
  • kill_switch: Pointer to bool (*bool). Using a pointer allows distinguishing between three states:
    • nil (key omitted) — unset, falls through to parent.
    • *false (explicit kill_switch: false) — locally set to inactive. Does not fall through.
    • *true (explicit kill_switch: true) — locally set to active.
  • keep_history: Pointer to bool (*bool). Controls whether sticky comment updates (from post-review, post-comment, and issues post-comment) append the previous body as a collapsed "Previous run" <details> block. Uses the same three-state pointer semantics as kill_switch:
    • nil (key omitted) — unset, falls through to parent. Code default is true (history appended, preserving existing behavior).
    • *true (explicit keep_history: true) — updates collapse old content into history blocks.
    • *false (explicit keep_history: false) — updates replace the comment body in-place with no history. Useful when accumulated "Previous run" blocks add unwanted noise (e.g., when comments are synced to Jira where <details> does not render as collapsible).

mint_url and inference — scalar override (ADR 0069 Decision 1)

mint_url stores the token mint URL. It is a flat string field on perRepoConfig and follows the same scalar override semantics as runtime: unset ("") falls through to parent, then to code default DefaultPerRepoMintURL (the hosted public mint).

inference groups inference backend settings under a single YAML key (inference:) using the PerRepoInferenceConfig struct. Each subfield (provider, project, region, wif_provider, and the three under openai) resolves independently through scalar override semantics:

  • inference.provider: Inference provider identifier (e.g. "vertex"). Unset ("") falls through to parent, then to code default "vertex".
  • inference.project: GCP project ID for inference. Unset ("") falls through to parent (no code default — must be provided by the installer).
  • inference.region: GCP region for inference. Unset ("") falls through to parent, then to code default "global".
  • inference.wif_provider: Full WIF provider resource name. Unset ("") falls through to parent (no code default — must be provided by the installer).
  • inference.openai.{audience,identity_provider_id,service_account_id}: the OpenAI Workload Identity identifiers for GPT on pi or codex (ADR 0092), written by fullsend github setup --openai-*. Each resolves independently through the layers; a run needs all three from one source. The FULLSEND_OPENAI_* runner variables, when any is set, replace the resolved block entirely.

The inference pointer itself (*PerRepoInferenceConfig) uses nil to mean "no local inference settings" — if the entire inference: key is omitted from YAML, all subfields fall through to the parent layer. If the key is present, each subfield is checked independently.

Example:

yaml
# config.base.yaml (base layer)
mint_url: https://mint.example.com
inference:
  provider: vertex
  project: base-project
  region: us-central1
  wif_provider: projects/123/locations/global/workloadIdentityPools/pool/providers/base

# config.yaml (overlay) — override project only
inference:
  project: my-project
# Effective:
#   mint_url: https://mint.example.com (from base)
#   inference.provider: vertex (from base)
#   inference.project: my-project (from overlay)
#   inference.region: us-central1 (from base)
#   inference.wif_provider: ...base... (from base)

models.aliases — per-key merge

models.aliases overrides fullsend's pinned model alias table per key (#6882). Keys are the existing alias vocabulary (opus, sonnet, haiku, fable); values are model ids or provider/id specs validated with ValidModelRef, and never another alias name — bare or as the id segment of a provider/id spec (aliases resolve once, so sonnet: opus or sonnet: anthropic-vertex/opus would reach the provider as the literal id opus). An unknown key is a config validation error.

Merge is per key across layers: an overlay that sets fable inherits the base's sonnet entry without restating it. A nil Models block (key omitted from YAML) falls through to the parent layer. Validation runs on the merged map, so a bad key in config.base.yaml fails an overlay write (and fullsend run) even when the overlay omits models:.

yaml
# config.base.yaml
models:
  aliases:
    sonnet: claude-sonnet-5

# config.yaml (overlay)
models:
  aliases:
    fable: claude-fable-5-1

# Effective: sonnet → claude-sonnet-5 (from base), fable → claude-fable-5-1 (from overlay),
# opus and haiku → fleet defaults (compiled-in).

tracker — scalar override

tracker stores the default issue tracker for fullsend issues commands (github, gitlab, or jira). Unset ("") means no default — --tracker is required on every fullsend issues invocation. When set, it is used as the default for --tracker on both fullsend issues get and fullsend issues post-comment; an explicit --tracker flag overrides it. Distinct from forge: a repo's hosting forge does not imply its issue tracker (e.g. a GitHub-hosted repo may track issues in Jira).

roles — replace if set

The roles field uses replace-if-set semantics with no union:

  • nil (key omitted from YAML) — falls through to parent, then to code default PerRepoDefaultRoles().
  • Non-nil including roles: [] (explicit empty list) — replaces the parent value entirely. There is no merge or union of role lists across layers. An explicit roles: [] is preserved through marshal roundtrips (it will not be dropped or collapse to nil).

Example:

yaml
# config.base.yaml (base layer)
roles:
  - triage
  - coder
  - review

# config.yaml (overlay) — replaces, does not union
roles:
  - triage
  - coder
# Effective: [triage, coder] — review is NOT inherited

agents — keyed merge by DerivedName()

The agents field uses keyed merge semantics. Each agent is identified by its DerivedName() — the explicit name field if set, otherwise derived from the source filename (e.g., triage.yamltriage).

Merge behavior:

  • nil (key omitted) — parent agents are returned unchanged.
  • agents: [] (explicit empty list) — no overlay entries, but parent agents remain visible. This is not deny-all.
  • Non-empty — overlay entries are merged with parent entries by name:
    • If an overlay entry matches a parent entry by DerivedName() (case- insensitive), the overlay fields are applied on top of the parent entry. Setting source in the overlay replaces the parent URL for that agent. Setting enabled toggles the agent without replacing its source.
    • Overlay entries that do not match any parent agent are appended to the result.
    • Parent agents with no matching overlay entry pass through unchanged.

Example:

yaml
# config.base.yaml (base layer)
agents:
  - source: https://example.com/triage.yaml#sha256=abc...
  - source: https://example.com/review.yaml#sha256=def...

# config.yaml (overlay) — disable review, add custom agent
agents:
  - name: review
    enabled: false
  - source: agents/my-custom.yaml
# Effective:
#   - triage.yaml from base (unchanged)
#   - review.yaml from base (disabled by overlay)
#   - my-custom.yaml from overlay (new entry)

allowed_remote_resources — union with deny-all

This field controls which URL prefixes are allowed for remote resources (agents, policies, skills, plugins, profiles, providers, and base composition). It uses special three-way semantics:

Overlay valueBehavior
nil (key omitted)Falls through to parent → code defaults
[] (explicit empty)Deny-all — no remote resources allowed, no fallthrough
Non-empty listUnion of overlay entries ∪ parent entries

When the overlay provides a non-empty list and a parent exists, the effective value is the union of both lists (overlay entries first, then any parent entries not already present). Code defaults are provided solely by the terminal perRepoDefaults parent — intermediate parents may return whatever allowlist they want, including omitting the built-in prefixes. The union does not force-append code defaults after the parent union.

Example:

yaml
# config.base.yaml (base layer)
allowed_remote_resources:
  - https://raw.githubusercontent.com/fullsend-ai/fullsend/
  - https://raw.githubusercontent.com/fullsend-ai/agents/

# config.yaml (overlay) — adds a custom prefix
allowed_remote_resources:
  - https://raw.githubusercontent.com/my-org/agents/
# Effective (union):
#   - https://raw.githubusercontent.com/my-org/agents/
#   - https://raw.githubusercontent.com/fullsend-ai/fullsend/
#   - https://raw.githubusercontent.com/fullsend-ai/agents/

To deny all remote resources (local agents only):

yaml
# config.yaml (overlay) — explicit empty list
allowed_remote_resources: []
# Effective: [] — no remote resources allowed

create_issues — replace whole object if set

The create_issues field uses replace-if-set semantics for the entire object:

  • nil (key omitted) — falls through to parent, then to code default nil.
  • Non-nil — replaces the parent value entirely. There is no merge of allow_targets lists across layers.

status_notifications — replace whole object if set

The status_notifications field uses the same replace-if-set semantics as create_issues:

  • nil (key omitted) — falls through to parent, then to code default nil.
  • Non-nil — replaces the parent value entirely, including nested comment.start/comment.completion settings.

Code defaults reference

When neither the overlay nor the base layer sets a field, the following compiled-in defaults apply:

FieldDefault value
version"1"
runtime"claude"
kill_switchfalse (inactive)
keep_historytrue (history appended)
roles["triage", "coder", "review", "fix", "retro", "prioritize"]
agentsnil (none configured)
allowed_remote_resources["https://raw.githubusercontent.com/fullsend-ai/fullsend/", "https://raw.githubusercontent.com/fullsend-ai/agents/"]
forge"" (GitHub)
tracker"" (none — --tracker is required unless set)
mint_url"https://mint.fullsend.sh" (hosted public mint)
inference.provider"vertex"
inference.project"" (empty — must be provided)
inference.region"global"
inference.wif_provider"" (empty — must be provided)
models.aliasesnil (fleet alias table compiled into the runtimes)
create_issuesnil
status_notificationsnil