Project Variables
A pre-parse injection system for cross-file value reuse and environment configuration.
Project Variables provide a deterministic, text-level substitution layer that resolves user-defined constants and environment variables into .xcaf manifests before the YAML AST is constructed.
Variables enable DRY (Don't Repeat Yourself) manifest authoring by extracting shared values—like organization names, model preferences, and base paths—into a single source of truth, reducing duplication across large agentic frameworks.
The Core Mechanism
The variable system operates on a tiered resolution model and a pre-parse expansion pipeline.
- Discovery & Layering: When
xcaffold apply(or any read command) scans thexcaf/directory, it first searches for variable files. It loads the baseproject.vars, layers any target-specific overrides likeproject.claude.vars, and finally applies developer-specific local overrides fromproject.vars.local. - Environment Filtering: Concurrently, the compiler reads the
allowed-env-varslist fromproject.xcafand extracts only those specific keys from the runtime environment. - Pre-Parse Expansion: As the parser processes each
.xcaffile, it performs a regex-based substitution on the raw byte stream, replacing${var.name}and${env.NAME}tokens with their concrete values. - Type Preservation: Variables injected into the YAML stream retain their native YAML types (string, boolean, integer, list) during the
yaml.Unmarshalphase, preserving structural integrity. Variable values themselves can also contain references to other variables (composition), which are resolved recursively by the parser.
Design Decisions
Pre-Parse Resolution vs. AST Traversal
We chose to expand variables at the raw text level before YAML parsing, rather than resolving them during the AST traversal phase (which is how ${agent.id.field} cross-references work). This design ensures that downstream compiler logic, such as policy enforcement and optimization passes, only ever sees literal values. It prevents the AST from being polluted with unresolved tokens and simplifies the compiler pipeline.
Properties-Style Syntax over YAML
Variable files use a simple key = value assignment syntax rather than standard YAML (key: value). This creates a clear visual distinction between defining a constant in a .vars file and defining a manifest field in a .xcaf file, reducing cognitive load for authors.
Explicit Environment Allow-Lists
Environment variables are powerful but pose a security risk if a malicious blueprint attempts to exfiltrate system secrets via ${env.AWS_ACCESS_KEY_ID}. We mandate that all accessible environment variables be explicitly declared in the project.xcaf allowed-env-vars array, establishing a secure perimeter around the compilation context.
Flexible Naming Conventions
Variable names are not restricted to kebab-case. Authors are free to use snake_case, camelCase, or PascalCase to match their team's preferences. Names must start with a letter and contain only alphanumeric characters, underscores, or hyphens (^[a-zA-Z][_a-zA-Z0-9-]*$).
Interaction with Other Concepts
- Multi-Target Compilation: The variable system's tiering mechanism integrates deeply with the compiler's target flags. When compiling for
claude,project.claude.varsis automatically injected, allowing providers to share a common manifest structure while receiving distinct runtime parameters (like model IDs). - State & Drift: The variable stack (
project.varsandproject.<target>.vars) is tracked in the project state file (.xcaffold/*.xcaf.state). Modifying a shared variable automatically invalidates the cache for all dependent compiled outputs, triggering a full recompilation on the nextapply.
When This Matters
- Managing Environments: When deploying agent configurations across multiple environments (development, staging, production), teams can use the
--var-fileCLI flag to inject environment-specific constants without altering the core.xcafmanifests. - Provider-Specific Tuning: When a workflow must run on both Claude and Gemini, but requires different timeout thresholds or model aliases for each, authors can declare those differences in
project.claude.varsandproject.gemini.varswhile maintaining a singleworkflow.xcaffile. - Local Development Secrets: Developers can use the gitignored
project.vars.localfile to inject personal API keys or test values during local development without risking a commit leak.
Related
- Multi-Target Rendering — how target-specific variable files integrate with compilation
- State & Drift — how variable files are tracked in state