This directory ships generic, safe examples for the self-host private config directory
(LOOPOVER_REPO_CONFIG_DIR, default /config in the Docker image / docker-compose.yml). It
contains no real policy, thresholds, logins, or repo names — copy what you need into your own
mounted config directory and edit it there (never in this repo).
See TEMPLATES.md for the full template catalog (minimal + exhaustive
loopover.yml starters, public-vs-private usage, and fleet notes for loopover,
awesome-claude, and metagraphed without committing private policy).
The private config directory is read by src/selfhost/private-config.ts and is kept out of the
public GitHub repo on purpose: contributors can read a public manifest, so anti-abuse thresholds,
maintainer/admin allowlists, autonomy dials, and model/effort settings belong here instead, where
only the self-host operator can see them.
The canonical manifest filename is .loopover.yml.
For a repo owner/repo, the reader tries, in priority order:
${LOOPOVER_REPO_CONFIG_DIR}/owner__repo/.loopover.yml # 1. owner-qualified folder (recommended)
${LOOPOVER_REPO_CONFIG_DIR}/repo/.loopover.yml # 2. bare repo-name folder
${LOOPOVER_REPO_CONFIG_DIR}/owner__repo.yml # 3. flat file (back-compat, brand-agnostic)
${LOOPOVER_REPO_CONFIG_DIR}/.loopover.yml # 4. global default, shared by every repo
${LOOPOVER_REPO_CONFIG_DIR}/_shared/.loopover.yml # 5. shared base (#1959), lowest priority
.yaml and .json are accepted everywhere .yml is. Every one of these files uses the exact
same schema as the public manifest — see loopover.full.yml (or
.loopover.yml.example at the repo root) for the exhaustive,
field-by-field reference. For the smallest safe starter, copy
loopover.minimal.yml (or .loopover.minimal.yml)
to your repo root as .loopover.yml or into your private mount and customize from there.
From highest to lowest priority:
- Private per-repo file, deep-merged over 2 and 3 when more than one exists (see below) — or used alone when it is the only private layer present.
- Private global default (
${LOOPOVER_REPO_CONFIG_DIR}/.loopover.yml) — deep-merged under 1 when both exist; used alone when a repo has no per-repo file of its own and no shared base is mounted. - Private shared base (
${LOOPOVER_REPO_CONFIG_DIR}/_shared/.loopover.yml, #1959) — the lowest-priority private layer, deep-merged under both 1 and 2. An operator running many repos writes a house review policy (e.g. a defaultreview.tone,path_filters, orexclude_paths) here once instead of copy-pasting it into every repo's per-repo file or the global default..yaml/.jsonare accepted, same as every other candidate. Absent (the default, common case) ⇒ byte-identical behavior to the pre-#1959 2-layer chain. - When none of the three private layers above exists, the loader falls back to the public
repo
.loopover.yml(or.github/loopover.yml) fetched from GitHub. - Dashboard/API-stored settings for the repo.
- Built-in safe defaults.
Layers 1-3 are evaluated together as one private-config layer: if any of a per-repo file, a global default, or a shared base exists privately, the public file in layer 4 is never consulted for that repo. This is unchanged from the original private-config behavior (#1390) — only the interaction among the three private layers is new (the per-repo/global interaction shipped first; the shared base is the newest, lowest layer, #1959).
This chain governs per-repo review policy only. A separate, lower-level set of deployment
environment variables (LOOPOVER_REVIEW_* flags, AI provider keys/models, self-host runtime
knobs, etc.) configures the deployment itself and sits underneath all 5 layers above — a
.loopover.yml/private-config value never overrides an operator's env-level kill-switch, it only
narrows what's already permitted. See the generated, always-current
SELFHOST_ENV_REFERENCE_ROWS (built by
npm run selfhost:env-reference from every env.SOMETHING read in the codebase) for the full list.
When two or more of {a per-repo file, a global default, a shared base} exist for a repo, they are merged in ascending priority — shared base first, global default overlaid on top of that, then the per-repo file overlaid on top of that (see Shared base layer below for the shared base specifically):
- Nested mappings (
gate,settings,review,features,contentLane, and their own nested blocks likegate.readinessorgate.aiReview) merge key by key. A higher-priority file only needs to mention the keys it wants to change; everything else is inherited from the next layer down. - Arrays (
wantedPaths,preferredLabels,testExpectations,review.pathInstructions,review.excludePaths,contentLane.duplicateKeyFields, etc.) replace wholesale — a higher-priority array is never concatenated with a lower layer's. - An explicit
nullat a key in a higher-priority file always overrides a lower layer's value there. This clears a setting wherever the manifest parser already treats an explicitnullas "off"/"clear" — e.g.settings.contributorOpenPrCap,settings.contributorOpenIssueCap,settings.accountAgeThresholdDays, and the enforcement label names (settings.blacklistLabel/contributorCapLabel/reviewNagLabel, see below) — and is a harmless no-op (equivalent to omitting the key) everywhere else. - If any layer fails to parse (or is malformed/oversized), it is dropped from the merge and the remaining, still-valid layers merge as if it were never mounted; a still-good layer's policy is never silently discarded just because another layer is broken, and a broken layer never blocks a review.
.loopover.yml (global default, at the config dir root):
settings:
contributorOpenPrCap: 3
autoCloseExemptLogins:
- your-admin-login
gate:
enabled: true
duplicates: blockowner__repo/.loopover.yml (per-repo override — only touches what's different for this repo):
gate:
enabled: true
# duplicates is inherited from global (still "block") — not repeated here.
aiReview:
mode: advisoryThe effective config for owner/repo has gate.duplicates: block (from global),
gate.aiReview.mode: advisory and gate.enabled: true (from the per-repo file), and
settings.contributorOpenPrCap: 3 plus the exempt login (both from global).
# owner__repo/.loopover.yml
settings:
contributorOpenPrCap: null # explicitly clears the global cap of 3 for this repo onlyShared anti-abuse mechanisms (the review-request-nag cooldown, the contributor open-item cap) exempt configured logins on top of the standing owner/admin/automation-bot exemption:
# .loopover.yml (global default)
settings:
autoCloseExemptLogins:
- your-trusted-regularAn operator running many repos through the same self-host instance can express one house
review policy — e.g. a default review.tone, a baseline path_filters/wantedPaths set, or
common exclude_paths — once, instead of copy-pasting it into every repo's per-repo file or
even the global default. That policy lives at:
${LOOPOVER_REPO_CONFIG_DIR}/_shared/.loopover.yml
(.yaml/.json also accepted, same lookup order as every other candidate — see
shared.loopover.yml for a starter). It sits at the lowest
priority of the three private layers: a per-repo file overlays a
global default, which overlays the shared base — the shared base fills in only the fields a higher
layer is silent on. This is the exact same deep-merge helper and array-replace/explicit-null-clear
semantics described above, folded across one more layer; it is not a new merge algorithm.
Absent shared base is the default, common case — with no _shared/.loopover.yml mounted,
behavior is byte-identical to the pre-#1959 2-layer chain. A
malformed or unreadable shared file fails safe exactly like a malformed per-repo or global file
always has: it is dropped from the merge and the remaining, still-valid layers combine as if it
were never mounted — a broken shared base never blocks a review. When a shared review: block
contributes, the parsed manifest carries review.sharedConfigSource (runtime provenance only,
#2046) with the relative path of the shared file that supplied the base layer.
_shared/.loopover.yml (shared base — one house policy for every repo on this instance):
review:
tone: friendly-terse
gate:
duplicates: block.loopover.yml (global default — this instance's own baseline, silent on review.tone):
gate:
enabled: trueowner__repo/.loopover.yml (per-repo override — only touches what's different for this repo):
gate:
enabled: true
# duplicates is inherited from the shared base (still "block") — neither this file nor the
# global default repeats it.The effective config for owner/repo has review.tone: friendly-terse (from the shared base;
neither global nor the per-repo file mentions it), gate.duplicates: block (from the shared base,
passed through untouched by global), and gate.enabled: true (set the same way by both global and
the per-repo file).
Two autonomy classes govern every label the bot can apply, and they are independent:
closeauthorizes the terminal merge/close/hold disposition and the anti-abuse enforcement labels tied to it (blacklist/contributor-cap/review-nag) — a label likeover-contributor-limitis inseparable metadata on its close, so it never needs a separate grant. Setsettings.contributorCapLabel/blacklistLabel/reviewNagLabelto explicitnull(not just omitted) to close/hold without applying any label at all.review_state_labelauthorizes the bot's own disposition-communication labels only —ready-to-merge/changes-requested/manual-review/migration-collisionby default. These are advisory commentary about the bot's own verdict, not enforcement, and default OFF like every autonomy class. For a one-shot review model, leave this at the default so a PR merges, closes, or holds through the required gate check alone — set it toautoonly if you specifically want that commentary as GitHub labels too.
All disposition labels are configurable under settings.*Label, and explicit null disables the
label without disabling the underlying merge/close/hold decision. Hard path guardrails are
config-as-code only and safe by default (#3943): settings.hardGuardrailGlobs is ADDED to a fixed,
built-in invariant set (DEFAULT_HARD_GUARDRAIL_GLOBS in src/review/guardrail-config.ts) — an
ordinary edit can only ever widen guardrail coverage, never shrink it. Set
settings.hardGuardrailGlobsOverridesInvariants: true to opt out of that safety net entirely: once
set, hardGuardrailGlobs REPLACES (never merges with) the built-in set or any lower-layer private
global default, including an explicit [] to disable path guardrails altogether — a deliberate,
separately-visible decision, not a side effect of trimming the list.
# .loopover.yml (global default) — recommended one-shot baseline
settings:
autonomy:
close: auto
# review_state_label intentionally omitted (defaults to observe)The broad autonomy.label class still exists but no longer gates any of the above — it is not
required for either family and applies to nothing on its own.
settings.reviewNagMonitoredMentions extends the @loopover-ping review-nag cooldown
(reviewNagPolicy/reviewNagMaxPings/reviewNagCooldownDays/reviewNagLabel — same settings,
one shared policy) to also throttle a thread's own author repeatedly @-mentioning a configured
maintainer login, counted independently per login and independently of the @loopover counter:
# .loopover.yml (global default)
settings:
reviewNagPolicy: hold
reviewNagMonitoredMentions:
- your-maintainer-loginOwner/admin/automation-bot logins and anyone on autoCloseExemptLogins are always exempt, and only
the thread's own author is ever throttled — a third party mentioning the login on someone else's
PR/issue never counts.
settings.linkedIssueLabelPropagation copies a label from a linked/closing issue onto the PR when
the issue already carries it — the only mechanism that can ever select a maintainer-reward or
moderation-weighted label; it is never inferred from a PR's title, changed files, AI output, or
existing PR labels. If your labels carry that kind of weight, this is exactly the sort of rule that
belongs in the private layer rather than the public manifest, so a contributor can see
that the mapping exists (via its effect) without being able to read the exact issue-label ->
PR-label rules and game them:
# .loopover.yml (global default)
settings:
linkedIssueLabelPropagation:
enabled: true
mode: exclusive_type_label
mappings:
- issueLabel: customer:vip
prLabel: triage:vip
removeOtherTypeLabels: falseA per-repo override's mappings list replaces the global default wholesale (the standard
array-replace overlay semantics above) — it does not merge with it.
- Private config (this directory): anti-abuse thresholds, the contributor cap, maintainer/ admin exemption logins, autonomy dials, model/effort overrides, and anything else you don't want a contributor reading and gaming.
- Public
.loopover.yml(repo root, contributor-visible): work-area guidance (wantedPaths), test expectations, and review-panel presentation — nothing here should describe your private enforcement strategy.
Never commit real policy into this directory or into these example files: no maintainer usernames,
no repo names, no thresholds beyond illustrative placeholders, no secrets or tokens. The
.loopover.yml-named template files shipped alongside this README (see the catalog in
TEMPLATES.md) are deliberately generic and inert — copy one into your own mounted
LOOPOVER_REPO_CONFIG_DIR, name the copy .loopover.yml, and edit the copy, not this one.