Configuration and the Cascade
Source
Configuration lives in the main skill's metadata in frontmatter. There is no separate config file. Each metadata key is handled by its corresponding middleware — cwd by the cwd tool, workspace by the workspace tool, and so on. Metadata keys map directly to tool names resolved via findTool().
---
name: my-app
metadata:
role: main
paths:
- ./skills
model:
id: us.anthropic.claude-sonnet-4-6
region: us-east-1
---
Main Skill Resolution
- Single
SKILL.mdor.skill.mdin project root → use it. - Multiple candidates → prefer
role: mainormetadata.main. - Ambiguous → error.
role: main (or metadata.main: true) serves dual purpose: it defines the config boundary (authority) for all tools in its subtree, and it is the skill that runs when no skill is named on the command line. The check is: role === 'main' || !!metadata?.main.
Fields
| Field | Type | Default | Description |
|---|---|---|---|
$inherit |
string | string[] | false | (none) | Inheritance declaration. String names a parent; array enables multiple inheritance via C3 linearization. $authority resolves to the filesystem authority. false skips the cascade entirely. |
paths |
PathEntry[] | [cwd, { external: library }, ...] |
Search path for tool resolution. Bootstrap sets [cwd, { external: library }, { ignore: "dist" }, { ignore: "node_modules" }, { ignore: ".git" }, { ignore: ".cache" }]. The default pipeline (default.skill.md) prepends workspace and appends additional ignore patterns. Supports { external: "path" } for isolated apps, { internal: "path" } for non-external paths with flags, and { ignore: "pattern" } for directories to skip during scanning. |
model |
object | — | Agent provider config (model, region, etc.). Auto-promoted to global overrides via $self. |
workspace |
string | ./workspace |
Workspace directory for compiled tools and build artifacts. |
mcp |
Record | — | External MCP server configs. Each entry has command/args/env (stdio) or url (Streamable HTTP). |
cwd |
string | process.cwd() |
Project root. Set by bootstrap. |
Authority Resolution
Every tool has an authority — the main skill that governs its configuration:
- Walk up from the tool's file location within the path entry that found it.
- At each directory, look for a skill with
role: main. - First one found is the authority.
- Non-external path entries share authority — resolution walks across their boundaries.
- External path entries (
{ external: "path" }) are isolated — walk up within that entry only. - The library's
default.skill.mdis the universal fallback authority. - A main skill is its own authority.
Per-Tool Metadata
Any skill can declare metadata keys that affect its own invocation. The tool's own keys are layered on top of the cascade result. CLI overrides win over both.
---
name: creative-writer
metadata:
model:
temperature: 0.9
---
The merged metadata is scoped to the current invocation. Parent contexts keep their own metadata. Child contexts go through the same cascade independently.
The Cascade
The cascade resolves a tool's effective metadata at invocation time. It walks the inheritance chain, collects metadata from each level, merges it according to declared strategies, layers the tool's own keys on top, and applies CLI/env overrides. The result determines which middleware runs and with what configuration.
The cascade produces a single flat object from multiple sources, applied in this order (later wins):
- Inherited metadata — public keys from the inheritance chain, merged root-first
- Own keys — the tool's own metadata (filtered by
$dynamic/$static/compatibility mode) - Injected metadata — from the context seed (
options.context.locals.middlewarefor non-cascading,options.context.nonlocals.middlewarefor cascading) - Local overrides —
$localpromotions +--localCLI +AGENT_APPS_LOCAL_*env - Global overrides —
$globalpromotions +--globalCLI +AGENT_APPS_GLOBAL_*env
Each layer is merged using the declared $merge strategy per key. The default strategy is replace.
Inheritance Resolution
When a tool does not declare $inherit, the cascade uses the filesystem authority. The authority's own inheritance chain is then walked.
Single inheritance: $inherit: "parent-name" resolves the named tool and walks from there. The chain terminates when a tool has no $inherit, when $inherit is not a string, or when a name has already been visited.
Multiple inheritance: $inherit: [mixin-a, mixin-b] resolves each reference independently and merges via C3 linearization (the same algorithm Python uses for method resolution order). $authority in the array resolves to the filesystem authority.
Opt out: $inherit: false skips the cascade entirely. Only the tool's own metadata and global overrides are used. Local overrides do not apply.
If the chain terminates without reaching default, the runtime automatically appends it. This ensures every tool gets the standard pipeline.
Public and Private Keys
At each level of the inheritance chain, $public and $private control which keys propagate to children:
| Declaration | What propagates |
|---|---|
| Neither | Everything |
Only $public: [key1, key2] |
Only listed keys |
Only $private: [key1, key2] |
Everything except listed keys |
Both accept path expressions (bare keys or JSONPath). The declaring tool always sees all of its own keys — public/private only affects what children inherit.
Merge Strategies
Declared via $merge. The default is replace — child overwrites parent.
| Strategy | Behavior | Applies to |
|---|---|---|
replace |
Child overwrites parent entirely | Any type |
shallow |
{ ...parent, ...child } one level deep |
Objects |
deep |
Recursive merge; scalars/arrays replace | Objects |
prepend |
[...child, ...parent] |
Arrays |
append |
[...parent, ...child] |
Arrays |
$merge propagates through the cascade. A child can override per key. JSONPath expressions like "$.mcp.headers" target nested keys.
Examples:
Replace (default): parent: { workspace: "./a" } + child: { workspace: "./b" } → { workspace: "./b" }
Shallow: parent: { model: { id: "sonnet", region: "us-east-1" } } + child: { model: { temperature: 0.9 } } → { model: { id: "sonnet", region: "us-east-1", temperature: 0.9 } }
Prepend: parent: { paths: ["./lib"] } + child: { paths: ["./skills"] } → { paths: ["./skills", "./lib"] }
Ordering with $order
Middleware ordering is declared in a single $order block with per-key { before, after } objects. Constraints reference other middleware names or phase sentinels ($pre-configure, $configure, $post-configure, $pre-execute, $execute, $post-execute).
$order:
cwd: { after: [$pre-configure], before: [$configure] }
trust: { after: [$pre-execute], before: [$execute] }
Middleware without explicit $order defaults to after: [$configure], before: [$post-configure]. $order also supports JSONPath keys for array element ordering. Sentinel-to-sentinel ordering is also supported — $order entries whose keys start with $ create edges between phase sentinels, ensuring the sentinel chain stays connected even when no middleware entry spans a gap.
Removal with $remove
An array of path expressions resolved against the merged metadata. Matched keys or array elements are deleted. Applied after merging at each level, so a child can remove inherited keys.
Dynamic, Static, and Compatibility Mode
| Mode | No annotation | $dynamic |
$static |
|---|---|---|---|
| Normal | All keys → tools | Listed → tools | Listed → data |
| Compatibility | All keys → data | Listed → tools | Listed → data |
Compatibility mode activates automatically for SKILL.md folder-skills — all metadata keys are treated as plain data unless $dynamic explicitly opts them in as tool invocations. This preserves backward compatibility for folder-skills that use metadata keys as configuration data. Normal .skill.md and .skill.ts files use normal mode by default.
When both $dynamic and $static are present, $dynamic wins. Neither propagates through the cascade.
Local and Global Promotions
$local copies keys from the merged metadata into globals.config.localOverrides. These apply to tools whose authority matches the project's main skill.
$global copies keys into globals.config.globalOverrides. These apply unconditionally to all tools.
Tools can declare $global: ["$self"] or $local: ["$self"] in their own frontmatter to auto-promote when they appear as keys in the main skill's metadata. The model, trust, and mcp middleware use this — declaring model: in your main skill automatically promotes it to global overrides without needing $global: [model]. Similarly, tools can declare $private: ["$self"] to automatically keep themselves private when they appear in the cascade — transport middleware (web, websocket, webhook, email, slack), per-skill middleware (session, state, auth, roles, schedule, event), and delegate all use this so user skills don't need to declare $private: [web] or $private: [session]. Explicit $global/$local/$private annotations on the main skill take precedence over $self declarations.
metadata:
$global: [my-custom-config] # explicit promotion for custom keys
my-custom-config: { key: value }
model: # auto-promoted via $self — no annotation needed
id: us.anthropic.claude-sonnet-4-6
CLI overrides (--local, --global) take priority over annotations. Promotions are resolved once from the main skill's metadata and cached.
CLI and Environment Overrides
| Tier | CLI flag | Env prefix | Scope |
|---|---|---|---|
| Config | --config key=value |
AGENT_APPS_CONFIG_* |
Pre-cascade globals (cwd, workspace). Not part of the cascade. |
| Local | --local key=value |
AGENT_APPS_LOCAL_* |
Applied when authority matches the project's main skill. |
| Global | --global key=value |
AGENT_APPS_GLOBAL_* |
Applied unconditionally. |
CLI flags take priority over env vars within the same tier. Env vars use double underscores for nesting, single underscores become hyphens: AGENT_APPS_LOCAL_MODEL__REGION=us-east-1 → { model: { region: "us-east-1" } }.
Override merge behavior: shallow for objects, replace for scalars/arrays.
String Interpolation
String values in metadata support ${path} expansion. Every expression resolves against the context via pathGet(ctx, path).
| Syntax | Resolves to |
|---|---|
${locals.config.X} |
ctx.locals.config.X |
${globals.config.X} |
ctx.globals.config.X |
${path:-fallback} |
Default value if primary is undefined/null/empty. Recursively expanded. |
Interpolation runs per-level during the cascade — before annotation extraction and merging. Both keys and values are interpolated. Escape with backslash: \${name} for literal ${...}.
No implicit environment variable resolution. Use :env[VAR] in prompts or AGENT_APPS_LOCAL_* env vars for config.
From Metadata to Middleware Chain
The merged metadata determines which middleware runs. Each key is checked: does a tool with that exact name exist? If yes, it becomes a middleware entry with the metadata value as args. If no, it is silently skipped.
Entries are topologically sorted using Kahn's algorithm with insertion order as tiebreaker. Phase sentinels constrain ordering but are excluded from the output. Cycles in constraints throw a configuration error.
Auto-Static Cycle Detection
The cascade runs Tarjan's SCC algorithm on the tool dependency graph. Any toolKey in the same strongly connected component as the current tool is demoted to data. Self-references are always removed. Middleware authors do not need $inherit: false or $static to prevent cycles.
Caching
Cascade results are cached on globals.cache.cascade, keyed on toolName + pathsKey. Skipped when injected metadata is present. Invalidation is explicit — clearToolCache(name) after compile, clearAllCaches() after clean. No TTL.
Paths and External Dependencies
metadata:
paths:
- ./skills # part of this app
- { external: ./.agent-apps/packages } # hub packages: isolated authority
- { external: ./libs/third-party } # external app
- { internal: ./vanilla-skills } # non-external with flags
- { ignore: "dist" } # skip during scanning
All entries searched left-to-right. Earlier entries win. internal entries are semantically identical to strings but allow flags. External entries have isolated authority resolution — they never cross into your app's authority. Hub packages are external by default. { ignore } entries specify glob patterns for directories to skip during tool scanning — they are not searched as paths.
Shared Resources on Globals
$global is the primary mechanism for cross-boundary config sharing. Key properties on ctx.globals:
globals.config.localOverrides/globals.config.globalOverrides— populated by annotations and CLI flagsglobals.config.cwd,globals.config.library,globals.config.workspace,globals.config.paths— initial values from CLI/env/constantsglobals.tools— MCP connections and programmatic toolsglobals.runtime— top-level invoker for out-of-pipeline use (scheduled tasks, MCP elicitation)globals.internals— runtime-private state (event handlers, event queues, trust decisions, etc.)
Summary
inherit chain (root-first, public keys only, per-level interpolation)
→ own keys (filtered by $dynamic/$static/compatibility)
→ injected metadata
→ local overrides ($local + --local)
→ global overrides ($global + --global)
→ toolKeys computation (auto-static cycle detection)
→ middleware chain building ($order → topoSort)