> For the complete documentation index, see [llms.txt](/llms.txt)

# glotto.yml project settings

## Project settings

### `diagnostics`

```yaml
diagnostics:
  rules:
    no-error-response: error          # remap a lint rule's severity (off | warn | error)
    mutation-no-idempotency-key: off  # suppress a rule
  max_warnings: 10                    # release-gate: linting fails above this budget
```

Tunes spec linting without touching the rules engine: `rules` overrides per-rule severity (`off`
suppresses), and `max_warnings` is a release-gating threshold (lint exits non-zero once warnings
exceed it). Optional and additive. See the [Diagnostics reference](/docs/diagnostics-config#tuning-diagnostics)
for the full rule list and behavior.

### `settings`

```yaml
settings:
  detect_breaking_changes: true
```

Stainless-parity toggles. `detect_breaking_changes` makes
[breaking-change detection](/docs/breaking-changes) call out every breaking change at the top of
the preview comment on your pull request, rather than leaving it in a table cell. Optional and
additive.

### `code_owners`

```yaml
code_owners:
  '*':
    - '@acme/sdk-team'
  'src/**':
    - '@acme/core-team'
    - '@octocat'
```

Declares who reviews changes to the repos Glotto manages. Optional and additive — set nothing and
your output is byte-for-byte what it was.

Rules are emitted **in the order you declare them**, because a CODEOWNERS file is resolved
*last-match-wins*: the rule furthest down that matches a path is the one that owns it. So the block
above gives the SDK team everything and hands `src/**` to the core team and `@octocat`.

**Owner syntax is per-provider**, resolved from each target's
[`repo_provider`](/docs/glotto-yml#targets) and defaulting to `github`:

| Provider | Owners it accepts |
|---|---|
| `github` | `@login` (letters, digits and single interior hyphens, ≤39 characters), `@org/team-slug` at **exactly** two segments, or an email address. |
| `gitlab` | The same, plus `_` and `.` inside a segment, and `@group/subgroup/…` nested to any depth. |
| `bitbucket` | A workspace member's handle, account id, uuid, or display name — resolved against your workspace when you apply it. |
| `azure-repos` | An identity's display name, unique name, or email — resolved against your organization when you apply it. |

A GitHub team handle is not a GitLab one, and an owner that a forge cannot resolve makes it ignore
the **whole line** — the path is left unowned and nothing says so. That is why Glotto
rejects an owner the target's provider could not resolve rather than emitting it
([`GLOTTO_CONFIG_CODE_OWNERS`](/docs/diagnostics-config#config-diagnostics)).

**Two of the four providers read no CODEOWNERS file at all.** GitHub and GitLab receive
`.github/CODEOWNERS` and `.gitlab/CODEOWNERS` from `glotto generate`; Bitbucket configures a
repo-wide default-reviewer set and Azure Repos a required-reviewers branch policy, both through
their APIs — so for those two, Glotto applies the block by calling the provider rather than by
writing a file into your repository.

It authenticates with the credentials you connected the repository with; see
[Connect your VCS](/docs/vcs-connect). Bitbucket's default reviewers have **no path scoping**,
so the result names which of your patterns had to be flattened, and any owner whose directory
matched nothing, rather than reporting a plain success.

A target with a [`repo_path`](/docs/glotto-yml#targets) — an SDK living in a subtree of a shared repo — receives no
file either: GitHub and GitLab read CODEOWNERS only from the repository **root**, so one written
into the subtree would never be opened. Every one of these cases is reported by name, by
`glotto generate`, as
[`GLOTTO_CONFIG_CODE_OWNERS_NOT_APPLIED`](/docs/diagnostics-config#config-diagnostics).

### `mcp`

```yaml
mcp:
  package_name: "@acme/api-mcp"   # emitted package identity (default: <org>-mcp; a valid npm name)
  registry_name: io.github.acme/api  # opt-in: emit a server.json for the official MCP registry
  modes: [code, tools, dynamic]  # MCP server modes to enable (omit for all three)
  filters_enabled: true
  search_docs: true
  operations:             # per-tool overrides (canonical <resource>_<method> keys)
    pets_get: { description: "Fetch a single pet by id.", name: fetch_pet }
  permissions:            # method allow/block sets (convenience layer, NOT a security boundary)
    allow_http_gets: true          # allow every operation mapped to HTTP GET
    allowed_methods: [pets\..*]    # regexes over the qualified <resource>.<method> name
    blocked_methods: [pets\.delete]  # applied last — beats both allow keys
  experimental:
    async_tasks: false    # EXPERIMENTAL: task-capable tools for long-running operations (default off)
```

Configuration for the generated multi-mode MCP server. See the [MCP server guide](/docs/mcp-server).
`permissions` narrows which operations the emitted server can reach: patterns match the
fully-qualified `<resource path>.<method>` name (`pets.photos.add`) and are fully anchored, the
allow set is constrained only when `allowed_methods` or `allow_http_gets` is set, and
`blocked_methods` is subtracted last. Denied operations are never registered in Tools/Dynamic Mode,
and Code Mode's `execute` refuses submitted code that references one before running it. It is a
**convenience layer that keeps an agent in its lane, not a security boundary** — the Code Mode half
is static analysis, which obfuscation defeats; use a scoped API token or the MCP Cloud gateway for
the real boundary. See [Method permissions](/docs/mcp-server#method-permissions).
`experimental.async_tasks` opts the operations your spec marks long-running into the MCP
**async tasks** capability — see
[Experimental: async tasks](/docs/mcp-server#experimental-async-tasks). It is experimental and
off by default — and currently **withheld**: the final 2026-07-28 MCP spec moved tasks to the
`io.modelcontextprotocol/tasks` extension, and until the emission is rebuilt for that shape,
enabling the flag over a long-running-capable API fails generation with an actionable error.

### `docs`

```yaml
docs:
  theme: { primary: "#5b21b6" }
  authoring: { format: mdx, base_path: ./docs }
  deploy: { target: cloudflare, custom_domain: docs.acme.com }
  ask_ai: { endpoint: https://ask.acme.com }   # docs "Ask AI" widget
  analytics: { posthog_key: phc_yourkey }      # cookieless analytics (opt-in; PostHog or Plausible)
  search:                                      # rename and order the generated search facets
    facets:
      - { filter: method, label: HTTP method }
      - { filter: resource, label: API resource }
  i18n: { default_locale: en, locales: [en, de] }   # site locales (routing + lang + chrome strings)
  structured_data: {}                          # schema.org JSON-LD on every page (opt-in)
  versions: [ { slug: v2, default: true, openapi: { source: ./v2.yaml } } ]
```

The generated docs site: theme, authoring, deploy target (`cloudflare` / `vercel` / `netlify` /
`static` / `self-host`), the Ask-AI widget, opt-in analytics, locales, structured data, and
multi-version docs.

`search.facets` controls the names and display order of the generated search filter panel. Each
entry's `filter` is one of `method`, `resource`, `language`, or `parameter`; `label` is optional,
and the list order is the display order. A filter you omit keeps its default name and appears after
the configured filters. Omitting `search` preserves the default generated site unchanged.

`structured_data` is off unless you set it (`structured_data: { enabled: false }` turns it back off
explicitly). With it on, every generated page carries one schema.org JSON-LD block derived from your
spec and your configured site identity — an `APIReference` per operation with the same title,
description, and canonical URL its `<head>` already advertises, a `SoftwareSourceCode` per code
sample you show, breadcrumbs matching the page's place in the navigation, and `WebSite` +
`Organization` on the home page. It is re-derived on every regeneration, so it cannot drift from
your spec the way hand-maintained markup does.

For the things your spec cannot know — your organization's `sameAs` profiles, a `HowTo` on a guide,
a locale-prefixed route — add your own nodes:

```yaml
docs:
  structured_data:
    extra_nodes:                      # appended to every page
      - "@type": Organization
        "@id": https://acme.com/#org
        sameAs: ["https://github.com/acme"]
    extra_nodes_by_route:             # appended to one route only
      /guides/quickstart:
        - "@type": HowTo
          name: Quickstart
```

A node is free-form: whatever keys you write are emitted as authored, inside the same single
JSON-LD document, escaped the same way the derived nodes are. What Glotto does check is the node's
**shape** — that it is a non-empty object, that it carries a `@type` a consumer can dispatch on, and
that `@id`/`@context` hold the forms JSON-LD defines for them. A node that fails one of those, or
that claims an `@id` belonging to a node Glotto derived from your spec, is reported by
`glotto generate` ([`GLOTTO_CONFIG_DOCS_STRUCTURED_DATA`](/docs/diagnostics)) and left out; your
other nodes still emit. This is a structural check, not a vocabulary one — a misspelled `@type` is
emitted as you wrote it. The merge is **additive** — your nodes
follow the derived ones and cannot replace, reorder, or remove them, so the graph Glotto guarantees
stays the graph Glotto guarantees. Route keys are matched leniently (`guides/quickstart`,
`/guides/quickstart/` and `/guides/quickstart` are the same route), and a route that isn't a
generated page still gets its nodes, which is how you reach locale-prefixed routes. Set neither key
and your site is byte-identical to the derived-only output; set `enabled: false` and the whole
feature is off, injected nodes included.

`deploy.custom_domain` is the bare hostname — `docs.acme.com`, not a URL, and no path, port, or
wildcard — because it becomes both your canonical URLs' authority and the domain handed to your
deploy provider ([`GLOTTO_CONFIG_DOCS_DEPLOY_DOMAIN`](/docs/diagnostics)). Each `versions[].slug`
becomes a `/<slug>/` route, so it may only use characters a URL path keeps verbatim
([`GLOTTO_CONFIG_DOCS_VERSIONS`](/docs/diagnostics)). Colours under
`og_images.template.colors` are painted by the card renderer rather than a browser, so hex,
comma-separated `rgb()`/`rgba()`, and CSS named colours work but `hsl()` and gradients do not
([`GLOTTO_CONFIG_DOCS_OG_TEMPLATE`](/docs/diagnostics)). `glotto generate` reports each of these.

Each `versions` entry declares its snapshot with **exactly one** of `openapi`, `asyncapi`, or
`graphql` — the same three input keys the top level accepts, in the same shapes, so a GraphQL
snapshot carries its own `operations` document:

```yaml
docs:
  versions:
    - slug: v1
      graphql: { source: ./v1.graphql, operations: ./v1-operations.graphql }
    - slug: v2
      default: true
      openapi: { source: ./v2.yaml }
```

Versions may mix input kinds — the case a migration produces — and each version's reference pages
are built from its own snapshot. Only an `openapi` version co-serves an `openapi.json` (and so
contributes a `service-desc` link to the site's API catalog); the others simply serve none.
Versioning is docs-only: your SDKs always come from the top-level source.
`analytics` activates the site's built-in cookieless analytics against your own project —
PostHog by default (`posthog_key` is the publishable client key; optional `posthog_host` selects
EU cloud or self-hosted ingestion) or Plausible via `provider: plausible` + `plausible_domain`
(optional `plausible_host` for self-hosted). It counts page views plus three product events —
search queries, feedback votes, and playground calls — with no cookies, Do-Not-Track honored,
and nothing sent beyond each event's named fields (never feedback comment text, parameter
values, or credentials). `i18n` declares the
site's locales: `default_locale` sets the `<html lang>` and stays at the unprefixed routes,
additional `locales` get `/<locale>/` route prefixes for your translated pages plus a generated
chrome string catalog to translate (`src/lib/ui-strings.mjs`).

### `license`

```yaml
license: MIT    # or Apache-2.0
```

The permissive license stamped onto generated SDKs and emitted templates. Defaults to `MIT`.

### `release`

```yaml
release:
  mode: auto    # or manual
```

Controls the release flow for generated SDKs. `mode` is the **only** key this block accepts. Any
other is a configuration error reported against your `glotto.yml` before anything is generated, so
a file naming branches here fails at the door rather than being silently ignored.

Absent, releases behave as `auto`: Glotto opens the release pull request for you, and merging it
publishes. `manual` stops Glotto opening that pull request — you open it yourself from the branch
Glotto has already pushed. Publishing on merge is an `auto`-only behaviour: the release webhook
does not act on a repository whose project is in `manual` mode.

**The branch names are not a `glotto.yml` key.** To move one of them, use
[`targets.<lang>.release`](/docs/glotto-yml#targets), which takes `branch`, `baseline_branch` and
`base_branch` per target:

```yaml
targets:
  typescript:
    repo: acme/acme-typescript
    release:
      branch: sdk-next               # the release PR's head — the merged result
      baseline_branch: sdk-generated # the pristine generator output (the merge base)
      base_branch: trunk             # the release PR's base — released code
```

Unset, each defaults to Glotto's
[`generated` → `next` → `main`](/docs/multi-vcs-release) vocabulary.

A **project-level** set of the same three names exists, but it lives in the project's
control-plane configuration (`PUT /v1/projects/:id/config`) rather than in your repository's
`glotto.yml`. Setting them once for every target from `glotto.yml` is not supported yet.

One of those three carries a second meaning worth knowing before you set it: the project-level
`base_branch` is both **the branch Glotto watches on your spec repository** and **the base of the
pull request opened on a split-layout target**. Moving it changes which pushes trigger a release
*and* where the release lands — set it when a repository's trunk is not called `main`, not to
retarget one of the two.

The staging repositories Glotto hosts are not affected by any of these names. Their three
branches are created with the repository and are always `generated`, `next` and `main`; every
build pushes its pristine output there whatever your targets are called.

### `readme`

```yaml
readme:
  example_requests:
    headline: listPets           # operationId fronting the README quickstart
    pagination: listInvoices     # operationId fronting the Pagination section
    streaming: streamEvents      # operationId fronting the Streaming section
    # or, with example-value overrides (wire parameter names):
    # headline:
    #   operation: updatePet
    #   params: { petId: p_42, notify: true }
```

Configures the `README.md` shipped inside each generated SDK — every SDK language target ships
one. `example_requests.headline` selects the operation fronting the README's usage example by
`operationId` — as a plain string, or as `{ operation, params }` where `params` overrides the
example values rendered in the quickstart, keyed by wire parameter name (path and query parameters
and top-level request-body fields). `example_requests.pagination` and `example_requests.streaming`
select, in the same two forms, the operations fronting the README's **Pagination** and
**Streaming** sections (the named operation must actually paginate / stream; a slot's `params`
apply only to its own section's example). Absent slots default to the first matching operation,
with example values sampled from the spec's `example`/`default` fields. `glotto generate` warns
when an operation or a params key doesn't match the spec, or when a `pagination`/`streaming`
operation isn't of that kind. The README's code blocks are the same generated snippets the docs
site renders, so they always match the current client surface.

### `hosted`

```yaml
hosted:
  project: prj_9f2c81
```

Binds the repository to its Glotto control-plane project. Hosted mode resolves the
project as `--project`, then `GLOTTO_PROJECT_ID`, then this committed value — so a checkout with
only the API token in the environment can publish check-results to the hosted run. The project id
is not a secret; credentials (`GLOTTO_API_TOKEN`) and the API URL stay environment-only and have
no config key. Optional and additive, and ignored by generation entirely.
