# Configuration

Forge follows a **zero-config default, full override** philosophy. It works with
no configuration at all — but when you need to change something, everything is
overridable through a single optional file: `.forge/config.yaml`.

::: info One file, all optional
`.forge/config.yaml` is the single optional per-project override file. Every
section and every key in it is optional. Omit what you do not need, and Forge
uses its built-in defaults.
:::

## Resolution order

Forge resolves configuration in four layers. **Later layers win:**

```mermaid
flowchart LR
  A[Built-in defaults] --> B[.forge/config.yaml]
  B --> C[.forge/config.local.yaml]
  C --> D[Environment &amp; flags]
```

1. **Built-in defaults** — baked into the binary.
2. **`.forge/config.yaml`** — per-project overrides, committed with the repo.
3. **`.forge/config.local.yaml`** — per-machine overrides, **gitignored**.
4. **Environment variables and per-invocation CLI flags** — highest precedence.

::: tip See where a value came from
Run `forge config explain` to print the fully resolved configuration with the
**origin layer** annotated for every value — so you always know whether a
setting came from a default, the project file, the local file, or an env var.
:::

## Schema version

`schema_version` is the only **top-level scalar** key — it is not a section. It
declares which version of the config schema the file is written against.

- `schema_version` (int) — current version is `1`. Omit it and Forge assumes
  `1`; there is no need to set it until a future schema revision exists.

```yaml
schema_version: 1
```

## Sections

All sections below are optional.

| Section | Purpose |
| --- | --- |
| `workflow` | TDD, adaptive depth, forced depth, phase overrides |
| `agents` | Which agents are configured and at what scope |
| `repo` | Git-hosting provider, self-hosted hosts, related repos |
| `memory` | Backends for spec memory, codebase memory, team sync |
| `learn` | Continuous-learning ledger: candidate threshold, auto-approve |
| `budget` | Spend ceilings enforced against the per-change budget ledger |
| `persona` | The assistant's voice and language |
| `docsite` | `forge docsite publish` target, output, sections — not `install`/`dev`/`build` |
| `models` | Per-phase model/provider assignments |
| `mcp` | MCP servers to expose |
| `review` | Review gate and provider |
| `guardrails` | Permission posture injected into each agent (`level`); other keys reserved |
| `extensions` | Installed extensions |

### `workflow`

- `strict_tdd` (bool) — enforce tests-first. **Default: `true`.**
- `adaptive_depth` (bool) — let Forge triage per-change depth. **Default: `true`.**
- `force_depth` (`trivial` | `standard` | `full`) — pin depth for all changes.
  **Default: unset** — depth is decided per-change by `adaptive_depth` instead.
- `phases` (list) — override the phase list. **Default: unset** — Forge uses
  its built-in spine (`explore, propose, specify, clarify, plan, tasks,
  implement, verify, archive`).
- `breaker_threshold` (int) — how many **consecutive** `forge verify` failures
  open the per-change circuit breaker. **Default: unset**, which resolves to
  `3`. `forge verify --max-failures <n>` overrides it for one run.
  See [The Circuit Breaker](/harness/circuit-breaker).

```yaml
workflow:
  strict_tdd: true               # default: true
  adaptive_depth: true           # default: true
  force_depth: standard          # default: unset (trivial|standard|full)
  breaker_threshold: 5           # default: unset -> 3
  phases:                        # default: unset -> built-in spine
    - explore
    - propose
    - specify
    - clarify
    - plan
    - tasks
    - implement
    - verify
    - archive
```

### `agents`

- `scope` (`global` | `workspace`) — where agent config is written.
  **Default: `global`.**
- `enabled` (list) — restrict to specific agents. **Default: unset** — every
  detected/installed agent is enabled.

```yaml
agents:
  scope: workspace               # default: global
  enabled:                       # default: unset (all detected agents)
    - claude-code
    - cursor
    - opencode
```

### `repo`

Git-hosting context and related repositories. All optional — with no `repo:`
section, the provider is auto-detected from the git remote. See
[Git Providers & Related Repos](/features/providers).

- `provider` (`gitlab` | `github` | `gitea` | `bitbucket` | `generic`) — force
  the provider, overriding remote-based auto-detection. **Default: unset**
  (auto-detect from the git remote).
- `hosts` (map) — map a self-hosted host to a provider kind, e.g.
  `gitlab.example.com: gitlab`. **Default: unset** (empty map).
- `related` (list) — sibling repos in a multi-repo project; each takes
  `name`, and a `path` (local checkout) or `url`, plus an optional `provider`
  override (else derived from the url/path). **Default: unset** (no related
  repos).

