fabro/docs/public/execution/run-configuration.mdx
fabro-sh-0530[bot] 94c657b92e
feat: add run.checkpoint.skip_git_hooks to bypass Git commit hooks (#355)
## Summary

Adds an opt-in `skip_git_hooks` boolean to `[run.checkpoint]` that
causes Fabro-managed run-branch checkpoint commits to pass `--no-verify`
to `git commit`, bypassing local hooks such as `pre-commit` and
`commit-msg`. Defaults to `false`. Metadata-branch snapshots and Fabro
`[[run.hooks]]` are unaffected.

```toml
[run.checkpoint]
skip_git_hooks = true
```

### Plan Summary

- `RunCheckpointSettings` (dense, in `fabro-types`) gains
`skip_git_hooks: bool` with `#[serde(default)]`.
- `RunCheckpointLayer` (sparse, in `fabro-config`) gains
`skip_git_hooks: Option<bool>` so layered config can distinguish unset
from explicit `false`.
- `RunCheckpointLayer::combine` is refactored from a wholesale-replace
to field-level merging: `exclude_globs` keeps its existing replace-wins
semantics; `skip_git_hooks` uses `.or()` (highest-priority layer that
sets it wins).
- `resolve_checkpoint` resolves `None → false`.
- `git_checkpoint` / `checked_git_checkpoint` in `sandbox_git.rs` accept
a new `skip_git_hooks: bool` and append `--no-verify` when true.
- `parallel_branch_commit_cmd` (new helper in `handler/parallel.rs`)
replaces the inline format string and accepts the same flag.
- `GitState` carries `checkpoint_skip_git_hooks`;
`RunOptions::checkpoint_skip_git_hooks()` exposes it; `execute.rs` and
`git.rs` thread it through.
- OpenAPI schema, TypeScript API client, and docs are updated.

### Key design decisions

**Field-level merging in `combine`**: the previous
`RunCheckpointLayer::combine` replaced the whole struct when
`self.exclude_globs` was non-empty. The refactor keeps that same replace
rule for `exclude_globs` while adding independent `Option::or` merging
for `skip_git_hooks`, so the two fields don't interfere.

**`--no-verify` only on run-branch commits**: the flag is injected only
in the two Git commit paths Fabro controls for run-branch checkpoints.
Metadata-branch snapshots use `git2` and never fire local hooks
regardless of this setting.


### Fabro Details

<details>
<summary>Ran 9 stages in 48m 3s for $13.91</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 3s | – | 0 |
| preflight_compile | 2m 4s | – | 0 |
| preflight_lint | 2m 15s | – | 0 |
| implement | 24m 13s | $10.38 | 0 |
| simplify_opus | 10m 31s | $1.59 | 0 |
| simplify_gpt | 5m 8s | $1.93 | 0 |
| verify | 3m 4s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **48m 3s** | **$13.91** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-05-22 17:26:06 -04:00

563 lines
20 KiB
Text

---
title: "Run Configuration"
description: "Configure workflow runs with TOML files"
---
A run config is a TOML file that bundles a workflow graph with all the settings needed to execute it — the goal, model, sandbox, prepare steps, inputs, and hooks. Instead of passing a dozen CLI flags, you check a `.toml` file into version control and launch with a single command:
```bash
fabro run run.toml
```
## Minimal example
A run config needs at minimum a schema version and a goal:
```toml title="run.toml"
_version = 1
[workflow]
graph = "workflow.fabro"
[run]
goal = "Implement the login feature"
```
| Field | Required | Description |
|---|---|---|
| `_version` | No (defaults to `1`) | Schema version. Must be `1` in the first pass. |
| `[workflow].graph` | No | Path to the Graphviz workflow file, relative to the TOML file's directory. Defaults to `workflow.fabro`. |
| `[run].goal` | No | What the workflow should accomplish. Passed to agents and available via `--goal` CLI flag or Graphviz graph `goal` attribute. |
Goal precedence: CLI `--goal` > `[run].goal` > Graphviz graph attribute.
## Full example
```toml title="run.toml"
_version = 1
[workflow]
graph = ".fabro/workflows/ci.fabro"
[run]
goal = "Run the CI pipeline"
working_dir = "/tmp/workdir"
[run.model]
name = "claude-sonnet-4-5"
fallbacks = ["openai", "gemini"]
[run.model.controls]
reasoning_effort = "high"
[[run.prepare.steps]]
script = "git clone https://github.com/fabro-sh/fabro repo"
[[run.prepare.steps]]
script = "cd repo && npm install"
[run.sandbox]
provider = "daytona"
preserve = false
[run.sandbox.daytona]
auto_stop_interval = 60
[run.sandbox.daytona.labels]
project = "fabro"
env = "ci"
[run.sandbox.daytona.snapshot]
name = "node-20"
cpu = 4
memory = "8GB"
disk = "20GB"
dockerfile = "FROM node:20-slim\nRUN apt-get update && apt-get install -y git"
[run.sandbox.env]
API_KEY = "{{ env.MY_API_KEY }}"
NODE_ENV = "production"
[run.integrations.github.permissions]
contents = "write"
pull_requests = "write"
[run.checkpoint]
exclude_globs = ["**/node_modules/**", "**/.cache/**"]
[run.inputs]
repo_name = "fabro"
repo_url = "https://github.com/fabro-sh/fabro"
[run.artifacts]
include = ["test-results/**", "playwright-report/**"]
[run.agent.mcps.playwright]
type = "sandbox"
command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless"]
port = 3100
[run.pull_request]
enabled = true
draft = false
[[run.hooks]]
id = "pre-check"
event = "stage_start"
script = "./scripts/pre-check.sh"
blocking = true
sandbox = false
[[run.hooks]]
event = "run_complete"
script = "echo done"
```
## Sections
### `[run.model]`
Override the default model and provider for all nodes that don't have an explicit model assigned via a [stylesheet](/workflows/stylesheets).
```toml title="run.toml"
[run.model]
name = "claude-sonnet-4-5"
```
| Field | Description |
|---|---|
| `name` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`). See [Models](/core-concepts/models). |
| `provider` | Provider name (optional — auto-inferred from the model catalog). Only needed for models not in the catalog or to force a specific provider. |
| `fallbacks` | Ordered list of model references to try when the primary is unavailable. Entries can be bare provider tokens (`"openai"`), bare model aliases, or qualified `"provider/model"` references. |
Provider values are catalog provider ID strings. Built-in IDs like `anthropic` and `openai` work, and settings-defined IDs like `proxy` work after they are added under `[llm.providers.<id>]`.
#### `[run.model.controls]`
Set default model controls for all nodes that do not override them in the workflow stylesheet:
```toml title="run.toml"
[run.model]
provider = "proxy"
name = "team-code"
[run.model.controls]
reasoning_effort = "high"
speed = "fast"
```
| Field | Description |
|---|---|
| `reasoning_effort` | Native reasoning-effort value to request when the selected model allows it, such as `"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`. |
| `speed` | Native speed value to request when the selected model declares it, such as `"fast"`. The standard speed is implicit and does not need to be set. |
#### Fallbacks with splice
Use the reserved `"..."` marker in `fallbacks` to splice in the inherited list from lower-precedence layers:
```toml title="run.toml"
[run.model]
# Prepend "anthropic" to whatever fallbacks the project config already defines.
fallbacks = ["anthropic", "..."]
```
### `[run.prepare]`
Ordered list of steps to run before the workflow starts. Use this to clone repositories, install dependencies, or prepare the environment.
```toml title="run.toml"
[[run.prepare.steps]]
script = "pip install -r requirements.txt"
[[run.prepare.steps]]
script = "npm install"
```
| Field | Description |
|---|---|
| `script` | Shell-evaluated command (runs through `sh -c`). |
| `command` | Argv-style command, mutually exclusive with `script`. |
| `env` | Additional environment variables for this step. |
Each step must exit with status 0. If any step fails, the run aborts before the workflow starts. Prepare steps replace across layers — the higher-precedence layer wins wholesale.
### `[run.clone]`
Configure whether clone-based sandboxes clone the run's GitHub origin before execution.
```toml title="run.toml"
[run.clone]
enabled = true
```
Set `enabled = false` to start Docker and Daytona runs with an empty provider workspace. Use [prepare steps](#runprepare) to clone or create any files the workflow needs.
### `[run.run_branch]`
Configure Fabro's managed `fabro/run/<id>` checkpoint branch.
```toml title="run.toml"
[run.run_branch]
enabled = true
push = true
```
| Field | Description |
|---|---|
| `enabled` | When `false`, Fabro does not create the managed run branch or checkpoint commits. This also disables metadata branch writes. |
| `push` | When `false`, Fabro creates local checkpoint commits but does not push `fabro/run/<id>` to the remote. |
### `[run.meta_branch]`
Configure Fabro's managed `fabro/meta/<id>` metadata branch.
```toml title="run.toml"
[run.meta_branch]
enabled = true
push = true
```
| Field | Description |
|---|---|
| `enabled` | When `false`, Fabro skips metadata branch snapshots. |
| `push` | When `false`, Fabro writes metadata snapshots locally but does not push `fabro/meta/<id>` to the remote. |
### `[run.sandbox]`
Configure how agent tools (bash, file edits) are executed.
```toml title="run.toml"
[run.sandbox]
provider = "docker"
preserve = true
```
| Field | Description |
|---|---|
| `provider` | Sandbox mode: `docker` (default), `local`, or `daytona`. |
| `preserve` | When `true`, keep the sandbox alive after the run finishes. Useful for debugging. |
| `devcontainer` | When `true`, use the repo's `devcontainer.json` to configure the sandbox. See [Devcontainers](/execution/devcontainers). |
#### `[run.sandbox.docker]`
Additional settings when using the Docker sandbox:
```toml title="run.toml"
[run.sandbox]
provider = "docker"
[run.sandbox.docker]
image = "buildpack-deps:noble"
network_mode = "bridge"
memory_limit = "4GB"
cpu_quota = 200000
```
| Field | Description |
|---|---|
| `image` | Docker image used for the run container. Defaults to `buildpack-deps:noble`. |
| `network_mode` | Docker network mode. Defaults to `bridge`. |
| `memory_limit` | Memory limit using human-readable units such as `"4GB"` or `"512MiB"`. |
| `cpu_quota` | Docker CPU quota in microseconds per 100ms period. Defaults to `200000`. |
| `env_vars` | Provider-level environment variables passed to the Docker container. For workflow tool execution, prefer `[run.sandbox.env]`. |
#### `[run.sandbox.daytona]`
Additional settings when using the Daytona cloud sandbox:
```toml title="run.toml"
[run.sandbox.daytona]
auto_stop_interval = 60
[run.sandbox.daytona.labels]
project = "fabro"
env = "staging"
[[run.sandbox.daytona.volumes]]
volume_id = "vol-agent-state"
mount_path = "/home/daytona/agent-state"
subpath = "agent-auth"
[run.sandbox.daytona.snapshot]
name = "my-snapshot"
cpu = 4
memory = "8GB"
disk = "20GB"
dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update"
# Or reference an external Dockerfile:
# dockerfile = { path = "./Dockerfile" }
```
| Field | Description |
|---|---|
| `auto_stop_interval` | Minutes of inactivity before the sandbox auto-stops. |
| `labels` | Key-value labels attached to the sandbox for filtering and identification. Labels merge across layers (sticky merge-by-key). |
| `volumes` | Existing Daytona volumes to mount at sandbox creation. Each entry requires `volume_id` and `mount_path`; `subpath` is optional. Fabro does not create or manage volume lifecycle. |
| `snapshot.name` | Snapshot name to create or use for the sandbox. |
| `snapshot.cpu` | CPU cores for the snapshot (integer). |
| `snapshot.memory` | Memory size using human-readable units: `"8GB"`, `"16GiB"`, or bare integers that default to GB. |
| `snapshot.disk` | Disk size using the same units as `memory`. |
| `snapshot.dockerfile` | Dockerfile content (inline string) or path (`{ path = "..." }`) for building the snapshot image. Paths are resolved relative to the TOML file's directory. |
| `network` | Network access mode: `"allow_all"` (default), `"block"`, or `{ allow_list = ["..."] }`. See [Sandboxing](/administration/sandboxing#network-access-control). |
#### Local sandbox
When `run.sandbox.provider = "local"`, Fabro runs directly in the resolved working directory. If you want local isolation, create or enter a separate clone or Git worktree yourself, then run with `--sandbox local`.
#### `[run.sandbox.env]`
Pass environment variables into sandbox command and agent execution. Values can be literal strings or host environment references using `{{ env.VARNAME }}` syntax:
```toml title="run.toml"
[run.sandbox.env]
API_KEY = "{{ env.MY_API_KEY }}"
NODE_ENV = "production"
SERVICE_URL = "https://api.{{ env.REGION }}.example.com"
```
| Syntax | Description |
|---|---|
| `"literal"` | Static value passed as-is |
| `"{{ env.VARNAME }}"` | Whole-value reference resolved from the host environment at consumption time |
| `"prefix-{{ env.X }}-suffix"` | Substring interpolation; multiple tokens per string are supported |
Missing host variables produce a hard error pointing at the specific field and unresolved token. `run.sandbox.env` is a sticky merge-by-key map: entries from all layers combine, with higher-precedence layers overriding individual keys.
### `[run.integrations.github.permissions]`
Request a scoped GitHub App token for workflow stages that need `GITHUB_TOKEN` inside the sandbox. Values map directly to GitHub App permission names and access levels.
```toml title="run.toml"
[run.integrations.github.permissions]
contents = "write"
pull_requests = "write"
issues = "read"
```
Only requested permissions are included. The upper bound is the permission set granted to the installed GitHub App, and Fabro logs a warning and continues without `GITHUB_TOKEN` if the app is not configured or is not installed on the repository.
This table follows the normal settings precedence order. A higher-precedence layer can set `permissions = {}` to clear inherited permissions and run without a GitHub token.
### `[run.checkpoint]`
Configure how git checkpoint commits behave.
```toml title="run.toml"
[run.checkpoint]
exclude_globs = ["**/node_modules/**", "**/.cache/**", "**/dist/**"]
skip_git_hooks = false
```
| Field | Description |
|---|---|
| `exclude_globs` | Glob patterns for files to exclude from checkpoint commits. Uses git pathspec `:(glob,exclude)` syntax. |
| `skip_git_hooks` | When `true`, Fabro-managed run-branch checkpoint commits bypass local Git commit hooks (e.g. `pre-commit`, `commit-msg`). Defaults to `false`. Does not affect Fabro workflow `[[run.hooks]]` or metadata-branch snapshots. |
`exclude_globs` replaces across layers — the higher-precedence layer wins wholesale. `skip_git_hooks` uses normal override semantics: the highest layer that sets it wins.
### `[run.inputs]`
Define inputs that are rendered into final workflow string attributes. See [Variables](/workflows/variables) for the full reference.
```toml title="run.toml"
[run.inputs]
repo_name = "fabro"
repo_url = "https://github.com/fabro-sh/fabro"
language = "rust"
```
Inputs can be used in final string attributes with `{{ inputs.name }}` syntax:
```dot title="c-i.fabro"
digraph CI {
graph [goal="Run tests for {{ inputs.repo_name }}"]
clone [shape=parallelogram, script="git clone {{ inputs.repo_url }} repo"]
test [label="Test", prompt="Run the {{ inputs.language }} test suite."]
}
```
Inputs cannot parameterize workflow structure or file references such as node IDs, edges, `import` paths, `@file` paths, or child workflow paths.
If a workflow template references an undefined input like `{{ inputs.langauge }}`, `fabro validate` reports a warning. Run-style commands promote that diagnostic to an error before creating or starting a run.
TOML `[run.inputs]` tables replace wholesale across layers. Unlike labels, TOML input tables do not merge by key — the highest-precedence config layer that sets `inputs` wins its entire map.
CLI input flags are sparse overrides on top of the resolved config inputs:
```bash
fabro run .fabro/workflows/ci/workflow.toml -I repo_name=fabro-2 --input language=rust
```
Repeat `-I` / `--input` to override multiple keys. CLI input flags have the highest precedence, merge per key, and preserve unrelated inherited inputs. Duplicate CLI keys are accepted; the last value wins.
### `[run.artifacts]`
Configure automatic collection of test artifacts (Playwright reports, JUnit XML, screenshots, etc.) from the execution environment after each stage.
```toml title="run.toml"
[run.artifacts]
include = ["test-results/**", "playwright-report/**", "*.trace.zip"]
```
| Field | Description |
|---|---|
| `include` | Glob patterns for files to collect as assets. Matched against the working directory after each stage completes. |
Artifact collection is opt-in — when no `[run.artifacts]` section is present, no file scanning occurs.
### `[run.agent]`
Configure workflow agent behavior that is not tied to a single stage.
```toml title="run.toml"
[run.agent]
fabro_tools = true
```
`fabro_tools` defaults to `false`. Set it to `true` only for runs whose agents should be able to create, search, inspect, and interact with Fabro runs through the built-in Fabro run tools. This setting is separate from normal agent `permissions` and from MCP server configuration.
### `[run.agent.mcps]`
Configure [MCP servers](/agents/mcp) available to agent stages during the workflow run. Each server is a named TOML table under `[run.agent.mcps]`. All three transport types are supported: `stdio`, `http`, and `sandbox`.
```toml title="run.toml"
[run.agent.mcps.playwright]
type = "sandbox"
command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless", "--browser", "chromium"]
port = 3100
startup_timeout = "60s"
tool_timeout = "2m"
```
| Field | Description | Default |
|---|---|---|
| `type` | Transport type: `"stdio"`, `"http"`, or `"sandbox"`. | — |
| `script` | (stdio, sandbox) Shell-evaluated startup command, mutually exclusive with `command`. | — |
| `command` | (stdio, sandbox) Argv array: executable + arguments. | — |
| `port` | (sandbox) Port the server listens on inside the sandbox. | — |
| `url` | (http) The MCP server endpoint URL. | — |
| `env` | (stdio, sandbox) Additional environment variables. | `{}` |
| `headers` | (http) Optional HTTP headers for authentication. | `{}` |
| `startup_timeout` | Max duration for server startup + MCP handshake (e.g. `"10s"`, `"1m"`). | `"10s"` |
| `tool_timeout` | Max duration for a single tool call. | `"60s"` |
The `sandbox` transport runs the MCP server inside the workflow's sandbox. This is useful for tools that need access to the sandbox environment, such as browser automation with Playwright. See [MCP](/agents/mcp#sandbox) for details.
### `[run.pull_request]`
Automatically open a GitHub pull request when the workflow run completes successfully. Requires a [GitHub App](/integrations/github) to be configured.
```toml title="run.toml"
[run.pull_request]
enabled = true
draft = true
auto_merge = false
merge_strategy = "squash"
```
| Field | Description |
|---|---|
| `enabled` | When `true`, Fabro creates a PR from the agent's working branch after a successful run. Default: `false`. |
| `draft` | When `true`, the PR is created as a draft pull request. Default: `true`. |
| `auto_merge` | When `true`, enables GitHub auto-merge on the created PR. Implies `draft = false` since GitHub doesn't allow auto-merge on draft PRs. The repository must have auto-merge enabled in GitHub settings. Default: `false`. |
| `merge_strategy` | Merge method when `auto_merge` is enabled: `squash` (default), `merge`, or `rebase`. |
### `[[run.hooks]]`
Define hooks that run in response to lifecycle events. Each hook is a TOML array entry:
```toml title="run.toml"
[[run.hooks]]
id = "pre-check"
name = "Pre-check script"
event = "stage_start"
script = "./scripts/pre-check.sh"
matcher = "agent"
blocking = true
timeout = "30s"
sandbox = false
```
| Field | Description |
|---|---|
| `id` | Optional merge identity. Hooks with the same `id` replace each other across layers. |
| `name` | Optional display name for the hook. |
| `event` | Lifecycle event: `run_start`, `run_complete`, `stage_start`, `stage_complete`, etc. |
| `script` | Shell-evaluated command (equivalent to the old `type = "command"` shorthand). |
| `command` | Argv-style command (alternative to `script`). |
| `matcher` | Regex matched against node ID or handler type. Limits which stages trigger this hook. |
| `blocking` | Whether the hook must complete before execution continues. Defaults vary by event. |
| `timeout` | Human-readable hook timeout (e.g. `"30s"`, `"1m"`). Default: `"60s"`. |
| `sandbox` | Run inside the sandbox (`true`, default) or on the host (`false`). |
Hook merge semantics: hooks with matching `id` values replace in place. Hooks without an `id` from a higher-precedence layer append after the fully merged inherited hook list.
See [Hooks](/agents/hooks) for hook types beyond scripts (HTTP, prompt, agent).
## Graph path resolution
The `[workflow].graph` path is resolved relative to the TOML file's parent directory, not the current working directory. This means a run config and its workflow can live side by side:
```
project/
runs/
ci.toml # [workflow] graph = "ci.fabro"
ci.fabro
```
Absolute paths are used as-is.
## Precedence
Settings can come from multiple sources. Fabro resolves them in this order (first match wins):
| Source | Priority |
|---|---|
| Node-level [stylesheet](/workflows/stylesheets) | Highest |
| CLI flags (`--model`, `--provider`, `--sandbox`) | |
| Run config TOML (`workflow.toml` or equivalent) | |
| Project defaults (`.fabro/project.toml`) | |
| Machine defaults (`~/.fabro/settings.toml`) | |
| Graphviz graph attributes (`default_model`, `default_provider`) | |
| Built-in defaults | Lowest |
<Note>
Stylesheet rules on individual nodes always take priority over run config values.
</Note>
### Project defaults (`.fabro/project.toml`)
The `.fabro/project.toml` project config can set default values for any of the `[run.*]` sections described above. These defaults apply to all runs in the project unless the workflow config overrides them:
```toml title=".fabro/project.toml"
_version = 1
[run.model]
name = "claude-sonnet-4-5"
[run.sandbox]
provider = "daytona"
[run.sandbox.daytona.snapshot]
name = "my-project-snapshot"
```
Project defaults and workflow config values merge per the normative merge matrix: most fields merge by field (higher-precedence wins per key), TOML `run.inputs` tables replace wholesale, CLI input flags merge per key at highest precedence, `run.sandbox.env` sticky-merges by key, and `run.prepare.steps` replaces whole-list.
### Machine defaults
When running locally, the machine defaults at `~/.fabro/settings.toml` can set run-scoped defaults too. Same merge rules apply.
## Validation
Fabro validates the run config when it loads:
- **`_version` check** — Only `_version = 1` (or missing, which defaults to `1`) is accepted. The legacy top-level `version` key is rejected with a rename hint.
- **Unknown keys** — Any top-level key not in `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, or `_version` is rejected with a targeted rename hint pointing at the v2 replacement path.
- **Variable check** — Undefined workflow or prompt template variables produce diagnostics. `fabro validate` reports them as warnings; run-style commands treat them as errors before creating or starting a run.
Use `fabro preflight` to validate a run config without executing it:
```bash
fabro preflight run.toml
```