Configuration

repomatic reads two kinds of pyproject.toml configuration. Its own settings live in [tool.repomatic], documented below. The third-party tools it runs are configured through their own standard [tool.*] sections ([tool.ruff], [tool.mypy], [tool.typos], [tool.nuitka], and so on); repomatic discovers and resolves these through its tool runner, where they are documented.

[tool.repomatic] configuration

Downstream projects can customize workflow behavior by adding a [tool.repomatic] section in their pyproject.toml. These options control the defaults for the corresponding CLI commands.

The [tool.repomatic] section is powered by Click Extra’s pyproject.toml configuration. Click Extra handles CWD-first discovery (walking up to the VCS root), key normalization (kebab-case to snake_case), and typed dataclass schemas (nested sub-tables, opaque dict fields, strict validation).

[tool.repomatic]
pypi-package-history = ["old-name", "older-name"]

awesome-template.sync = false
binaries.sync = false
bumpversion.sync = false
cache.max-age = 14
dep-sources.sync = false
dev-release.sync = false
gitignore.sync = false
labels.sync = false
mailmap.sync = false
setup-guide = false
sphinx.builder = "dirhtml"
site.deploy = "cloudflare-pages"
site.cloudflare-project = "my-legacy-project-name"
site.cloudflare-compatibility-date = "2026-06-16"
site.cloudflare-placement = "smart"
uv-lock.sync = false

dependency-graph.output = "./docs/assets/dependencies.mmd"
dependency-graph.all-groups = true
dependency-graph.all-extras = true
dependency-graph.no-groups = []
dependency-graph.no-extras = []
dependency-graph.level = 0

metrics.sync = true
metrics.store = "./docs/assets/metrics.csv"

gitignore.location = "./.gitignore"
gitignore.extra-categories = ["terraform", "go"]
gitignore.extra-content = '''
# Claude Code
.claude/
'''

exclude = ["skills", "workflows/debug.yaml", "zizmor"]

flavor.agent = "claude_code"
flavor.ci = "github_ci"

labels.extra-files = ["https://example.com/my-labels.toml"]

nuitka.dev-targets = ["linux-arm64", "windows-x64"]
nuitka.enabled = false
nuitka.entry-points = ["mpm"]
nuitka.nofollow-imports = []
nuitka.unstable-targets = ["linux-arm64", "windows-arm64"]

workflow.sync = false
workflow.source-paths = ["extra_platforms"]
workflow.extra-paths = ["install.sh", "dotfiles/**"]
workflow.ignore-paths = ["uv.lock"]

[tool.repomatic.labels.file-rules]
"📚 docs" = ["docs/**", "!docs/generated/**"]

[tool.repomatic.labels.content-rules]
"🛡️ security" = ["CVE", "vulnerability"]

[tool.repomatic.workflow.paths]
"tests.yaml" = ["install.sh", "packages.toml", ".github/workflows/tests.yaml"]

abandoned-versions

Versions documented in the changelog but never published.

Type: list[str] | Default: []

A version reached only its [changelog] Release vX.Y.Z freeze and was then skipped per CLAUDE.md § Skip and move forward (botched build, broken artifact, bad metadata) without rewriting history. List those versions here so lint-changelog reports them as skipped (an info log line) instead of flagging them every run as X.Y.Z: not found on PyPI. Applies to both PyPI lookups and the git-tag fallback.

Example:

[tool.repomatic]
abandoned-versions = []

action-pins.sync

Whether the sync-action-pins job is enabled for this project.

Type: bool | Default: true