```yaml
repo:
  provider: ""                   # empty = auto-detect from the remote
  hosts:
    gitlab.example.com: gitlab
  related:
    - name: api
      path: ../api
    - name: web
      url: https://github.com/acme/web
```

### `memory`

- `spec` (`engram`) — backend for spec memory. **Default: `engram`.**
- `codebase` (`engram` | `graphify` | `graphiti`) — backend for codebase
  memory. **Default: `engram`.**
- `team_sync` (bool) — enable committable team memory export. **Default:
  unset** — detected as on iff the repo has a git remote.

```yaml
memory:
  spec: engram                   # default: engram
  codebase: engram               # default: engram
  team_sync: true                # default: unset -> detected (on iff repo has a remote)
```

### `learn`

- `threshold` (int) — pending-candidate ceiling used by `forge learn review` /
  `promote` to flag candidates as eligible. **Default: unset** (no ceiling —
  `<= 0` also means no ceiling).
- `auto_approve` (bool) — allow `forge learn promote` to write without an
  explicit `--approve`. **Default: `false`**, so promotion stays human-gated.

```yaml
learn:
  threshold: 20                  # default: unset (no ceiling)
  auto_approve: false            # default: false
```

### `budget`

Ceilings the per-change spend ledger is enforced against. Each dimension is
independent, and each is opt-in: unset (or `<= 0`) means no ceiling for that
dimension — the ledger still accumulates, it just never blocks. The matching
`--max-*` flag on `forge budget record` / `forge budget check` overrides the
configured value for one run.

- `max_tokens` (int) — cap on the change's cumulative reported tokens.
  **Default: unset** (no ceiling).
- `max_usd` (float) — cap on the change's cumulative reported dollar spend.
  **Default: unset** (no ceiling). `forge run` has no flag for this one, so a
  ceiling here is the only way to enforce it inside the run loop.
- `max_tool_calls` (int) — cap on the change's cumulative reported tool
  calls. **Default: unset** (no ceiling).

Reaching exactly a limit is within budget; strictly over it blocks. See
[The Spend Budget](/harness/budget).

```yaml
budget:
  max_tokens: 500000             # default: unset (no ceiling)
  max_usd: 25.00                 # default: unset (no ceiling)
  max_tool_calls: 2000           # default: unset (no ceiling)
```

### `persona`

Selects the standing persona and conversation language.

- `mode` (`forge` | `forge-neutral-artifacts` | `neutral` | `custom`) — the
  persona to apply. Defaults to `neutral`.
- `language` (string) — the conversation language, as a language code.
  Defaults to `en`.

::: warning The key is `mode`, not `id`
There is no `persona.id` key. A `persona.id:` entry is silently ignored by the
resolver — the persona is selected with `persona.mode`.
:::

The persona modes and the replies-vs-artifacts distinction are documented under
[Personas](/guide/components#personas). The `--persona` flag on `forge install`
overrides this for a single run.

```yaml
persona:
  mode: forge
  language: en
```

### `docsite`

Controls **`forge docsite publish`** — rendering forge's `.forge/` state into
a documentation site you already own. Every field is optional; with no
`docsite:` section the generator and output directory are detected from
files already on disk.

::: warning This section is `publish`-only — it has no install-time meaning
`forge docsite install`/`dev`/`build` scaffold and run a *separate*
generator toolchain **inside** `.forge/` itself (a different site from the
one `publish` renders into). Which target is installed lives entirely in
`.forge/.docsite-install.json`, written by `install` — **not** here. Setting
`docsite.target` does not select what `install` scaffolds, and `install`
never reads or writes this section. There are zero new config keys for
`install`/`dev`/`build`: the manifest is the sole authority for what is
installed, and `dev`/`build` always read it, never this config.
:::

- `target` (`auto` | `vitepress` | `docusaurus` | `mintlify` | `starlight` |
  `mkdocs` | `neutral`) — which generator to emit for. **Default: `auto`**,
  which detects the generator from disk and falls back to `neutral`.
- `out` (string) — output directory, relative to the repo root. **Default:
  unset**, so it is derived from detection.
- `pattern` (string) — the information-architecture pattern for the generated
  site. **Default: `auto`**, which picks one from the project type.
- `sections` (list) — top-level sections to generate, in order. **Default:
  `[guide, reference, decisions]`.**
- `scaffold` (bool) — let `publish` write the generator's config file when it
  is missing. **Default: `false`**, and never applies under `target: auto`.
  This gate is `publish`-only: `install` always scaffolds its own template
  set unconditionally, regardless of this key — running `install` is itself
  the explicit consent this gate exists to require of `publish`.
- `llms_txt` (bool) — also emit `/llms.txt` alongside the site. **Default:
  `true`.**
- `auto_on_archive` (bool) — regenerate the site during the archive phase.
  **Default: `false`.**
- `exclude` (list) — glob patterns kept out of the source scan. **Default:
  `["**/*_test.go", "vendor/**", "node_modules/**"]`.**

```yaml
docsite:
  target: vitepress               # default: auto
  out: docs                       # default: unset (derived from detection)
  pattern: auto                   # default: auto
  sections:                       # default: [guide, reference, decisions]
    - guide
    - reference
    - decisions
  scaffold: true                  # default: false
  llms_txt: true                  # default: true
  auto_on_archive: false          # default: false
  exclude:                        # default: ["**/*_test.go", "vendor/**", "node_modules/**"]
    - "**/*_test.go"
    - "vendor/**"
    - "node_modules/**"
```

### `models`

A per-phase map assigning a provider/model to phases such as `explore`,
`design`, and `implement`. **Default: unset** (empty map) — each phase uses
the coding agent's own default model unless a phase key here overrides it.

```yaml
models:                           # default: unset (empty map)
  explore: anthropic/claude-opus
  design: anthropic/claude-opus
  implement: anthropic/claude-sonnet
```

### `mcp`

- `servers` (list) — MCP servers to expose. **Default: `[context7]`.**

```yaml
mcp:
  servers:                        # default: [context7]
    - context7
    - filesystem
```

### `review`

- `gate` (`off` | `pre-commit` | `pre-push` | `ci`) — when review runs. Any
  other value is treated as `off`, so the gate fails closed rather than
  activating an unknown mode. The init wizard's **Review Gate** step offers
  exactly these four modes. **Default: `off`.**
- `provider` — the review provider. **Default: unset.**

```yaml
review:
  gate: pre-push                  # default: off (off|pre-commit|pre-push|ci)
  provider: anthropic             # default: unset
```

### `guardrails`

::: info What `guardrails` controls today
`guardrails` is carried through the resolver untyped (an arbitrary key/value
map), and one key in it **is** enforced: the guarded-vs-bypass posture that
Forge injects into each agent's own permission config during install and sync.
Every other key in this section is still reserved for a later phase.
:::

- `level` (`off` | `guarded` | `strict` | `bypass`) — the permission posture.
  This is the key the `forge init` wizard writes. **Default: unset**, which
  resolves to the guarded posture.
- `default_mode` (`guarded` | `bypass`) — an accepted alias for `level`, so a
  file written against the published PRD schema works unchanged. **Default:
  unset.**

Only the value `bypass` changes behavior, and only when a config layer sets it
**explicitly** — it is never selected automatically. Every other value,
including an absent `guardrails` section, resolves to the guarded fail-safe
posture:

- **Guarded (default)** — each agent is asked to confirm tool calls: Claude
  Code gets `defaultMode: default`, OpenCode/Kilocode get `bash: "*": ask`, and
  Codex gets `approval_policy: on-request`.
- **Bypass** — restores auto-accept (Claude Code `bypassPermissions`, Codex
  `approval_policy: never`) and, on Claude Code, installs a blocking
  `PreToolUse` hook for destructive git commands as the substitute for the
  confirmation that auto-accept removes.

The secrets deny-list overlay is injected in **both** postures. Other keys —
`posture`, `secrets.deny`, `destructive_actions` — are accepted and carried
through but not read; they illustrate the intended future shape only.

The wider guardrails story (the deny-list contents, the destructive-action
hook, and `forge secscan`) is documented under
[The Guardrails Gate](/harness/guardrails).

```yaml
guardrails:
  level: guarded                  # default: unset -> guarded
  secrets:
    deny:
      - AWS_SECRET_ACCESS_KEY
      - STRIPE_SECRET_KEY
```

### `extensions`

- A list of `{ id, version }` entries for installed extensions. **Default:
  unset** (empty list — no extensions installed).

```yaml
extensions:                       # default: unset (empty list)
  - id: forge-graphql
    version: 1.2.0
  - id: forge-terraform
    version: 0.4.1
```

## Complete example

The following `config.yaml` shows **non-default** values across every section.
Copy it as a starting point and delete anything you do not need — remember that
every key is optional.

```yaml
# .forge/config.yaml — every section and key is optional.

schema_version: 1               # config schema version (current: 1)

workflow:
  strict_tdd: true              # tests-first (default: true)
  adaptive_depth: true          # triage depth per change (default: true)
  force_depth: standard         # pin all changes to a depth: trivial|standard|full
  breaker_threshold: 5          # consecutive verify failures that open the breaker (unset = 3)
  phases:                       # override the phase list (advanced)
    - explore
    - propose
    - specify
    - clarify
    - plan
    - tasks
    - implement
    - verify
    - archive

agents:
  scope: workspace              # global|workspace (default: global)
  enabled:                      # restrict to specific agents
    - claude-code
    - cursor
    - opencode

repo:                           # omit entirely to auto-detect from the remote
  provider: gitlab              # gitlab|github|gitea|bitbucket|generic
  hosts:                        # map self-hosted hosts to a provider kind
    gitlab.example.com: gitlab
  related:                      # sibling repos in a multi-repo project
    - name: api
      path: ../api
    - name: web
      url: https://github.com/acme/web

memory:
  spec: engram                  # spec memory backend
  codebase: graphify            # engram|graphify|graphiti
  team_sync: true               # export committable team memory

learn:
  threshold: 20                 # pending-candidate ceiling (unset or <=0 = none)
  auto_approve: false           # keep `forge learn promote` human-gated (default)

budget:                         # per-change spend ceilings (unset or <=0 = none)
  max_tokens: 500000            # token ceiling
  max_usd: 25.00                # dollar ceiling
  max_tool_calls: 2000          # tool-call ceiling

persona:
  mode: forge                   # persona mode — see /guide/components#personas
  language: en

docsite:                        # `forge docsite publish` only — install/dev/build read
                                 # .forge/.docsite-install.json instead, never this section
  target: vitepress             # auto|vitepress|docusaurus|mintlify|starlight|mkdocs|neutral
  out: docs                     # output dir (default: derived from detection)
  pattern: auto                 # information-architecture pattern
  sections:                     # default: [guide, reference, decisions]
    - guide
    - reference
    - decisions
  scaffold: true                # write the generator config if missing (default: false)
  llms_txt: true                # also emit /llms.txt (default: true)
  auto_on_archive: false        # regenerate during archive (default: false)
  exclude:                      # kept out of the source scan
    - "**/*_test.go"
    - "vendor/**"
    - "node_modules/**"

models:                         # per-phase provider/model assignments
  explore: anthropic/claude-opus
  design: anthropic/claude-opus
  implement: anthropic/claude-sonnet

mcp:
  servers:                      # default: [context7]
    - context7
    - filesystem

review:
  gate: pre-push                # off|pre-commit|pre-push|ci
  provider: anthropic

guardrails:
  level: guarded                # off|guarded|strict|bypass; only bypass changes
                                # the permissions injected into each agent
  posture: ask                  # reserved: carried through, not read today
  secrets:
    deny:
      - AWS_SECRET_ACCESS_KEY
      - STRIPE_SECRET_KEY
  destructive_actions: ask      # reserved: carried through, not read today

extensions:
  - id: forge-graphql
    version: 1.2.0
  - id: forge-terraform
    version: 0.4.1
```

::: warning Local overrides are per-machine
`.forge/config.local.yaml` is gitignored on purpose. Use it for machine-specific
settings (a local model endpoint, a personal provider key selection) that should
never be shared with the team. Anything meant for the whole team belongs in
`.forge/config.yaml`.
:::

## Environment variables

Select settings can be overridden at the highest precedence for CI or one-off
runs — with a **per-invocation CLI flag** or a **`FORGE_*` environment
variable** — without editing any file. Because these win over every file layer,
they are ideal for temporarily forcing behavior in a pipeline. For example:

```sh
forge install --scope workspace           # override install scope for one run
forge install --channel beta              # override release channel for one run
FORGE_ACTOR="ci-bot" forge trace show --change c1   # attribute events in CI
FORGE_BUDGET_MAX_TOKENS=200000 forge run --change c1  # tighten the spend ceiling for one pipeline
```

Any two-level config key maps to a `FORGE_*` variable by upper-casing the path
and replacing the dot with an underscore: `workflow.breaker_threshold` becomes
`FORGE_WORKFLOW_BREAKER_THRESHOLD`, `budget.max_usd` becomes
`FORGE_BUDGET_MAX_USD`.

The `FORGE_INIT_*` variables (`FORGE_INIT_AGENTS`, `FORGE_INIT_PRESET`,
`FORGE_INIT_SCOPE`, `FORGE_INIT_YES`, …) drive `forge init` headlessly. For the
full flag-and-environment surface, see the [CLI reference](/reference/cli).

## Next steps

- [The Workflow](/guide/workflow) — what the `workflow` section controls.
- [Agents](/guide/agents) — the agents the `agents` section enables.
- [Commands](/guide/commands) — including `config explain`.
- [CLI reference](/reference/cli) — every flag and its environment-variable equivalent.
- [Best Practices](/guide/best-practices) — recommended settings.