Bumps SHA-pinned GitHub Actions (uses: owner/repo@<sha> # vX.Y.Z) to the latest release passing the minimum-release-age cooldown. Projects that pin actions by hand can set this to false.

Example:

[tool.repomatic]
action-pins.sync = true

agent.location

Path to the agent’s instructions file, relative to the repository root.

Type: str | Default: "./claude.md"

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Only the agent component writes here, merging the audience-tagged sections it owns into whatever the file already holds. Point it at AGENTS.md for the cross-agent convention, or anywhere else the file actually lives: a repository keeping its instructions outside the root (./dotfiles/.agents/AGENTS.md) is the case this exists for.

Caution

One character from subagents_location, and they write different things: this one an instructions document, that one a directory of subagent definitions. The components are agent and subagents for the same reason.

Example:

[tool.repomatic]
agent.location = "./claude.md"

awesome-template.sync

Whether awesome-template sync is enabled for this project.

Type: bool | Default: true

Repositories whose name starts with awesome- get their boilerplate synced from files bundled in repomatic. Set to false to opt out.

Example:

[tool.repomatic]
awesome-template.sync = true

binaries.sync

Whether the release pipeline records released binaries into the repository.

Type: bool | Default: true

When enabled, the scan-virustotal release job regenerates the binaries catalog (docs/binaries.md and docs/assets/binaries.csv) and pushes it, along with the scan history (docs/assets/virustotal-scans.csv), straight to the default branch without a pull request: the release-lane exception documented in docs/operation-contracts.md. Set to false to keep the repository untouched: binaries are still scanned on VirusTotal (seeding AV vendor databases), but no catalog page, CSV, or scan record is committed.

Example:

[tool.repomatic]
binaries.sync = true

bumpversion.sync

Whether bumpversion config sync is enabled for this project.

Type: bool | Default: true

Projects that manage their own [tool.bumpversion] section and do not want the autofix job to overwrite it can set this to false.

Example:

[tool.repomatic]
bumpversion.sync = true

cache.dir

Override the binary cache directory path.

Type: str | Default: ""

When empty (the default), the cache uses the platform convention: ~/Library/Caches/repomatic on macOS, $XDG_CACHE_HOME/repomatic or ~/.cache/repomatic on Linux, %LOCALAPPDATA%\repomatic\Cache on Windows. The REPOMATIC_CACHE_DIR environment variable takes precedence over this setting.

Example:

[tool.repomatic]
cache.dir = ""

cache.github-release-ttl

Freshness TTL for cached single-release bodies (seconds).

Type: int | Default: 604800

GitHub release bodies are immutable once published, so a long TTL (7 days) is safe. Set to 0 to disable caching for single-release lookups.

Example:

[tool.repomatic]
cache.github-release-ttl = 604800

cache.github-releases-ttl

Freshness TTL for cached all-releases responses (seconds).

Type: int | Default: 86400

New releases can appear at any time, so a shorter TTL (24 hours) balances freshness with API savings.

Example:

[tool.repomatic]
cache.github-releases-ttl = 86400

cache.max-age

Auto-purge cached entries older than this many days.

Type: int | Default: 30

Set to 0 to disable auto-purge. The REPOMATIC_CACHE_MAX_AGE environment variable takes precedence over this setting.

Example:

[tool.repomatic]
cache.max-age = 30

cache.npm-ttl

Freshness TTL for cached npm registry metadata (seconds).

Type: int | Default: 86400

New npm versions can appear at any time, so a 24-hour TTL balances freshness with request savings. Set to 0 to disable caching for npm lookups.

Example:

[tool.repomatic]
cache.npm-ttl = 86400

cache.pypi-ttl

Freshness TTL for cached PyPI metadata (seconds).

Type: int | Default: 86400

PyPI metadata changes when new versions are published. A 24-hour TTL avoids redundant API calls while keeping data reasonably current.

Example:

[tool.repomatic]
cache.pypi-ttl = 86400

changelog.archive-location

File path of the changelog archive, relative to the root of the repository.

Type: str | Default: ""

The archive holds older release sections split out of the live changelog to keep it small. Empty (the default) disables archive handling.

When set, lint-changelog treats versions documented in the archive as present, so they are neither reported nor re-inserted as orphans (versions found on PyPI, GitHub, or git tags but missing from the changelog). The archive is frozen: its released entries are immutable and are not re-validated against their canonical release dates.

Example:

[tool.repomatic]
changelog.archive-location = ""

changelog.bullet-word-threshold

Word count above which lint-changelog warns about a changelog bullet.

Type: int | Default: 40

A changelog entry is a release note, not a commit message: ideally one short sentence stating what changed (see CLAUDE.md § Changelog entry length). lint-changelog emits a non-fatal warning for every bullet in the unreleased section longer than this many words, nudging verbose, implementation-heavy entries back toward a user-facing summary. Released sections are immutable and never flagged. Set to 0 to disable the check.

Example:

[tool.repomatic]
changelog.bullet-word-threshold = 40

changelog.location

File path of the changelog, relative to the root of the repository.

Type: str | Default: "./changelog.md"

Example:

[tool.repomatic]
changelog.location = "./changelog.md"

dep-sources.sync

Whether the sync-dep-sources updater is enabled for this project.

Type: bool | Default: true

Swaps a dependency tracked from a git branch back to its released version once the release named by its .dev version floor ships on PyPI (see repomatic.dep_sources for the managed idiom). Projects that manage [tool.uv.sources] overrides by hand can set this to false.

Example:

[tool.repomatic]
dep-sources.sync = true

dependency-graph.all-extras

Whether to include all optional extras in the graph.

Type: bool | Default: true

When True, the update-dep-graph command behaves as if --all-extras was passed.

Example:

[tool.repomatic]
dependency-graph.all-extras = true

dependency-graph.all-groups

Whether to include all dependency groups in the graph.

Type: bool | Default: true

When True, the update-dep-graph command behaves as if --all-groups was passed. Projects that want to exclude development dependency groups (docs, test, typing) from their published graph can set this to false.

Example:

[tool.repomatic]
dependency-graph.all-groups = true

dependency-graph.level

Maximum depth of the dependency graph.

Type: int | Default: (none)

None means unlimited. 1 = directly-declared deps only, 2 = adds their deps, etc. Equivalent to --level.

dependency-graph.no-extras

Optional extras to exclude from the graph.

Type: list[str] | Default: []

Equivalent to passing --no-extra for each entry. Takes precedence over dependency-graph.all-extras.

Example:

[tool.repomatic]
dependency-graph.no-extras = []

dependency-graph.no-groups

Dependency groups to exclude from the graph.

Type: list[str] | Default: []

Equivalent to passing --no-group for each entry. Takes precedence over dependency-graph.all-groups.

Example:

[tool.repomatic]
dependency-graph.no-groups = []

dependency-graph.output

Path where the dependency graph Mermaid diagram should be written.

Type: str | Default: "./docs/assets/dependencies.mmd"

The dependency graph visualizes the project’s dependency tree in Mermaid format.

Example:

[tool.repomatic]
dependency-graph.output = "./docs/assets/dependencies.mmd"

dev-release.sync

Whether dev pre-release sync is enabled for this project.

Type: bool | Default: true

Projects that do not want a rolling draft pre-release maintained on GitHub can set this to false.

Example:

[tool.repomatic]
dev-release.sync = true

docs.apidoc-exclude

Glob patterns for modules to exclude from sphinx-apidoc.

Type: list[str] | Default: []

Passed as positional exclude arguments after the source directory (e.g., ["setup.py", "tests"]).

Example:

[tool.repomatic]
docs.apidoc-exclude = []

docs.apidoc-extra-args

Extra arguments appended to the sphinx-apidoc invocation.

Type: list[str] | Default: []

The base flags --no-toc --module-first are always applied. Use this for project-specific options (e.g., ["--implicit-namespaces"]).

Example:

[tool.repomatic]
docs.apidoc-extra-args = []

docs.update-script

Path to a Python script run after sphinx-apidoc to generate dynamic content.

Type: str | Default: "./docs/docs_update.py"

Resolved relative to the repository root. Must reside under the docs/ directory for security. Set to an empty string to disable.

Example:

[tool.repomatic]
docs.update-script = "./docs/docs_update.py"

exclude

Additional components and files to exclude from repomatic operations.

Type: list[str] | Default: []

Additive to the default exclusions (agents, labels, skills). Bare names exclude an entire component (e.g., "workflows"). Qualified component/identifier entries exclude a specific file within a component (e.g., "workflows/debug.yaml", "skills/repomatic-audit", "labels/labels.toml").

Affects repomatic init, workflow sync, and workflow create. Explicit CLI positional arguments override this list.

Example:

[tool.repomatic]
exclude = []

flavor.agent

AI coding agent whose asset layout the bundled skills and agents target.

Type: str | Default: "claude_code"

Accepts a extra_platforms.ALL_AGENTS trait ID present in AGENT_LAYOUTS. Hyphens are normalized, so claude-code works too.

Example:

[tool.repomatic]
flavor.agent = "claude_code"

flavor.ci

CI system the bundled workflows target.

Type: str | Default: "github_ci"

Accepts a extra_platforms.ALL_CI trait ID. Only github_ci is implemented: every bundled workflow is a GitHub Actions workflow, so any other value is rejected rather than quietly emitting the wrong thing.

Example:

[tool.repomatic]
flavor.ci = "github_ci"

gitignore.extra-categories

Additional gitignore template categories to fetch from gitignore.io.

Type: list[str] | Default: []

List of template names (e.g., ["Python", "Node", "Terraform"]) to combine with the generated .gitignore content.

Example:

[tool.repomatic]
gitignore.extra-categories = []

gitignore.extra-content

Content appended at the end of the generated .gitignore file.

Type: str | Default: (see example)

“Appended” describes where the string lands, after the gitignore.io block, not how a downstream value combines with the default above: setting this key replaces that default wholesale, so the entries shown there are lost unless the override repeats them. repomatic.gitignore.orphaned_rules() catches that for any rule an earlier sync already wrote to disk, but not for one this repository never materialized, so copy the default and extend it rather than writing only the new lines. Reach for extra_categories instead when adding whole gitignore.io templates: that one is additive.

The .cc-writes entry is the one carrying a **/ prefix, because it is the one Claude Code does not place at the repository root: the directory is staged beside whichever working directory the session tracks, so a single cd into a subtree leaves one there instead. Anchoring it would miss every copy but the root’s.

Example:

[tool.repomatic]
gitignore.extra-content = '''
# Claude Code local files.
.claude/scheduled_tasks.lock
.claude/settings.local.json
**/.claude/.cc-writes/

# Sphinx linkcheck output.
docs/_linkcheck/
'''

gitignore.location

File path of the .gitignore to update, relative to the root of the repository.

Type: str | Default: "./.gitignore"

Example:

[tool.repomatic]
gitignore.location = "./.gitignore"

gitignore.sync

Whether .gitignore sync is enabled for this project.

Type: bool | Default: true

Projects that manage their own .gitignore and do not want the autofix job to overwrite it can set this to false.

Example:

[tool.repomatic]
gitignore.sync = true

include

Components and files to force-include, overriding default exclusions.

Type: list[str] | Default: []

Use this to opt into components that are excluded by default (agents, labels, skills). Each entry is subtracted from the effective exclude set (defaults + user exclude) and bypasses RepoScope filtering, so scope-restricted components (like awesome-only skills or Python-only publish-pypi-action) are included regardless of repository type. Qualified entries (component/file) implicitly select the parent component. Same syntax as exclude.

Example:

[tool.repomatic]
include = []

labels.content-rules

Per-label patterns matched against an issue or pull request’s text.

Type: dict[str, list[str]] | Default: {}

The [tool.repomatic.labels.content-rules] table maps each label to the patterns that apply it, evaluated by apply-labels against the title and body. Any one pattern matching applies the label:

[tool.repomatic.labels.content-rules]
"🥭 mango" = ["mango", "papaya"]
"🐛 bug" = []

A bare pattern is a literal keyword, matched case-insensitively on word boundaries; the /regex/flags form passes a regex through instead, with i, m and s honored. An entry for a label the bundled defaults also carry replaces the default entry, and an empty list disables it (see repomatic.labels.DEFAULT_CONTENT_RULES).

labels.extra

Inline label definitions applied at sync time under the default profile.

Type: list[dict[str, str | bool | list[str]]] | Default: []

Each entry is a mapping carrying labelmaker’s per-label specification: name (required), color (single color or multi-color list), description, create, update, enforce-case, rename-from and on-rename-clash. A rename-from list renames an existing label in place, preserving its issue and PR associations. Entries are serialized into a temporary TOML file as [[profiles.default.labels]] blocks and applied by labelmaker apply, so no extra-labels/*.toml file needs committing.

For label sets that need multiple profiles, commit a hand-written file under extra-labels/ or download one via extra-files instead.

Example:

[tool.repomatic]
labels.extra = []

labels.extra-files

URLs of additional label definition files (JSON, JSON5, TOML, or YAML).

Type: list[str] | Default: []

Each URL is downloaded into extra-labels/ and applied separately by labelmaker. For inline definitions that need no external file, use extra instead.

Example:

[tool.repomatic]
labels.extra-files = []

labels.file-rules

Per-label globs matched against the paths a pull request changes.

Type: dict[str, list[str]] | Default: {}

The [tool.repomatic.labels.file-rules] table maps each label to the globs that apply it, evaluated by apply-labels against the changed files. The label applies when any changed file matches the glob set:

[tool.repomatic.labels.file-rules]
"🥭 mango" = ["orchard/**", "!orchard/generated/**"]

Globs follow the minimatch dialect (** crosses directories, {a,b} expands, a leading dot needs no special casing), and a !-prefixed entry subtracts from the label’s other globs the way a .gitignore line would. An entry for a label the bundled defaults also carry replaces the default entry, and an empty list disables it (see repomatic.labels.DEFAULT_FILE_RULES).

labels.sync

Whether label sync is enabled for this project.

Type: bool | Default: true

Projects that manage their own repository labels and do not want the labels workflow to overwrite them can set this to false.

Example:

[tool.repomatic]
labels.sync = true

lint-deps.allow

Packages that may ship from somewhere other than PyPI, and why.

Type: dict[str, str] | Default: {}

lint-deps blocks a release whose dependencies do not all resolve from the index its users will install from. A handful of arrangements are legitimate exceptions: a member of the same monorepo published under its own name, a private mirror an internal project genuinely targets. Name each one here, mapped to the reason it is safe:

[tool.repomatic]
lint-deps.allow = { papaya = "monorepo workspace member, published separately" }

A mapping rather than a list, deliberately: the reason is the point. An exemption without one is indistinguishable from a forgotten development shortcut six months later, which is the exact thing this gate exists to catch. The reason renders in the report and in the release PR banner, so an accepted exception stays visible instead of disappearing.

Per-package only, with no global off switch, following exclude-newer-package: an exemption narrow enough to name is one somebody weighed. Listing a package does not silence its transitive dependencies, which stay gated on their own.

lint-deps.comment-word-threshold

Word count above which lint-deps warns about a floor comment.

Type: int | Default: 40

A floor comment justifies the version in force: what breaks below it, and where the project would notice. It is not a running log of every earlier floor, which is what it turns into when each bump appends a paragraph and deletes nothing. lint-deps emits a non-fatal warning for every comment longer than this many words. Set to 0 to disable the check.

It starts at the same 40 words as changelog.bullet-word-threshold, and stays an independent knob: both cap a paragraph written for a reader who came looking for one fact, but a project that wants its floors terser than its release notes says so here alone.

Example:

[tool.repomatic]
lint-deps.comment-word-threshold = 40

mailmap.sync

Whether .mailmap sync is enabled for this project.

Type: bool | Default: true

Projects that manage their own .mailmap and do not want the autofix job to overwrite it can set this to false.

Example:

[tool.repomatic]
mailmap.sync = true

manpages.asset-name

Filename stem (without the .tar.gz extension) for the man-page tarball uploaded to the GitHub release.

Type: str | Default: ""

Defaults to <package-name>-manpages when left empty and manpages.script is set. Has no effect when manpages.script is empty.

Example:

[tool.repomatic]
manpages.asset-name = ""

manpages.script

Click command target whose tree gets rendered as roff .1 files and attached as a tarball asset on every GitHub release.

Type: str | Default: ""

Same shape the click-extra wrap --man CLI accepts: a module:function path (preferred for projects whose console-script entry point dispatches through a wrapper), an entry-point name, a .py file path, or a plain importable module name. Leave empty to disable release-attached man pages.

Example:

[tool.repomatic]
manpages.script = ""

metrics.charts

Charts to draw from the accumulated history, one array-of-tables entry each.

Type: list[dict[str, str | list[str]]] | Default: []

Each entry carries an output path, an optional metric (stars by default, and only a metric the store accrues can be charted), an optional mode (absolute, the default, or relative) measuring the horizontal axis, an optional scale (linear, the default, or logarithmic) measuring the vertical one, an optional only list naming the subjects to plot in draw order, and an optional title used as the chart’s accessible name:

[[tool.repomatic.metrics.charts]]
output = "./docs/assets/star-history.svg"

[[tool.repomatic.metrics.charts]]
mode = "relative"
output = "./docs/assets/star-history-by-age.svg"

[[tool.repomatic.metrics.charts]]
only = [ "apricot" ]
output = "./docs/assets/star-history-apricot.svg"

[[tool.repomatic.metrics.charts]]
scale = "logarithmic"
output = "./docs/assets/star-history-compared.svg"

The two axes are independent, and a chart comparing projects of different sizes usually wants both: mode = "relative" slides every curve onto a common origin, and scale = "logarithmic" keeps the smallest of them off the axis.

An entry omitting only plots every declared subject. Declaring none of these leaves the history accruing with nothing drawn from it, which is a valid way to collect first and decide later.

Example:

[tool.repomatic]
metrics.charts = []

metrics.colors

Per-subject [light, dark] hex pairs overriding the positional palette.

Type: dict[str, list[str]] | Default: {}

Hues are assigned from repomatic.metric_chart.SERIES_PALETTE in draw order, so a subject keeps its colour as long as the order holds. Pin one here when it must survive a reordering, or when a chart plots more curves than the palette holds:

[tool.repomatic.metrics.colors]
apricot = [ "#2a78d6", "#3987e5" ]

metrics.forges

Self-hosted forge instances, mapping each host to the software it runs.

Type: dict[str, str] | Default: {}

Merged over repomatic.forge.FORGE_APIS, which only knows the three public hosts. A self-hosted instance is never guessed from its name, so an undeclared host raises rather than sampling nothing:

[tool.repomatic.metrics.forges]
"gitlab.example.org" = "gitlab"
"codeberg.example.org" = "forgejo"

Values are forgejo, github or gitlab; Gitea instances read as forgejo, whose API they share.

metrics.predecessors

Retired forerunners, mapping the subject they precede to their own repository.

Type: dict[str, str] | Default: {}

A project that reopened under a new repository carries an audience it inherited rather than one it gathered, which a by-age chart would otherwise misreport as the fastest start in the field:

[tool.repomatic.metrics.predecessors]
papaya = "old-owner/papaya"

Drawn in the successor’s own hue to tie the two together, but dashed and never joined to it: the counts are independent tallies on separate repositories, so a continuous line would claim a running total no repository ever showed. The forerunner’s line stops where its successor’s begins.

metrics.skip

Subjects deliberately left unmeasured, mapped to the reason why.

Type: dict[str, str] | Default: {}

A mapping rather than a list, following lint-deps.allow: the reason is the point. A project absent from both tables is an oversight a conformance test can report, while one listed here is a decision:

[tool.repomatic.metrics.skip]
papaya = "Ships in a distribution package with no public repository."

Nothing is sampled for them, and whatever renders the readings leaves their cells empty.

metrics.store

Where the readings accumulate, one row per subject, metric and date.

Type: str | Default: "./docs/assets/metrics.csv"

Example:

[tool.repomatic]
metrics.store = "./docs/assets/metrics.csv"

metrics.subjects

Repositories to track, mapping each subject name to its repository.

Type: dict[str, str] | Default: {}

The name labels the curve and keys its colour, so it is what a reader sees. A bare owner/name is GitHub; anything else is a full URL on whichever forge hosts it:

[tool.repomatic.metrics.subjects]
apricot = "apricot-org/apricot"
papaya = "https://gitlab.com/papaya/papaya"

Every subject is read for every metric its forge answers. The two deep collectors are GitHub-only and skip the rest with a note: an exact star reconstruction reads per-star timestamps, and the archive backfill mines github.com pages.

metrics.sync

Whether sample-metrics records readings for this repository.

Type: bool | Default: false

Opt-in, and the gate on the metrics.yaml workflow: a repository tracking nothing should not carry a weekly job, and an accumulating store is a commitment a maintainer makes deliberately.

Example:

[tool.repomatic]
metrics.sync = false

minimum-release-age

Stabilization window before a new upstream release is adopted.

Type: str | Default: "1 week"

Shared cooldown for the sync-tool-versions, sync-action-pins, and sync-workflow-pins jobs: a release is only proposed once it has been public for at least this long, giving upstream time to yank a bad cut. It also gates repomatic run’s ad-hoc installs at run time, so their transitive trees honor the same window: uvx tools via uv’s --exclude-newer, npm tools via npm’s min-release-age. repomatic init honors it too: the derived upstream workflow pin steps back to the newest release past the window (override with --no-cooldown). The GitHub/PyPI/npm counterpart to uv’s exclude-newer (which guards sync-uv-lock). Accepts the same friendly durations (8 days, 2 weeks, 36 hours). Set to 0 days to adopt releases immediately.

Example:

[tool.repomatic]
minimum-release-age = "1 week"

notification.unsubscribe

Whether the unsubscribe-threads workflow is enabled.

Type: bool | Default: false

Notifications are per-user across all repos. Enable on the single repo where you want scheduled cleanup of closed notification threads. Requires a classic PAT with notifications scope stored as REPOMATIC_NOTIFICATIONS_PAT.

Example:

[tool.repomatic]
notification.unsubscribe = false

nuitka.dev-targets

Nuitka build targets compiled on ordinary pushes, as a canary.

Type: list[str] | Default: ['linux-arm64']

An ordinary push to the default branch rebuilds binaries only for these targets: enough to catch a compilation break early, while freeing runner slots the full fleet would occupy on every code push just to refresh the rolling dev pre-release (a draft). The full target roster still builds on release commits, on the weekly schedule trigger, and on workflow_dispatch. Defaults to ["linux-arm64"], the fastest and cheapest builder. Set to [] to skip dev builds entirely.

Example:

[tool.repomatic]
nuitka.dev-targets = ["linux-arm64"]

nuitka.enabled

Whether Nuitka binary compilation is enabled for this project.

Type: bool | Default: true

Projects with [project.scripts] entries that are not intended to produce standalone binaries (e.g., libraries with convenience CLI wrappers) can set this to false to opt out of Nuitka compilation.

Example:

[tool.repomatic]
nuitka.enabled = true

nuitka.entry-points

Which [project.scripts] entry points produce Nuitka binaries.

Type: list[str] | Default: []

List of CLI IDs (e.g., ["mpm"]) to compile. When empty (the default), deduplicates by callable target: keeps the first entry point for each unique module:callable pair. This avoids building duplicate binaries when a project declares alias entry points (like both mpm and meta-package-manager pointing to the same function).

Example:

[tool.repomatic]
nuitka.entry-points = []

nuitka.extras

[project.optional-dependencies] extras to install before the Nuitka build.

Type: list[str] | Default: []

List of extra names (like ["sbom"]) to sync into the build venv before invoking Nuitka. By default the binary build only sees the project’s base dependencies, which matches a bare pip install <package> and excludes optional features. Listing an extra here calls uv sync --frozen --extra <name> before the Nuitka build so the binary can bundle the optional feature’s third-party packages (paired with --include-package in [tool.nuitka] for imports guarded behind try/except).

Example:

[tool.repomatic]
nuitka.extras = []

nuitka.nofollow-imports

Module names Nuitka must not follow into the compiled binary.

Type: list[str] | Default: ['tkinter']

Each name is forwarded as a --nofollow-import-to flag by repomatic run nuitka. Defaults to ["tkinter"]: boltons.ecoutils (in the dependency tree of every click-extra CLI) probes tkinter inside a guarded try/ except import, which otherwise drags the whole Tcl/Tk stack into every binary. Excluded modules raise ImportError when imported at run time, which guarded imports absorb. GUI projects that really ship tkinter can set this to [].

Example:

[tool.repomatic]
nuitka.nofollow-imports = ["tkinter"]

nuitka.unstable-targets

Nuitka build targets allowed to fail without blocking the release.

Type: list[str] | Default: []

List of target names (e.g., ["linux-arm64", "windows-x64"]) that are marked as unstable. Jobs for these targets will be allowed to fail without preventing the release workflow from succeeding.

Example:

[tool.repomatic]
nuitka.unstable-targets = []

pypi-package-history

Former PyPI package names for projects that were renamed.

Type: list[str] | Default: []

When a project changes its PyPI name, older versions remain published under the previous name. List former names here so lint-changelog can fetch release metadata from all names and generate correct PyPI URLs.

Example:

[tool.repomatic]
pypi-package-history = []

release-assets

Extra asset filenames attached to every GitHub release.

Type: list[str] | Default: []

Each listed file must be produced by a job the consumer defines in its own release workflow (alongside the build lane the engine call already gates on) and uploaded as a run artifact named release-asset-<filename>. The engine’s extra-assets job downloads the artifacts, attests them with the same provenance chain as the compiled binaries, and attaches them to the release draft before publication locks it (GitHub immutable releases).

The build code stays in the downstream repository as regular workflow code, reviewed and linted there: the engine never executes consumer-supplied commands. Filenames must be space-free, as they travel through a space-separated job environment variable. Leave empty to disable, which keeps the job silent.

Example:

[tool.repomatic]
release-assets = []

settings.location

Path to the agent’s project settings file, relative to the repository root.

Type: str | Default: "./.claude/settings.json"

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Only the plugin component writes here, merging the marketplace and enablement keys it owns into whatever the file already holds.

Example:

[tool.repomatic]
settings.location = "./.claude/settings.json"

setup-guide

Whether the setup guide issue is enabled for this project.

Type: bool | Default: true

Projects that do not need REPOMATIC_PAT or manage their own PAT setup can set this to false to suppress the setup guide issue.

Example:

[tool.repomatic]
setup-guide = true

site.cloudflare-compatibility-date

Workers runtime date the Cloudflare Pages project is pinned to.

Type: str | Default: ""

A YYYY-MM-DD date, compared and enforced by repomatic cloudflare-pages against the live project’s deployment_configs, on both the production and preview environments. Inert while the project has no Pages Functions, which is exactly how it drifts unnoticed: the value only starts mattering the moment a Function is added, long after anyone last chose it. Empty (the default) leaves the live value unmanaged.

This is server-side state, not the wrangler.toml key of the same name: Cloudflare honours the project’s own configuration, and the file only matters to a build that a Direct Upload project never runs. lint-repo warns when a committed wrangler.toml disagrees, so the repository states one value rather than two.

Example:

[tool.repomatic]
site.cloudflare-compatibility-date = ""

site.cloudflare-placement

Smart Placement mode declared for the Cloudflare Pages project.

Type: str | Default: ""

smart or off, compared and enforced by repomatic cloudflare-pages on both environments. For a static site it changes nothing measurable and costs nothing; declaring it means the dashboard toggle stops looking like an accident. Empty (the default) leaves the live value unmanaged.

Example:

[tool.repomatic]
site.cloudflare-placement = ""

site.cloudflare-project

Name of the Cloudflare Pages project the site deploys into.

Type: str | Default: ""

Empty (the default) names the project after the repository, which is what the deploy job falls back to. Set it when the project predates repomatic or otherwise cannot carry the repository’s name: renaming a live Pages project would move the <project>.pages.dev hostname every custom domain CNAMEs through.

Example:

[tool.repomatic]
site.cloudflare-project = ""

site.deploy

Where this repository’s built site is published.

Type: str | Default: "github-pages"

github-pages, the default, has the Docs workflow upload the Sphinx tree as a Pages artifact and deploy it with the repository’s own OIDC identity: no stored credential, and nothing to configure beyond enabling Pages.

cloudflare-pages uploads it to a Cloudflare Pages project instead, named per site.cloudflare-project, through wrangler pages deploy. That path needs one repository secret, CLOUDFLARE_API_TOKEN, and it trades the OIDC deploy for a long-lived token: the Docs workflow’s monthly run is what surfaces its expiry, since Cloudflare warns about neither an approaching lapse nor a passed one.

A property of the site rather than of Sphinx. A repository whose site is built by its own workflow (a Pelican blog, a hand-rolled static tree) declares the target here too: that is what turns on the credential checks, the setup-guide step and the Cloudflare drift job for it, even though the Docs workflow’s own Sphinx build never runs.

Choose Cloudflare for what the edge can do rather than for speed. A custom domain on Cloudflare Pages carries its own certificate, so the zone’s apex can be proxied, which is what a _redirects file, a real 404.html and any edge rule on the apex all depend on.

Example:

[tool.repomatic]
site.deploy = "github-pages"

skills.location

Directory prefix for skill folders, relative to the repository root.

Type: str | Default: "./.claude/skills/"

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Skill files are written as {skills_location}/{skill-id}/SKILL.md. Useful for repositories where .claude/ is not at the root (like dotfiles repos that store configs under a subdirectory).

Example:

[tool.repomatic]
skills.location = "./.claude/skills/"

sphinx.builder

Sphinx builder producing the deployed documentation site.

Type: str | Default: "html"

The default html writes page.html, so the site serves /page.html. Setting it to dirhtml writes page/index.html instead, so the same page serves at /page/ and the published URLs carry no extension, which is the shape search engines and most static hosts expect.

The one Sphinx setting a project cannot make in its own conf.py, hence a config key: the builder is chosen on the command line, and docs.yaml is what runs it. Switching an already-published site republishes every URL it has: the old paths stop existing, so the repository’s own absolute self-links (readme, packaging specs) move in the same commit, and whatever fronts the site redirects the old ones.

Example:

[tool.repomatic]
sphinx.builder = "html"

subagents.location

Directory prefix for subagent definitions, relative to the repository root.

Type: str | Default: "./.claude/agents/"

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Subagent files are written as {subagents_location}/{agent-id}.md. Useful for repositories where .claude/ is not at the root (like dotfiles repos that store configs under a subdirectory).

Example:

[tool.repomatic]
subagents.location = "./.claude/agents/"

sync-runner-images.ignore

Runner labels never to propose, whatever GitHub announces about them.

Type: list[str] | Default: []

A sync-* job regenerates on every push, so a proposal declined by closing its pull request comes back on the next one. Without somewhere to record the decision, the only way to stop a proposal already considered and rejected is to disable the whole operation. Naming the label here is the one-line commit that makes a “no” stick:

[tool.repomatic.sync-runner-images]
# 26.04 stays out until its capacity settles: queue time matters more here
# than the compute it wins.
ignore = [ "ubuntu-26.04", "ubuntu-26.04-arm" ]

Applies to both shapes: an ignored label is neither probed when it arrives nor proposed as a successor when something retires onto it.

Example:

[tool.repomatic]
sync-runner-images.ignore = []

test-matrix.exclude

Extra exclude rules applied to both full and PR test matrices.

Type: list[dict[str, str]] | Default: []

Each entry is a dict of GitHub Actions matrix keys (like {"os": "windows-11-arm"}) that removes matching combinations. Additive to the upstream default excludes.

Example:

[tool.repomatic]
test-matrix.exclude = []

test-matrix.full-include

Full-matrix-only job rows, added as standalone matrix combinations.

Type: list[dict[str, str]] | Default: []

Each entry is a dict of GitHub Actions matrix keys fully describing one job (like {"os": "ubuntu-26.04-arm", "python-version": "3.10", "click-version": "8.3.1"}). Unlike include, these are appended as independent rows of the full matrix, never merged into the base cross-product, so a cell can’t overwrite a shipped-config job that shares its os and python-version. Keys left out inherit the matrix defaults (the single-key include entries, plus state: stable), so a cell lists only what differs from the shipped configuration.

Use this for heterogeneous coverage, like pinning each release of a dependency to its own runner and Python, where carving the same shape from the base cross-product with exclude would take many rules. Like variations and unstable, it touches the full matrix only; the PR matrix stays a curated reduced set. Adding any entry makes the full matrix emit as a flat job list ({"include": [...]}), which GitHub runs verbatim with no cross-product expansion.

Example:

[tool.repomatic]
test-matrix.full-include = []

test-matrix.include

Extra include directives applied to both full and PR test matrices.

Type: list[dict[str, str]] | Default: []

Each entry is a dict of GitHub Actions matrix keys that adds or augments matrix combinations. Additive to the upstream default includes.

Because includes apply to both matrices, a directive whose keys are not PR base axes is risky. In the PR matrix only os and python-version are base axes, so a key like click-version (injected by another include) has nothing to match and GitHub’s expansion adds the directive to every PR job, overwriting it. To flag a value continue-on-error, prefer unstable over an include carrying state: unstable.

Example:

[tool.repomatic]
test-matrix.include = []

test-matrix.remove

Per-axis value removals applied to both full and PR test matrices.

Type: dict[str, list[str]] | Default: {}

Outer key is the variation/axis ID (e.g., os, python-version). Inner list contains values to drop from that axis. Applied after replacements but before excludes, includes, and variations.

test-matrix.replace

Per-axis value replacements applied to both full and PR test matrices.

Type: dict[str, dict[str, str]] | Default: {}

Outer key is the variation/axis ID (e.g., os, python-version). Inner dict maps old values to new values. Applied before removals, excludes, includes, and variations.

test-matrix.unstable

Full-matrix-only combinations to flag continue-on-error in CI.

Type: list[dict[str, str]] | Default: []

Each entry is a dict of GitHub Actions matrix keys (like {"click-version": "main"}). Every full-matrix combination matching an entry gets a state: unstable value, which tests.yaml reads to set continue-on-error. Like variations, this applies to the full matrix only; the PR matrix stays a curated stable set.

Prefer this over an include entry carrying state: unstable. include applies to both matrices, and in the PR matrix a key like click-version is not a base axis (another include injects it), so GitHub’s expansion would add the directive to every PR job and overwrite it. unstable only touches the full matrix, sidestepping that hijack.

Example:

[tool.repomatic]
test-matrix.unstable = []

test-matrix.variations

Extra matrix dimension values added to the full test matrix only.

Type: dict[str, list[str]] | Default: {}

Each key is a dimension ID (e.g., os, click-version) and its value is a list of additional entries. For existing dimensions, values are merged with the upstream defaults. For new dimension IDs, a new axis is created. Only affects the full matrix; the PR matrix stays a curated reduced set.

tool-versions.sync

Whether the sync-tool-versions job is enabled for this project.

Type: bool | Default: true

Bumps every tool in the repomatic run registry to the latest release passing the minimum-release-age cooldown (GitHub releases for binary tools, PyPI for the rest), recomputing binary checksums in the same pass. Projects that pin tool versions by hand can set this to false.

Example:

[tool.repomatic]
tool-versions.sync = true

uv-lock.sync

Whether uv.lock sync is enabled for this project.

Type: bool | Default: true

Projects that manage their own lock file strategy and do not want the sync-uv-lock job to run uv lock --upgrade can set this to false.

Example:

[tool.repomatic]
uv-lock.sync = true

vulnerable-deps.sources

Advisory databases to consult for known vulnerabilities.

Type: list[str] | Default: ['uv-audit', 'github-advisories']

Recognized values:

  • "uv-audit": PyPA Advisory Database via uv audit (works locally and in CI without a GitHub token).

  • "github-advisories": GitHub Advisory Database via the repository’s Dependabot alerts (CI-only, requires a token with Dependabot alerts: Read-only).

Sources are unioned and deduplicated per package by advisory identity: entries sharing an advisory_id or a cross-referenced CVE/GHSA/PYSEC alias are merged. Repositories that distrust GHSA, or have no Dependabot alerts enabled, can opt out with sources = ["uv-audit"].

Example:

[tool.repomatic]
vulnerable-deps.sources = ["uv-audit", "github-advisories"]

vulnerable-deps.sync

Whether the fix-vulnerable-deps job is enabled for this project.

Type: bool | Default: true

Projects that manage their own vulnerability remediation flow can set this to false to skip the autofix job.

Example:

[tool.repomatic]
vulnerable-deps.sync = true

workflow.extra-paths

Literal entries to append to every workflow’s paths: filter.

Type: list[str] | Default: []

Applies to thin-caller and header-only sync. Useful for repo-specific files that should re-trigger CI but are not detected by the canonical paths: filter (e.g., install.sh, dotfiles/**).

Per-workflow overrides in paths ignore this list: when an entry exists for a given filename, that entry is treated as the complete list.

Example:

[tool.repomatic]
workflow.extra-paths = []

workflow.ignore-paths

Literal entries to strip from every workflow’s paths: filter.

Type: list[str] | Default: []

Useful for canonical entries that don’t exist downstream (e.g., tests/**, uv.lock in repos with no Python tests or lockfile). Match is by exact string equality. Applies before extra_paths.

Per-workflow overrides in paths ignore this list.

Example:

[tool.repomatic]
workflow.ignore-paths = []

workflow.paths

Per-workflow override of the paths: filter, keyed by filename.

Type: dict[str, list[str]] | Default: {}

When a workflow filename appears here, its paths: blocks (in push, pull_request, etc.) are replaced wholesale with the listed entries. source_paths, extra_paths, and ignore_paths do not apply when a per-workflow override is set: the list is treated as authoritative.

Override only takes effect on triggers that already have a paths: filter in the canonical workflow. Workflows without paths: upstream keep their unrestricted trigger semantics.

Example:

[tool.repomatic.workflow.paths]
"tests.yaml" = ["install.sh", "packages.toml", ".github/workflows/tests.yaml"]

workflow.source-paths

Source code directory names for workflow trigger paths: filters.

Type: list[str] | Default: (none)

When set, thin-caller and header-only workflows include paths: filters using these directory names (as name/** globs) alongside universal paths like pyproject.toml and uv.lock.

When None (default), source paths are auto-derived from [project.name] in pyproject.toml by replacing hyphens with underscores, the universal Python convention. For example, name = "extra-platforms" automatically uses ["extra_platforms"].

workflow.sync

Whether workflow sync is enabled for this project.

Type: bool | Default: true

Projects that manage their own workflow files and do not want the autofix job to sync thin callers or headers can set this to false.

Example:

[tool.repomatic]
workflow.sync = true

workflow-pins.sync

Whether the sync-workflow-pins job is enabled for this project.

Type: bool | Default: true

Bumps version literals embedded in workflow YAML (npm pkg@x installs and uvx '<pkg>==x' PyPI pins) to the latest release passing the minimum-release-age cooldown. Projects that pin these by hand can set this to false.

Example:

[tool.repomatic]
workflow-pins.sync = true

Option

Description

Default

abandoned-versions

Versions documented in the changelog but never published.

[]

action-pins.sync

Whether the sync-action-pins job is enabled for this project.

true

agent.location

Path to the agent’s instructions file, relative to the repository root.

"./claude.md"

awesome-template.sync

Whether awesome-template sync is enabled for this project.

true

binaries.sync

Whether the release pipeline records released binaries into the repository.

true

bumpversion.sync

Whether bumpversion config sync is enabled for this project.

true

cache.dir

Override the binary cache directory path.

""

cache.github-release-ttl

Freshness TTL for cached single-release bodies (seconds).

604800

cache.github-releases-ttl

Freshness TTL for cached all-releases responses (seconds).

86400

cache.max-age

Auto-purge cached entries older than this many days.

30

cache.npm-ttl

Freshness TTL for cached npm registry metadata (seconds).

86400

cache.pypi-ttl

Freshness TTL for cached PyPI metadata (seconds).

86400

changelog.archive-location

File path of the changelog archive, relative to the root of the repository.

""

changelog.bullet-word-threshold

Word count above which lint-changelog warns about a changelog bullet.

40

changelog.location

File path of the changelog, relative to the root of the repository.

"./changelog.md"

dep-sources.sync

Whether the sync-dep-sources updater is enabled for this project.

true

dependency-graph.all-extras

Whether to include all optional extras in the graph.

true

dependency-graph.all-groups

Whether to include all dependency groups in the graph.

true

dependency-graph.level

Maximum depth of the dependency graph.

(none)

dependency-graph.no-extras

Optional extras to exclude from the graph.

[]

dependency-graph.no-groups

Dependency groups to exclude from the graph.

[]

dependency-graph.output

Path where the dependency graph Mermaid diagram should be written.

"./docs/assets/dependencies.mmd"

dev-release.sync

Whether dev pre-release sync is enabled for this project.

true

docs.apidoc-exclude

Glob patterns for modules to exclude from sphinx-apidoc.

[]

docs.apidoc-extra-args

Extra arguments appended to the sphinx-apidoc invocation.

[]

docs.update-script

Path to a Python script run after sphinx-apidoc to generate dynamic content.

"./docs/docs_update.py"

exclude

Additional components and files to exclude from repomatic operations.

[]

flavor.agent

AI coding agent whose asset layout the bundled skills and agents target.

"claude_code"

flavor.ci

CI system the bundled workflows target.

"github_ci"

gitignore.extra-categories

Additional gitignore template categories to fetch from gitignore.io.

[]

gitignore.extra-content

Content appended at the end of the generated .gitignore file.

(see example)

gitignore.location

File path of the .gitignore to update, relative to the root of the repository.

"./.gitignore"

gitignore.sync

Whether .gitignore sync is enabled for this project.

true

include

Components and files to force-include, overriding default exclusions.

[]

labels.content-rules

Per-label patterns matched against an issue or pull request’s text.

{}

labels.extra

Inline label definitions applied at sync time under the default profile.

[]

labels.extra-files

URLs of additional label definition files (JSON, JSON5, TOML, or YAML).

[]

labels.file-rules

Per-label globs matched against the paths a pull request changes.

{}

labels.sync

Whether label sync is enabled for this project.

true

lint-deps.allow

Packages that may ship from somewhere other than PyPI, and why.

{}

lint-deps.comment-word-threshold

Word count above which lint-deps warns about a floor comment.

40

mailmap.sync

Whether .mailmap sync is enabled for this project.

true

manpages.asset-name

Filename stem (without the .tar.gz extension) for the man-page tarball uploaded to the GitHub release.

""

manpages.script

Click command target whose tree gets rendered as roff .1 files and attached as a tarball asset on every GitHub release.

""

metrics.charts

Charts to draw from the accumulated history, one array-of-tables entry each.

[]

metrics.colors

Per-subject [light, dark] hex pairs overriding the positional palette.

{}

metrics.forges

Self-hosted forge instances, mapping each host to the software it runs.

{}

metrics.predecessors

Retired forerunners, mapping the subject they precede to their own repository.

{}

metrics.skip

Subjects deliberately left unmeasured, mapped to the reason why.

{}

metrics.store

Where the readings accumulate, one row per subject, metric and date.

"./docs/assets/metrics.csv"

metrics.subjects

Repositories to track, mapping each subject name to its repository.

{}

metrics.sync

Whether sample-metrics records readings for this repository.

false

minimum-release-age

Stabilization window before a new upstream release is adopted.

"1 week"

notification.unsubscribe

Whether the unsubscribe-threads workflow is enabled.

false

nuitka.dev-targets

Nuitka build targets compiled on ordinary pushes, as a canary.

['linux-arm64']

nuitka.enabled

Whether Nuitka binary compilation is enabled for this project.

true

nuitka.entry-points

Which [project.scripts] entry points produce Nuitka binaries.

[]

nuitka.extras

[project.optional-dependencies] extras to install before the Nuitka build.

[]

nuitka.nofollow-imports

Module names Nuitka must not follow into the compiled binary.

['tkinter']

nuitka.unstable-targets

Nuitka build targets allowed to fail without blocking the release.

[]

pypi-package-history

Former PyPI package names for projects that were renamed.

[]

release-assets

Extra asset filenames attached to every GitHub release.

[]

settings.location

Path to the agent’s project settings file, relative to the repository root.

"./.claude/settings.json"

setup-guide

Whether the setup guide issue is enabled for this project.

true

site.cloudflare-compatibility-date

Workers runtime date the Cloudflare Pages project is pinned to.

""

site.cloudflare-placement

Smart Placement mode declared for the Cloudflare Pages project.

""

site.cloudflare-project

Name of the Cloudflare Pages project the site deploys into.

""

site.deploy

Where this repository’s built site is published.

"github-pages"

skills.location

Directory prefix for skill folders, relative to the repository root.

"./.claude/skills/"

sphinx.builder

Sphinx builder producing the deployed documentation site.

"html"

subagents.location

Directory prefix for subagent definitions, relative to the repository root.

"./.claude/agents/"

sync-runner-images.ignore

Runner labels never to propose, whatever GitHub announces about them.

[]

test-matrix.exclude

Extra exclude rules applied to both full and PR test matrices.

[]

test-matrix.full-include

Full-matrix-only job rows, added as standalone matrix combinations.

[]

test-matrix.include

Extra include directives applied to both full and PR test matrices.

[]

test-matrix.remove

Per-axis value removals applied to both full and PR test matrices.

{}

test-matrix.replace

Per-axis value replacements applied to both full and PR test matrices.

{}

test-matrix.unstable

Full-matrix-only combinations to flag continue-on-error in CI.

[]

test-matrix.variations

Extra matrix dimension values added to the full test matrix only.

{}

tool-versions.sync

Whether the sync-tool-versions job is enabled for this project.

true

uv-lock.sync

Whether uv.lock sync is enabled for this project.

true

vulnerable-deps.sources

Advisory databases to consult for known vulnerabilities.

['uv-audit', 'github-advisories']

vulnerable-deps.sync

Whether the fix-vulnerable-deps job is enabled for this project.

true

workflow.extra-paths

Literal entries to append to every workflow’s paths: filter.

[]

workflow.ignore-paths

Literal entries to strip from every workflow’s paths: filter.

[]

workflow.paths

Per-workflow override of the paths: filter, keyed by filename.

{}

workflow.source-paths

Source code directory names for workflow trigger paths: filters.

(none)

workflow.sync

Whether workflow sync is enabled for this project.

true

workflow-pins.sync

Whether the sync-workflow-pins job is enabled for this project.

true

Ephemeral components

Most components write files a repository is meant to commit. The labels component does not: labels.toml is an input that sync-labels regenerates from this configuration right before handing it to labelmaker, so a copy sitting in the working tree is never the one that gets used.

The file is therefore staged only when the component is named explicitly:

$ repomatic init labels

A bare repomatic init leaves it out, and listing labels in include does not change that: the override is refused with a warning rather than silently honored. Everything downstream repositories customize (labels.extra, labels.file-rules, labels.content-rules, labels.extra-files) is read straight from pyproject.toml, so no generated label file needs committing. A .github/labeller-*.yaml left over from the era when the labeller ran as a GitHub Action is dead weight: nothing reads it any more, delete it.

Diverging from a managed file

Every file repomatic manages is a generated output, not a starting template. sync-repomatic, sync-gitignore and their siblings rebuild these files from [tool.repomatic] on each run and open a pull request for the difference, so an edit made straight to the file survives exactly until the next sync. The revert arrives as an ordinary-looking sync pull request, which is what makes it easy to miss: the diff is undoing a deliberate local decision and reads like routine maintenance.

sync-gitignore is the exception, because there the loss is silent and total rather than a visible diff line: it refuses to write when a rule on disk is absent from what it generated, listing the rules at stake and exiting non-zero. Move them into gitignore.extra-content to keep them, or pass --drop-orphans to confirm they should go. Comments and ordering are not compared, so a reformatted file is not mistaken for a rewritten one.

There are two ways to keep a divergence, and picking the wrong one is why the edit keeps coming back.

When the file already exposes a knob for what you want to change, set the knob. .gitignore is the common case: everything past the gitignore.io block is gitignore.extra-content, so a pattern appended by hand to the generated file is dropped on the next sync, while the same pattern in pyproject.toml is emitted every time. Mind that the option replaces its default rather than extending it, so the entries repomatic ships have to be repeated alongside the new ones or they disappear too:

gitignore.extra-content = '''
# Claude Code local files.
.claude/scheduled_tasks.lock
.claude/settings.local.json
**/.claude/.cc-writes/

# Sphinx linkcheck output.
docs/_linkcheck/

# Downloaded weather readings, re-fetched on demand.
weather-samples/
'''

Everything above the last entry there is the shipped default, quoted back verbatim; only weather-samples/ is this repository’s own. Drop one of the repeated lines and the pattern it carried stops being emitted.

When no knob covers the change, reach for exclude only if you understand what it means, because it is not an opt-out from syncing: it declares that the repository has no such file. The sync-repomatic job runs repomatic init --delete-unmodified --delete-excluded, so the first sync after the entry lands opens a pull request deleting the file rather than leaving the local version alone. Excluding a workflow to protect your edits to it removes your CI instead.

Keeping a customized file therefore means keeping it out of the managed set entirely, by giving it a name repomatic does not own:

# The repository has no upstream tests.yaml: its own check lives unmanaged in
# workflows/install.yaml, which repomatic never generates and never deletes.
exclude = ["workflows/tests.yaml"]

The rename is what preserves the file; the exclude entry merely stops repomatic from recreating the managed one beside it. A repository that wants to freeze workflows under their existing names has the blunter workflow.sync = false, which returns before any workflow is written and leaves every file on disk untouched, at the cost of ending upstream fixes for all of them, not just the customized one.

Prefer a knob whenever one exists. Reach for a rename when a file’s purpose genuinely diverges from the template, and for workflow.sync = false when a repository has taken over its workflows wholesale.

Workflow triggers deserve particular care, because a reverted trigger restores itself. GitHub builds a pull_request run from the merge commit rather than from either branch alone, so removing a pull_request: trigger on the default branch takes effect immediately for open pull requests: their merge commits pick up the new file and stop firing. The sync pull request restoring that trigger is the exception, since it is the one merge commit where the trigger still exists, and it re-enables the workflow for itself. A repository that removes a trigger and then watches a single pull request keep running it is usually looking at the sync that puts it back, and excluding the file settles both halves at once.

Labeller rules

repomatic apply-labels evaluates both families on every issue and pull request opened. Each is a table mapping a label to the patterns that apply it: labels.content-rules against the thread’s title and body, labels.file-rules against the paths a pull request changes. Any one pattern matching applies the label.

[tool.repomatic.labels.content-rules]
"📦 manager: apk" = ["apk", "alpine", "alpine linux"]
"🐛 bug" = []

[tool.repomatic.labels.file-rules]
"📦 manager: apk" = ["managers/apk.*", "tests/*apk*"]

A content pattern is a plain keyword by default: matched case-insensitively and anchored on word boundaries, so Alpine matches and prefix does not trip a fix rule. Wrap a pattern in slashes (/traceback|stack trace/i) to pass a regex through instead, with the i, m and s flags honored. File patterns are minimatch-dialect globs (** crosses directories, {a,b} expands, a leading dot needs no special casing), and a !-prefixed glob subtracts from the label’s other globs the way a .gitignore line would.

Your entry for a label replaces the bundled default entry for that label, an empty list disables it (as "🐛 bug" = [] above), and labels the defaults do not mention are added. Untouched defaults keep flowing from upstream releases. The default rule sets ship in repomatic.labels.DEFAULT_CONTENT_RULES and repomatic.labels.DEFAULT_FILE_RULES, and every rule must name a label the repository actually defines: see labels.extra above for declaring new ones.

Tune rules for precision, not recall. The labeller pre-labels for the maintainer’s first pass and never replaces it: a missing label costs one manual click, while a wrong one is noise on every thread that trips it. Never key a rule off a token the project prints in its own output, or a user pasting a trace sets every such label at once.

Forge sampling

sample-metrics records what forges say about a set of repositories, into one CSV committed here. It is opt-in, and metrics.sync is also what decides whether repomatic init hands the repository the metrics.yaml workflow at all.

Every reading is one row: which repository, which metric, on which date, what it said, and where the figure came from. How long a row is kept is a property of the metric rather than of the caller. A counter like the star count accrues, so its curve can be charted. An attribute like the date of a project’s newest commit keeps a single row, restamped only when the value moves, since nothing reads it chronologically and a hundred subjects sampled weekly would otherwise pile up thousands of rows a year.

[tool.repomatic.metrics]
store = "./docs/assets/metrics.csv"
sync = true

# A bare owner/name is GitHub; anything else is a full URL on whichever forge
# hosts it.
[tool.repomatic.metrics.subjects]
apricot = "apricot-org/apricot"
papaya = "https://gitlab.com/papaya/papaya"

# A project that reopened under a new repository: its forerunner is drawn
# dashed, in the successor's hue, and stops where the successor begins.
[tool.repomatic.metrics.predecessors]
papaya = "old-owner/papaya"

# A self-hosted instance is never guessed from its name.
[tool.repomatic.metrics.forges]
"gitlab.example.org" = "gitlab"

# A mapping rather than a list: the reason is the point.
[tool.repomatic.metrics.skip]
carrot = "Ships in a distribution package with no public repository."

[[tool.repomatic.metrics.charts]]
output = "./docs/assets/star-history.svg"

[[tool.repomatic.metrics.charts]]
mode = "relative"
output = "./docs/assets/star-history-by-age.svg"

[[tool.repomatic.metrics.charts]]
only = ["apricot"]
output = "./docs/assets/star-history-apricot.svg"

[[tool.repomatic.metrics.charts]]
scale = "logarithmic"
output = "./docs/assets/star-history-compared.svg"

A chart plots one metric, stars unless metric names another, and only a metric the store accrues can be charted. The two axes are set independently: mode measures the horizontal one as absolute (one shared calendar) or relative (every curve measured from its own origin, which compares trajectories rather than dates), and scale measures the vertical one as linear or logarithmic. A chart comparing projects of different sizes usually wants both, since a shared origin still leaves the smallest curve flat against the axis. title names the chart for a screen reader. Hues come from a twelve-slot palette assigned in draw order; pin one with [tool.repomatic.metrics.colors] when it must survive a reordering.

Two collectors are GitHub-only and skip every other forge with a note. An exact reconstruction rebuilds a star curve from the timestamp of every star a repository still holds, which works wherever the token administers it, so that curve is complete from day one rather than starting on the day sampling did. An archive backfill (--backfill-wayback) mines contemporaneous counts from archived github.com pages, for a repository nobody administers. Both are one-offs the scheduled job never runs.

--import-csv loads a star-history.com calendar export, for a repository the archives never captured either. That service read the same stargazer endpoint GitHub has since closed, so an export taken while it worked is the only surviving record of that repository’s past: a replacement cannot be downloaded today.

Flavors

[tool.repomatic.flavor] declares which ecosystems a repository targets, giving every present and future ecosystem decision one place to live instead of a new flag per feature.

Key

Default

Meaning

flavor.agent

claude_code

AI coding agent whose asset layout skills and agents follow.

flavor.ci

github_ci

CI system the bundled workflows target.

Values are trait IDs borrowed from extra-platforms, which already models both AI agents and CI systems, so repomatic inherits its vocabulary and detection helpers instead of maintaining a parallel enum. Hyphens are normalized, so claude-code and claude_code are the same thing.

Each value is checked twice, and the two failures read differently on purpose. A value outside the upstream vocabulary is a typo:

Unknown [tool.repomatic] flavor.ci = 'mango'. Expected an extra-platforms trait ID: azure_pipelines, bamboo, …

while a real ecosystem repomatic has not implemented says so plainly:

Unsupported [tool.repomatic] flavor.ci = 'gitlab_ci'. repomatic targets: github_ci.

flavor.agent drives where assets land: leave skills.location, subagents.location, agent.location and settings.location unset and each follows the agent’s own layout, while setting one explicitly overrides it for that asset alone. Defaults are static and never auto-detected: deriving them from the running agent would make a repository’s effective configuration depend on which tool last invoked repomatic, and repomatic metadata would stop being reproducible.

agent.location is the one worth setting by hand, because the file it names is the only asset a repository is likely to already own under a name of its own choosing. It defaults to the selected agent’s own filename (./claude.md for Claude Code), and pointing it at the cross-agent AGENTS.md, or anywhere outside the root, is what lets repomatic init agent sync a document that already exists rather than creating a second one beside it:

[tool.repomatic]
include = ["agent"]
agent.location = "./dotfiles/.agents/AGENTS.md"

Caution

agent.location and subagents.location differ by one character and name different things: the first an instructions document, the second a directory of subagent definitions. Setting the wrong one is silent, since neither is validated against the other.

Repository scope

A bare repomatic init writes only what a repository can actually use. Three traits, all read from the repository itself, decide what that means:

Trait

Detected from

Gates

Awesome list

A repository name starting with awesome-

awesome-template, the awesome-only skills, lychee

Python project

A PEP 621 [project] table that validates

debug.yaml, dependency locking, the test matrix

Distributable

A Python project without [tool.uv] package = false

changelog.md, changelog.yaml, release.yaml, publish-pypi-action

The last two come apart for a uv virtual project: a repository that declares [project] purely to carry dependencies and opts out of being built with [tool.uv] package = false. Blogs, docs sites and dotfiles repositories managed with uv all look like this. They keep everything a Python project needs (a uv.lock to sync, coverage config, the test matrix) and skip the release lane, which has nothing to publish or tag.

A pyproject.toml carrying only [tool.*] sections is not a Python project at all, so it gets neither group.

Scope is a default, not a rule. Naming a component on the command line, or listing it in include, materializes it regardless:

$ repomatic init changelog

Adopting a tool config

Tool configs are the one group a bare repomatic init never introduces. [tool.typos], [tool.ruff], [tool.pytest] and their siblings land only when named:

$ repomatic init typos

Adoption is one-way. Once the section exists, a bare init picks it back up on every run, so the sync-repomatic job keeps it aligned with the bundled template from then on: new canonical rules arrive, local additions survive. The section is a managed file like any other after that, which makes it subject to § Diverging from a managed file: the sync rebuilds it from the template and grafts local content back, so hand-written comments inside it do not survive.

Only the configs repomatic keeps syncing behave this way: typos, uv and bumpversion through this adoption path, plus lychee on awesome-list repos, where it lands by default rather than through explicit naming. The rest (ruff, pytest, coverage, mypy, mdformat) are starting points the repository owns outright after the first write, and init never revisits them.

[tool.X] bridge and tool runner

repomatic run also bridges the gap for tools that can’t read pyproject.toml natively: write your config in [tool.<name>] and repomatic translates it to the tool’s native format at invocation time. See the tool runner page for the full list of supported tools, config resolution precedence, binary caching, and a tutorial.

repomatic.config API

Configuration schema and loading for [tool.repomatic] in pyproject.toml.

Defines the Config dataclass, its TOML serialization helpers, and the load_repomatic_config function that reads, validates, and returns a typed Config instance.

class repomatic.config.CacheConfig(dir='', github_release_ttl=604800, github_releases_ttl=86400, max_age=30, npm_ttl=86400, pypi_ttl=86400)[source]

Bases: object

Nested schema for [tool.repomatic.cache].

dir: str = ''

Override the binary cache directory path.

When empty (the default), the cache uses the platform convention: ~/Library/Caches/repomatic on macOS, $XDG_CACHE_HOME/repomatic or ~/.cache/repomatic on Linux, %LOCALAPPDATA%\repomatic\Cache on Windows. The REPOMATIC_CACHE_DIR environment variable takes precedence over this setting.

github_release_ttl: int = 604800

Freshness TTL for cached single-release bodies (seconds).

GitHub release bodies are immutable once published, so a long TTL (7 days) is safe. Set to 0 to disable caching for single-release lookups.

github_releases_ttl: int = 86400

Freshness TTL for cached all-releases responses (seconds).

New releases can appear at any time, so a shorter TTL (24 hours) balances freshness with API savings.

max_age: int = 30

Auto-purge cached entries older than this many days.

Set to 0 to disable auto-purge. The REPOMATIC_CACHE_MAX_AGE environment variable takes precedence over this setting.

npm_ttl: int = 86400

Freshness TTL for cached npm registry metadata (seconds).

New npm versions can appear at any time, so a 24-hour TTL balances freshness with request savings. Set to 0 to disable caching for npm lookups.

pypi_ttl: int = 86400

Freshness TTL for cached PyPI metadata (seconds).

PyPI metadata changes when new versions are published. A 24-hour TTL avoids redundant API calls while keeping data reasonably current.

class repomatic.config.DependencyGraphConfig(all_extras=True, all_groups=True, level=None, no_extras=<factory>, no_groups=<factory>, output='./docs/assets/dependencies.mmd')[source]

Bases: object

Nested schema for [tool.repomatic.dependency-graph].

all_extras: bool = True

Whether to include all optional extras in the graph.

When True, the update-dep-graph command behaves as if --all-extras was passed.

all_groups: bool = True

Whether to include all dependency groups in the graph.

When True, the update-dep-graph command behaves as if --all-groups was passed. Projects that want to exclude development dependency groups (docs, test, typing) from their published graph can set this to false.

level: int | None = None

Maximum depth of the dependency graph.

None means unlimited. 1 = directly-declared deps only, 2 = adds their deps, etc. Equivalent to --level.

no_extras: list[str]

Optional extras to exclude from the graph.

Equivalent to passing --no-extra for each entry. Takes precedence over dependency-graph.all-extras.

no_groups: list[str]

Dependency groups to exclude from the graph.

Equivalent to passing --no-group for each entry. Takes precedence over dependency-graph.all-groups.

output: str = './docs/assets/dependencies.mmd'

Path where the dependency graph Mermaid diagram should be written.

The dependency graph visualizes the project’s dependency tree in Mermaid format.

class repomatic.config.DocsConfig(apidoc_exclude=<factory>, apidoc_extra_args=<factory>, update_script='./docs/docs_update.py')[source]

Bases: object

Nested schema for [tool.repomatic.docs].

apidoc_exclude: list[str]

Glob patterns for modules to exclude from sphinx-apidoc.

Passed as positional exclude arguments after the source directory (e.g., ["setup.py", "tests"]).

apidoc_extra_args: list[str]

Extra arguments appended to the sphinx-apidoc invocation.

The base flags --no-toc --module-first are always applied. Use this for project-specific options (e.g., ["--implicit-namespaces"]).

update_script: str = './docs/docs_update.py'

Path to a Python script run after sphinx-apidoc to generate dynamic content.

Resolved relative to the repository root. Must reside under the docs/ directory for security. Set to an empty string to disable.

class repomatic.config.AgentLayout(skills, subagents, instructions, settings)[source]

Bases: object

Where one AI coding agent expects its assets to live.

skills: str

Directory holding one folder per skill.

subagents: str

Directory holding subagent definitions.

Named for what it holds, not for the agent reading it: agents invited a one-character confusion with instructions, whose component and config key are agent, and the two write entirely different things.

instructions: str

File holding the agent’s own instructions, read on every session.

A file, like settings, and merged into rather than written whole: repomatic owns the audience-tagged sections and the repository owns the rest. The name each agent expects differs (claude.md for Claude Code, AGENTS.md for the cross-agent convention), which is the whole reason this is a layout field rather than the constant it started as.

settings: str

File holding the agent’s project-scoped settings.

A file rather than a directory, unlike its siblings: it is the one asset repomatic merges into rather than writes whole, so the path has to name the document itself.

repomatic.config.AGENT_LAYOUTS: Final[dict[str, AgentLayout]] = {'claude_code': AgentLayout(skills='./.claude/skills/', subagents='./.claude/agents/', instructions='./claude.md', settings='./.claude/settings.json')}

Asset layout per agent, keyed by extra_platforms.ALL_AGENTS trait ID.

Only agents repomatic can actually lay out appear here. cline and cursor are valid trait IDs but have no Agent Skills layout to target, so selecting one is rejected rather than silently producing a Claude Code tree.

repomatic.config.DEFAULT_AGENT: Final[str] = 'claude_code'

Agent assumed when [tool.repomatic.flavor] agent is unset.

repomatic.config.DEFAULT_CI: Final[str] = 'github_ci'

CI system assumed when [tool.repomatic.flavor] ci is unset.

repomatic.config.CLOUDFLARE_PLACEMENT_MODES: Final[frozenset[str]] = frozenset({'', 'off', 'smart'})

Values site.cloudflare-placement accepts, empty meaning unmanaged.

The vocabulary of the Pages project’s placement.mode field, which is what repomatic cloudflare-pages writes the setting through. Anything else would be PATCHed to the live project verbatim and rejected there, far from the pyproject.toml line that caused it.

repomatic.config.SITE_DEPLOY_TARGETS: Final[frozenset[str]] = frozenset({'cloudflare-pages', 'github-pages'})

Hosts a repository’s built site can be published to.

One deploy job per target, each with the permissions its own host needs, so a value outside this set has no job at all behind it. Config.__post_init__ rejects one rather than letting the workflow run green and publish nothing.

repomatic.config.location_path(location)[source]

Normalize a *.location config value into a bare repo-relative path.

The location defaults carry a ./ prefix (they read as paths in the reference table) and a directory location a trailing slash; neither belongs in a registry target or an output_dir / path join. One normalizer keeps every consumer spelling the same value the same way.

Parameters:

location (str) – A Config location field value, class default or resolved instance value alike.

Return type:

str

Returns:

The path with no ./ prefix and no trailing slash.

class repomatic.config.FlavorConfig(agent='claude_code', ci='github_ci')[source]

Bases: object

Nested schema for [tool.repomatic.flavor].

Declares which ecosystem repomatic is targeting, so a future decision has one place to branch on instead of a new flag per feature.

Note

Values are trait IDs from extra-platforms, which already models both AI agents and CI systems. Borrowing its vocabulary brings its detection helpers (current_agent(), is_github_ci()) and its naming along for free, instead of repomatic maintaining a parallel enum.

Caution

Defaults are static, never detected. Deriving them from current_agent() would make a repository’s effective configuration depend on which tool happened to invoke repomatic last, so repomatic metadata would stop being reproducible.

agent: str = 'claude_code'

AI coding agent whose asset layout the bundled skills and agents target.

Accepts a extra_platforms.ALL_AGENTS trait ID present in AGENT_LAYOUTS. Hyphens are normalized, so claude-code works too.

ci: str = 'github_ci'

CI system the bundled workflows target.

Accepts a extra_platforms.ALL_CI trait ID. Only github_ci is implemented: every bundled workflow is a GitHub Actions workflow, so any other value is rejected rather than quietly emitting the wrong thing.

property layout: AgentLayout

Asset layout for the selected agent.

class repomatic.config.GitignoreConfig(extra_categories=<factory>, extra_content=<factory>, location='./.gitignore', sync=True)[source]

Bases: object

Nested schema for [tool.repomatic.gitignore].

extra_categories: list[str]

Additional gitignore template categories to fetch from gitignore.io.

List of template names (e.g., ["Python", "Node", "Terraform"]) to combine with the generated .gitignore content.

extra_content: str

Content appended at the end of the generated .gitignore file.

“Appended” describes where the string lands, after the gitignore.io block, not how a downstream value combines with the default above: setting this key replaces that default wholesale, so the entries shown there are lost unless the override repeats them. repomatic.gitignore.orphaned_rules() catches that for any rule an earlier sync already wrote to disk, but not for one this repository never materialized, so copy the default and extend it rather than writing only the new lines. Reach for extra_categories instead when adding whole gitignore.io templates: that one is additive.

The .cc-writes entry is the one carrying a **/ prefix, because it is the one Claude Code does not place at the repository root: the directory is staged beside whichever working directory the session tracks, so a single cd into a subtree leaves one there instead. Anchoring it would miss every copy but the root’s.

location: str = './.gitignore'

File path of the .gitignore to update, relative to the root of the repository.

sync: bool = True

Whether .gitignore sync is enabled for this project.

Projects that manage their own .gitignore and do not want the autofix job to overwrite it can set this to false.

class repomatic.config.LabelsConfig(content_rules=<factory>, extra=<factory>, extra_files=<factory>, file_rules=<factory>, sync=True)[source]

Bases: object

Nested schema for [tool.repomatic.labels].

content_rules: dict[str, list[str]]

Per-label patterns matched against an issue or pull request’s text.

The [tool.repomatic.labels.content-rules] table maps each label to the patterns that apply it, evaluated by apply-labels against the title and body. Any one pattern matching applies the label:

[tool.repomatic.labels.content-rules]
"🥭 mango" = ["mango", "papaya"]
"🐛 bug" = []

A bare pattern is a literal keyword, matched case-insensitively on word boundaries; the /regex/flags form passes a regex through instead, with i, m and s honored. An entry for a label the bundled defaults also carry replaces the default entry, and an empty list disables it (see repomatic.labels.DEFAULT_CONTENT_RULES).

extra: list[dict[str, str | bool | list[str]]]

Inline label definitions applied at sync time under the default profile.

Each entry is a mapping carrying labelmaker’s per-label specification: name (required), color (single color or multi-color list), description, create, update, enforce-case, rename-from and on-rename-clash. A rename-from list renames an existing label in place, preserving its issue and PR associations. Entries are serialized into a temporary TOML file as [[profiles.default.labels]] blocks and applied by labelmaker apply, so no extra-labels/*.toml file needs committing.

For label sets that need multiple profiles, commit a hand-written file under extra-labels/ or download one via extra-files instead.

extra_files: list[str]

URLs of additional label definition files (JSON, JSON5, TOML, or YAML).

Each URL is downloaded into extra-labels/ and applied separately by labelmaker. For inline definitions that need no external file, use extra instead.

file_rules: dict[str, list[str]]

Per-label globs matched against the paths a pull request changes.

The [tool.repomatic.labels.file-rules] table maps each label to the globs that apply it, evaluated by apply-labels against the changed files. The label applies when any changed file matches the glob set:

[tool.repomatic.labels.file-rules]
"🥭 mango" = ["orchard/**", "!orchard/generated/**"]

Globs follow the minimatch dialect (** crosses directories, {a,b} expands, a leading dot needs no special casing), and a !-prefixed entry subtracts from the label’s other globs the way a .gitignore line would. An entry for a label the bundled defaults also carry replaces the default entry, and an empty list disables it (see repomatic.labels.DEFAULT_FILE_RULES).

sync: bool = True

Whether label sync is enabled for this project.

Projects that manage their own repository labels and do not want the labels workflow to overwrite them can set this to false.

class repomatic.config.LintDepsConfig(allow=<factory>, comment_word_threshold=40)[source]

Bases: object

Nested schema for [tool.repomatic.lint-deps].

allow: dict[str, str]

Packages that may ship from somewhere other than PyPI, and why.

lint-deps blocks a release whose dependencies do not all resolve from the index its users will install from. A handful of arrangements are legitimate exceptions: a member of the same monorepo published under its own name, a private mirror an internal project genuinely targets. Name each one here, mapped to the reason it is safe:

[tool.repomatic]
lint-deps.allow = { papaya = "monorepo workspace member, published separately" }

A mapping rather than a list, deliberately: the reason is the point. An exemption without one is indistinguishable from a forgotten development shortcut six months later, which is the exact thing this gate exists to catch. The reason renders in the report and in the release PR banner, so an accepted exception stays visible instead of disappearing.

Per-package only, with no global off switch, following exclude-newer-package: an exemption narrow enough to name is one somebody weighed. Listing a package does not silence its transitive dependencies, which stay gated on their own.

comment_word_threshold: int = 40

Word count above which lint-deps warns about a floor comment.

A floor comment justifies the version in force: what breaks below it, and where the project would notice. It is not a running log of every earlier floor, which is what it turns into when each bump appends a paragraph and deletes nothing. lint-deps emits a non-fatal warning for every comment longer than this many words. Set to 0 to disable the check.

It starts at the same 40 words as changelog.bullet-word-threshold, and stays an independent knob: both cap a paragraph written for a reader who came looking for one fact, but a project that wants its floors terser than its release notes says so here alone.

class repomatic.config.MetricsConfig(charts=<factory>, colors=<factory>, forges=<factory>, predecessors=<factory>, skip=<factory>, store='./docs/assets/metrics.csv', subjects=<factory>, sync=False)[source]

Bases: object

Nested schema for [tool.repomatic.metrics].

charts: list[dict[str, str | list[str]]]

Charts to draw from the accumulated history, one array-of-tables entry each.

Each entry carries an output path, an optional metric (stars by default, and only a metric the store accrues can be charted), an optional mode (absolute, the default, or relative) measuring the horizontal axis, an optional scale (linear, the default, or logarithmic) measuring the vertical one, an optional only list naming the subjects to plot in draw order, and an optional title used as the chart’s accessible name:

[[tool.repomatic.metrics.charts]]
output = "./docs/assets/star-history.svg"

[[tool.repomatic.metrics.charts]]
mode = "relative"
output = "./docs/assets/star-history-by-age.svg"

[[tool.repomatic.metrics.charts]]
only = [ "apricot" ]
output = "./docs/assets/star-history-apricot.svg"

[[tool.repomatic.metrics.charts]]
scale = "logarithmic"
output = "./docs/assets/star-history-compared.svg"

The two axes are independent, and a chart comparing projects of different sizes usually wants both: mode = "relative" slides every curve onto a common origin, and scale = "logarithmic" keeps the smallest of them off the axis.

An entry omitting only plots every declared subject. Declaring none of these leaves the history accruing with nothing drawn from it, which is a valid way to collect first and decide later.

colors: dict[str, list[str]]

Per-subject [light, dark] hex pairs overriding the positional palette.

Hues are assigned from repomatic.metric_chart.SERIES_PALETTE in draw order, so a subject keeps its colour as long as the order holds. Pin one here when it must survive a reordering, or when a chart plots more curves than the palette holds:

[tool.repomatic.metrics.colors]
apricot = [ "#2a78d6", "#3987e5" ]
forges: dict[str, str]

Self-hosted forge instances, mapping each host to the software it runs.

Merged over repomatic.forge.FORGE_APIS, which only knows the three public hosts. A self-hosted instance is never guessed from its name, so an undeclared host raises rather than sampling nothing:

[tool.repomatic.metrics.forges]
"gitlab.example.org" = "gitlab"
"codeberg.example.org" = "forgejo"

Values are forgejo, github or gitlab; Gitea instances read as forgejo, whose API they share.

predecessors: dict[str, str]

Retired forerunners, mapping the subject they precede to their own repository.

A project that reopened under a new repository carries an audience it inherited rather than one it gathered, which a by-age chart would otherwise misreport as the fastest start in the field:

[tool.repomatic.metrics.predecessors]
papaya = "old-owner/papaya"

Drawn in the successor’s own hue to tie the two together, but dashed and never joined to it: the counts are independent tallies on separate repositories, so a continuous line would claim a running total no repository ever showed. The forerunner’s line stops where its successor’s begins.

skip: dict[str, str]

Subjects deliberately left unmeasured, mapped to the reason why.

A mapping rather than a list, following lint-deps.allow: the reason is the point. A project absent from both tables is an oversight a conformance test can report, while one listed here is a decision:

[tool.repomatic.metrics.skip]
papaya = "Ships in a distribution package with no public repository."

Nothing is sampled for them, and whatever renders the readings leaves their cells empty.

store: str = './docs/assets/metrics.csv'

Where the readings accumulate, one row per subject, metric and date.

subjects: dict[str, str]

Repositories to track, mapping each subject name to its repository.

The name labels the curve and keys its colour, so it is what a reader sees. A bare owner/name is GitHub; anything else is a full URL on whichever forge hosts it:

[tool.repomatic.metrics.subjects]
apricot = "apricot-org/apricot"
papaya = "https://gitlab.com/papaya/papaya"

Every subject is read for every metric its forge answers. The two deep collectors are GitHub-only and skip the rest with a note: an exact star reconstruction reads per-star timestamps, and the archive backfill mines github.com pages.

sync: bool = False

Whether sample-metrics records readings for this repository.

Opt-in, and the gate on the metrics.yaml workflow: a repository tracking nothing should not carry a weekly job, and an accumulating store is a commitment a maintainer makes deliberately.

class repomatic.config.SyncRunnerImagesConfig(ignore=<factory>)[source]

Bases: object

Nested schema for [tool.repomatic.sync-runner-images].

ignore: list[str]

Runner labels never to propose, whatever GitHub announces about them.

A sync-* job regenerates on every push, so a proposal declined by closing its pull request comes back on the next one. Without somewhere to record the decision, the only way to stop a proposal already considered and rejected is to disable the whole operation. Naming the label here is the one-line commit that makes a “no” stick:

[tool.repomatic.sync-runner-images]
# 26.04 stays out until its capacity settles: queue time matters more here
# than the compute it wins.
ignore = [ "ubuntu-26.04", "ubuntu-26.04-arm" ]

Applies to both shapes: an ignored label is neither probed when it arrives nor proposed as a successor when something retires onto it.

class repomatic.config.TestMatrixConfig(exclude=<factory>, full_include=<factory>, include=<factory>, remove=<factory>, replace=<factory>, unstable=<factory>, variations=<factory>)[source]

Bases: object

Nested schema for [tool.repomatic.test-matrix].

Keys inside replace and variations are GitHub Actions matrix identifiers (e.g., os, python-version) and must not be normalized to snake_case. Click Extra’s click_extra.normalize_keys = False metadata on the parent field prevents this.

exclude: list[dict[str, str]]

Extra exclude rules applied to both full and PR test matrices.

Each entry is a dict of GitHub Actions matrix keys (like {"os": "windows-11-arm"}) that removes matching combinations. Additive to the upstream default excludes.

full_include: list[dict[str, str]]

Full-matrix-only job rows, added as standalone matrix combinations.

Each entry is a dict of GitHub Actions matrix keys fully describing one job (like {“os”: “ubuntu-26.04-arm”, “python-version”: “3.10”, “click-version”: “8.3.1”}`). Unlike``include`, these are appended as independent rows of the full matrix, never merged into the base cross-product, so a cell can’t overwrite a shipped-config job that shares its os and python-version. Keys left out inherit the matrix defaults (the single-key include entries, plus state: stable), so a cell lists only what differs from the shipped configuration.

Use this for heterogeneous coverage, like pinning each release of a dependency to its own runner and Python, where carving the same shape from the base cross-product with exclude would take many rules. Like variations and unstable, it touches the full matrix only; the PR matrix stays a curated reduced set. Adding any entry makes the full matrix emit as a flat job list ({"include": [...]}), which GitHub runs verbatim with no cross-product expansion.

include: list[dict[str, str]]

Extra include directives applied to both full and PR test matrices.

Each entry is a dict of GitHub Actions matrix keys that adds or augments matrix combinations. Additive to the upstream default includes.

Because includes apply to both matrices, a directive whose keys are not PR base axes is risky. In the PR matrix only os and python-version are base axes, so a key like click-version (injected by another include) has nothing to match and GitHub’s expansion adds the directive to every PR job, overwriting it. To flag a value continue-on-error, prefer unstable over an include carrying state: unstable.

remove: dict[str, list[str]]

Per-axis value removals applied to both full and PR test matrices.

Outer key is the variation/axis ID (e.g., os, python-version). Inner list contains values to drop from that axis. Applied after replacements but before excludes, includes, and variations.

replace: dict[str, dict[str, str]]

Per-axis value replacements applied to both full and PR test matrices.

Outer key is the variation/axis ID (e.g., os, python-version). Inner dict maps old values to new values. Applied before removals, excludes, includes, and variations.

unstable: list[dict[str, str]]

Full-matrix-only combinations to flag continue-on-error in CI.

Each entry is a dict of GitHub Actions matrix keys (like {"click-version": "main"}). Every full-matrix combination matching an entry gets a state: unstable value, which tests.yaml reads to set continue-on-error. Like variations, this applies to the full matrix only; the PR matrix stays a curated stable set.

Prefer this over an include entry carrying state: unstable. include applies to both matrices, and in the PR matrix a key like click-version is not a base axis (another include injects it), so GitHub’s expansion would add the directive to every PR job and overwrite it. unstable only touches the full matrix, sidestepping that hijack.

variations: dict[str, list[str]]

Extra matrix dimension values added to the full test matrix only.

Each key is a dimension ID (e.g., os, click-version) and its value is a list of additional entries. For existing dimensions, values are merged with the upstream defaults. For new dimension IDs, a new axis is created. Only affects the full matrix; the PR matrix stays a curated reduced set.

class repomatic.config.VulnerableDepsConfig(sources=<factory>, sync=True)[source]

Bases: object

Nested schema for [tool.repomatic.vulnerable-deps].

sources: list[str]

Advisory databases to consult for known vulnerabilities.

Recognized values:

  • "uv-audit": PyPA Advisory Database via uv audit (works locally and in CI without a GitHub token).

  • "github-advisories": GitHub Advisory Database via the repository’s Dependabot alerts (CI-only, requires a token with Dependabot alerts: Read-only).

Sources are unioned and deduplicated per package by advisory identity: entries sharing an advisory_id or a cross-referenced CVE/GHSA/PYSEC alias are merged. Repositories that distrust GHSA, or have no Dependabot alerts enabled, can opt out with sources = ["uv-audit"].

sync: bool = True

Whether the fix-vulnerable-deps job is enabled for this project.

Projects that manage their own vulnerability remediation flow can set this to false to skip the autofix job.

class repomatic.config.WorkflowConfig(source_paths=None, extra_paths=<factory>, ignore_paths=<factory>, paths=<factory>, sync=True)[source]

Bases: object

Nested schema for [tool.repomatic.workflow].

source_paths: list[str] | None = None

Source code directory names for workflow trigger paths: filters.

When set, thin-caller and header-only workflows include paths: filters using these directory names (as name/** globs) alongside universal paths like pyproject.toml and uv.lock.

When None (default), source paths are auto-derived from [project.name] in pyproject.toml by replacing hyphens with underscores, the universal Python convention. For example, name = "extra-platforms" automatically uses ["extra_platforms"].

extra_paths: list[str]

Literal entries to append to every workflow’s paths: filter.

Applies to thin-caller and header-only sync. Useful for repo-specific files that should re-trigger CI but are not detected by the canonical paths: filter (e.g., install.sh, dotfiles/**).

Per-workflow overrides in paths ignore this list: when an entry exists for a given filename, that entry is treated as the complete list.

ignore_paths: list[str]

Literal entries to strip from every workflow’s paths: filter.

Useful for canonical entries that don’t exist downstream (e.g., tests/**, uv.lock in repos with no Python tests or lockfile). Match is by exact string equality. Applies before extra_paths.

Per-workflow overrides in paths ignore this list.

paths: dict[str, list[str]]

Per-workflow override of the paths: filter, keyed by filename.

When a workflow filename appears here, its paths: blocks (in push, pull_request, etc.) are replaced wholesale with the listed entries. source_paths, extra_paths, and ignore_paths do not apply when a per-workflow override is set: the list is treated as authoritative.

Override only takes effect on triggers that already have a paths: filter in the canonical workflow. Workflows without paths: upstream keep their unrestricted trigger semantics.

Example:

[tool.repomatic.workflow.paths]
"tests.yaml" = ["install.sh", "packages.toml", ".github/workflows/tests.yaml"]
sync: bool = True

Whether workflow sync is enabled for this project.

Projects that manage their own workflow files and do not want the autofix job to sync thin callers or headers can set this to false.

class repomatic.config.Config(abandoned_versions=<factory>, action_pins_sync=True, agent_location='./claude.md', awesome_template_sync=True, binaries_sync=True, bumpversion_sync=True, cache=<factory>, changelog_archive_location='', changelog_bullet_word_threshold=40, changelog_location='./changelog.md', dep_sources_sync=True, dependency_graph=<factory>, dev_release_sync=True, docs=<factory>, exclude=<factory>, flavor=<factory>, gitignore=<factory>, include=<factory>, labels=<factory>, lint_deps=<factory>, mailmap_sync=True, manpages_asset_name='', manpages_script='', metrics=<factory>, minimum_release_age='1 week', notification_unsubscribe=False, nuitka_dev_targets=<factory>, nuitka_enabled=True, nuitka_entry_points=<factory>, nuitka_extras=<factory>, nuitka_nofollow_imports=<factory>, nuitka_unstable_targets=<factory>, pypi_package_history=<factory>, release_assets=<factory>, settings_location='./.claude/settings.json', setup_guide=True, site_cloudflare_compatibility_date='', site_cloudflare_placement='', site_cloudflare_project='', site_deploy='github-pages', skills_location='./.claude/skills/', sphinx_builder='html', subagents_location='./.claude/agents/', sync_runner_images=<factory>, test_matrix=<factory>, tool_versions_sync=True, uv_lock_sync=True, vulnerable_deps=<factory>, workflow=<factory>, workflow_pins_sync=True)[source]

Bases: object

Configuration schema for [tool.repomatic] in pyproject.toml.

This dataclass defines the structure and default values for repomatic configuration. Each field has a docstring explaining its purpose.

abandoned_versions: list[str]

Versions documented in the changelog but never published.

A version reached only its [changelog] Release vX.Y.Z freeze and was then skipped per CLAUDE.md § Skip and move forward (botched build, broken artifact, bad metadata) without rewriting history. List those versions here so lint-changelog reports them as skipped (an info log line) instead of flagging them every run as X.Y.Z: not found on PyPI. Applies to both PyPI lookups and the git-tag fallback.

action_pins_sync: bool = True

Whether the sync-action-pins job is enabled for this project.

Bumps SHA-pinned GitHub Actions (uses: owner/repo@<sha> # vX.Y.Z) to the latest release passing the minimum-release-age cooldown. Projects that pin actions by hand can set this to false.

agent_location: str = './claude.md'

Path to the agent’s instructions file, relative to the repository root.

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Only the agent component writes here, merging the audience-tagged sections it owns into whatever the file already holds. Point it at AGENTS.md for the cross-agent convention, or anywhere else the file actually lives: a repository keeping its instructions outside the root (./dotfiles/.agents/AGENTS.md) is the case this exists for.

Caution

One character from subagents_location, and they write different things: this one an instructions document, that one a directory of subagent definitions. The components are agent and subagents for the same reason.

awesome_template_sync: bool = True

Whether awesome-template sync is enabled for this project.

Repositories whose name starts with awesome- get their boilerplate synced from files bundled in repomatic. Set to false to opt out.

binaries_sync: bool = True

Whether the release pipeline records released binaries into the repository.

When enabled, the scan-virustotal release job regenerates the binaries catalog (docs/binaries.md and docs/assets/binaries.csv) and pushes it, along with the scan history (docs/assets/virustotal-scans.csv), straight to the default branch without a pull request: the release-lane exception documented in docs/operation-contracts.md. Set to false to keep the repository untouched: binaries are still scanned on VirusTotal (seeding AV vendor databases), but no catalog page, CSV, or scan record is committed.

bumpversion_sync: bool = True

Whether bumpversion config sync is enabled for this project.

Projects that manage their own [tool.bumpversion] section and do not want the autofix job to overwrite it can set this to false.

cache: CacheConfig

Binary cache configuration.

changelog_archive_location: str = ''

File path of the changelog archive, relative to the root of the repository.

The archive holds older release sections split out of the live changelog to keep it small. Empty (the default) disables archive handling.

When set, lint-changelog treats versions documented in the archive as present, so they are neither reported nor re-inserted as orphans (versions found on PyPI, GitHub, or git tags but missing from the changelog). The archive is frozen: its released entries are immutable and are not re-validated against their canonical release dates.

changelog_bullet_word_threshold: int = 40

Word count above which lint-changelog warns about a changelog bullet.

A changelog entry is a release note, not a commit message: ideally one short sentence stating what changed (see CLAUDE.md § Changelog entry length). lint-changelog emits a non-fatal warning for every bullet in the unreleased section longer than this many words, nudging verbose, implementation-heavy entries back toward a user-facing summary. Released sections are immutable and never flagged. Set to 0 to disable the check.

changelog_location: str = './changelog.md'

File path of the changelog, relative to the root of the repository.

dep_sources_sync: bool = True

Whether the sync-dep-sources updater is enabled for this project.

Swaps a dependency tracked from a git branch back to its released version once the release named by its .dev version floor ships on PyPI (see repomatic.dep_sources for the managed idiom). Projects that manage [tool.uv.sources] overrides by hand can set this to false.

dependency_graph: DependencyGraphConfig

Dependency graph generation configuration.

dev_release_sync: bool = True

Whether dev pre-release sync is enabled for this project.

Projects that do not want a rolling draft pre-release maintained on GitHub can set this to false.

docs: DocsConfig

Sphinx documentation generation configuration.

exclude: list[str]

Additional components and files to exclude from repomatic operations.

Additive to the default exclusions (agents, labels, skills). Bare names exclude an entire component (e.g., "workflows"). Qualified component/identifier entries exclude a specific file within a component (e.g., "workflows/debug.yaml", "skills/repomatic-audit", "labels/labels.toml").

Affects repomatic init, workflow sync, and workflow create. Explicit CLI positional arguments override this list.

flavor: FlavorConfig

Which agent and CI ecosystem this repository targets.

gitignore: GitignoreConfig

.gitignore sync configuration.

include: list[str]

Components and files to force-include, overriding default exclusions.

Use this to opt into components that are excluded by default (agents, labels, skills). Each entry is subtracted from the effective exclude set (defaults + user exclude) and bypasses RepoScope filtering, so scope-restricted components (like awesome-only skills or Python-only publish-pypi-action) are included regardless of repository type. Qualified entries (component/file) implicitly select the parent component. Same syntax as exclude.

labels: LabelsConfig

Repository label sync configuration.

lint_deps: LintDepsConfig

Dependency shippability gate configuration.

mailmap_sync: bool = True

Whether .mailmap sync is enabled for this project.

Projects that manage their own .mailmap and do not want the autofix job to overwrite it can set this to false.

manpages_asset_name: str = ''

Filename stem (without the .tar.gz extension) for the man-page tarball uploaded to the GitHub release.

Defaults to <package-name>-manpages when left empty and manpages.script is set. Has no effect when manpages.script is empty.

manpages_script: str = ''

Click command target whose tree gets rendered as roff .1 files and attached as a tarball asset on every GitHub release.

Same shape the click-extra wrap --man CLI accepts: a module:function path (preferred for projects whose console-script entry point dispatches through a wrapper), an entry-point name, a .py file path, or a plain importable module name. Leave empty to disable release-attached man pages.

metrics: MetricsConfig

What forges say about the repositories this project tracks, over time.

minimum_release_age: str = '1 week'

Stabilization window before a new upstream release is adopted.

Shared cooldown for the sync-tool-versions, sync-action-pins, and sync-workflow-pins jobs: a release is only proposed once it has been public for at least this long, giving upstream time to yank a bad cut. It also gates repomatic run’s ad-hoc installs at run time, so their transitive trees honor the same window: uvx tools via uv’s --exclude-newer, npm tools via npm’s min-release-age. repomatic init honors it too: the derived upstream workflow pin steps back to the newest release past the window (override with --no-cooldown). The GitHub/PyPI/npm counterpart to uv’s exclude-newer (which guards sync-uv-lock). Accepts the same friendly durations (8 days, 2 weeks, 36 hours). Set to 0 days to adopt releases immediately.

notification_unsubscribe: bool = False

Whether the unsubscribe-threads workflow is enabled.

Notifications are per-user across all repos. Enable on the single repo where you want scheduled cleanup of closed notification threads. Requires a classic PAT with notifications scope stored as REPOMATIC_NOTIFICATIONS_PAT.

nuitka_dev_targets: list[str]

Nuitka build targets compiled on ordinary pushes, as a canary.

An ordinary push to the default branch rebuilds binaries only for these targets: enough to catch a compilation break early, while freeing runner slots the full fleet would occupy on every code push just to refresh the rolling dev pre-release (a draft). The full target roster still builds on release commits, on the weekly schedule trigger, and on workflow_dispatch. Defaults to ["linux-arm64"], the fastest and cheapest builder. Set to [] to skip dev builds entirely.

nuitka_enabled: bool = True

Whether Nuitka binary compilation is enabled for this project.

Projects with [project.scripts] entries that are not intended to produce standalone binaries (e.g., libraries with convenience CLI wrappers) can set this to false to opt out of Nuitka compilation.

nuitka_entry_points: list[str]

Which [project.scripts] entry points produce Nuitka binaries.

List of CLI IDs (e.g., ["mpm"]) to compile. When empty (the default), deduplicates by callable target: keeps the first entry point for each unique module:callable pair. This avoids building duplicate binaries when a project declares alias entry points (like both mpm and meta-package-manager pointing to the same function).

nuitka_extras: list[str]

[project.optional-dependencies] extras to install before the Nuitka build.

List of extra names (like ["sbom"]) to sync into the build venv before invoking Nuitka. By default the binary build only sees the project’s base dependencies, which matches a bare pip install <package> and excludes optional features. Listing an extra here calls uv sync –frozen –extra <name> before the Nuitka build so the binary can bundle the optional feature’s third-party packages (paired with --include-package in [tool.nuitka] for imports guarded behind try/except).

nuitka_nofollow_imports: list[str]

Module names Nuitka must not follow into the compiled binary.

Each name is forwarded as a --nofollow-import-to flag by repomatic run nuitka`. Defaults to``[“tkinter”]``:boltons.ecoutils` (in the dependency tree of every click-extra CLI) probes tkinter inside a guarded ``try/ except import, which otherwise drags the whole Tcl/Tk stack into every binary. Excluded modules raise ImportError when imported at run time, which guarded imports absorb. GUI projects that really ship tkinter can set this to [].

nuitka_unstable_targets: list[str]

Nuitka build targets allowed to fail without blocking the release.

List of target names (e.g., ["linux-arm64", "windows-x64"]) that are marked as unstable. Jobs for these targets will be allowed to fail without preventing the release workflow from succeeding.

pypi_package_history: list[str]

Former PyPI package names for projects that were renamed.

When a project changes its PyPI name, older versions remain published under the previous name. List former names here so lint-changelog can fetch release metadata from all names and generate correct PyPI URLs.

release_assets: list[str]

Extra asset filenames attached to every GitHub release.

Each listed file must be produced by a job the consumer defines in its own release workflow (alongside the build lane the engine call already gates on) and uploaded as a run artifact named release-asset-<filename>. The engine’s extra-assets job downloads the artifacts, attests them with the same provenance chain as the compiled binaries, and attaches them to the release draft before publication locks it (GitHub immutable releases).

The build code stays in the downstream repository as regular workflow code, reviewed and linted there: the engine never executes consumer-supplied commands. Filenames must be space-free, as they travel through a space-separated job environment variable. Leave empty to disable, which keeps the job silent.

settings_location: str = './.claude/settings.json'

Path to the agent’s project settings file, relative to the repository root.

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Only the plugin component writes here, merging the marketplace and enablement keys it owns into whatever the file already holds.

setup_guide: bool = True

Whether the setup guide issue is enabled for this project.

Projects that do not need REPOMATIC_PAT or manage their own PAT setup can set this to false to suppress the setup guide issue.

site_cloudflare_compatibility_date: str = ''

Workers runtime date the Cloudflare Pages project is pinned to.

A YYYY-MM-DD date, compared and enforced by repomatic cloudflare-pages against the live project’s deployment_configs, on both the production and preview environments. Inert while the project has no Pages Functions, which is exactly how it drifts unnoticed: the value only starts mattering the moment a Function is added, long after anyone last chose it. Empty (the default) leaves the live value unmanaged.

This is server-side state, not the wrangler.toml key of the same name: Cloudflare honours the project’s own configuration, and the file only matters to a build that a Direct Upload project never runs. lint-repo warns when a committed wrangler.toml disagrees, so the repository states one value rather than two.

site_cloudflare_placement: str = ''

Smart Placement mode declared for the Cloudflare Pages project.

smart or off, compared and enforced by repomatic cloudflare-pages on both environments. For a static site it changes nothing measurable and costs nothing; declaring it means the dashboard toggle stops looking like an accident. Empty (the default) leaves the live value unmanaged.

site_cloudflare_project: str = ''

Name of the Cloudflare Pages project the site deploys into.

Empty (the default) names the project after the repository, which is what the deploy job falls back to. Set it when the project predates repomatic or otherwise cannot carry the repository’s name: renaming a live Pages project would move the <project>.pages.dev hostname every custom domain CNAMEs through.

site_deploy: str = 'github-pages'

Where this repository’s built site is published.

github-pages, the default, has the Docs workflow upload the Sphinx tree as a Pages artifact and deploy it with the repository’s own OIDC identity: no stored credential, and nothing to configure beyond enabling Pages.

cloudflare-pages uploads it to a Cloudflare Pages project instead, named per site.cloudflare-project, through wrangler pages deploy. That path needs one repository secret, CLOUDFLARE_API_TOKEN, and it trades the OIDC deploy for a long-lived token: the Docs workflow’s monthly run is what surfaces its expiry, since Cloudflare warns about neither an approaching lapse nor a passed one.

A property of the site rather than of Sphinx. A repository whose site is built by its own workflow (a Pelican blog, a hand-rolled static tree) declares the target here too: that is what turns on the credential checks, the setup-guide step and the Cloudflare drift job for it, even though the Docs workflow’s own Sphinx build never runs.

Choose Cloudflare for what the edge can do rather than for speed. A custom domain on Cloudflare Pages carries its own certificate, so the zone’s apex can be proxied, which is what a _redirects file, a real 404.html and any edge rule on the apex all depend on.

skills_location: str = './.claude/skills/'

Directory prefix for skill folders, relative to the repository root.

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Skill files are written as {skills_location}/{skill-id}/SKILL.md. Useful for repositories where .claude/ is not at the root (like dotfiles repos that store configs under a subdirectory).

sphinx_builder: str = 'html'

Sphinx builder producing the deployed documentation site.

The default html writes page.html, so the site serves /page.html. Setting it to dirhtml writes page/index.html instead, so the same page serves at /page/ and the published URLs carry no extension, which is the shape search engines and most static hosts expect.

The one Sphinx setting a project cannot make in its own conf.py, hence a config key: the builder is chosen on the command line, and docs.yaml is what runs it. Switching an already-published site republishes every URL it has: the old paths stop existing, so the repository’s own absolute self-links (readme, packaging specs) move in the same commit, and whatever fronts the site redirects the old ones.

subagents_location: str = './.claude/agents/'

Directory prefix for subagent definitions, relative to the repository root.

Left unset, it follows [tool.repomatic.flavor] agent; setting it explicitly overrides that.

Subagent files are written as {subagents_location}/{agent-id}.md. Useful for repositories where .claude/ is not at the root (like dotfiles repos that store configs under a subdirectory).

sync_runner_images: SyncRunnerImagesConfig

Runner image pull request configuration.

test_matrix: TestMatrixConfig

Per-project customizations for the GitHub Actions CI test matrix.

Keys inside this section are GitHub Actions matrix identifiers (e.g., os, python-version) and must not be normalized to snake_case.

tool_versions_sync: bool = True

Whether the sync-tool-versions job is enabled for this project.

Bumps every tool in the repomatic run registry to the latest release passing the minimum-release-age cooldown (GitHub releases for binary tools, PyPI for the rest), recomputing binary checksums in the same pass. Projects that pin tool versions by hand can set this to false.

uv_lock_sync: bool = True

Whether uv.lock sync is enabled for this project.

Projects that manage their own lock file strategy and do not want the sync-uv-lock job to run uv lock --upgrade can set this to false.

vulnerable_deps: VulnerableDepsConfig

Vulnerable dependency detection and remediation configuration.

workflow: WorkflowConfig

Workflow sync configuration.

workflow_pins_sync: bool = True

Whether the sync-workflow-pins job is enabled for this project.

Bumps version literals embedded in workflow YAML (npm pkg@x installs and uvx '<pkg>==x' PyPI pins) to the latest release passing the minimum-release-age cooldown. Projects that pin these by hand can set this to false.

repomatic.config.SUBCOMMAND_CONFIG_FIELDS: Final[frozenset[str]] = frozenset({'abandoned_versions', 'action_pins_sync', 'agent_location', 'awesome_template_sync', 'bumpversion_sync', 'cache', 'changelog_archive_location', 'changelog_bullet_word_threshold', 'changelog_location', 'dep_sources_sync', 'dependency_graph', 'dev_release_sync', 'docs', 'exclude', 'flavor', 'gitignore', 'include', 'labels', 'lint_deps', 'mailmap_sync', 'metrics', 'minimum_release_age', 'notification_unsubscribe', 'nuitka_enabled', 'nuitka_nofollow_imports', 'pypi_package_history', 'settings_location', 'setup_guide', 'site_cloudflare_compatibility_date', 'site_cloudflare_placement', 'skills_location', 'subagents_location', 'sync_runner_images', 'test_matrix', 'tool_versions_sync', 'uv_lock_sync', 'vulnerable_deps', 'workflow', 'workflow_pins_sync'})

Config fields consumed directly by subcommands, not needed as metadata outputs.

These fields are read directly from [tool.repomatic] in pyproject.toml by their respective subcommands (e.g. dep-graph), so they no longer need to be passed through workflow metadata outputs.

repomatic.config.escape_type_for_gfm_table(ftype)[source]

Escape outer brackets of nested generics for raw GFM table cells.

Nested generics like list[dict[str, str]] would otherwise be interpreted by mdformat as a markdown link reference and re-escaped on every reformat. Escaping the outermost brackets up front keeps the cell stable under mdformat. Simple generics like list[str] have no nested brackets and stay unescaped.

Apply this only when the value lands directly in a raw GFM table cell (e.g. CLI show-config output). Do not apply when wrapping the value in inline code backticks: inside a code span, backslashes are literal characters in CommonMark and would render visibly as \[.

Return type:

str

repomatic.config.CONFIG_REFERENCE_HEADER_DEFS: tuple[tuple[str, str], ...] = (('Option', 'option'), ('Type', 'type'), ('Default', 'default'), ('Description', 'description'))

Column definitions for the [tool.repomatic] configuration reference table.

repomatic.config.config_reference()[source]

Build the [tool.repomatic] configuration reference as table rows.

Introspection comes from click-extra’s schema_field_infos() (dotted kebab-case keys, type annotations, defaults, attribute-docstring summaries); this wrapper only applies the Markdown presentation of the show-config table. Returns a list of (option, type, default, description) tuples suitable for click_extra.table.print_table.

Return type:

list[tuple[str, str, str, str]]

repomatic.config.load_repomatic_config(pyproject_data=None)[source]

Load [tool.repomatic] config merged with Config defaults.

Delegates to click-extra’s schema-aware dataclass instantiation, which handles normalization, flattening, nested dataclasses, and opaque field extraction automatically based on field metadata and type hints.

Parameters:

pyproject_data (dict[str, Any] | None) – Pre-parsed pyproject.toml dict. If None, reads and parses pyproject.toml from the current working directory.

Return type:

Config