You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Wardens are policy driven. The governance file is the policy: one YAML or JSON
file that declares the desired state of your GitHub organization(s), and
everything else (auth, the CLI, the cycles, the Action, the schedule) serves it.
It is the one file you must author.
Loaded from the path given to --config on every subcommand
(reconcile, audit, report). YAML or JSON, decided by file extension.
Schema (authoritative): src/config/types.ts. Validation: src/config/load.ts
(throws a GovernanceConfigError with the exact field path on bad shape).
Selective-by-omission: an absent field or collection is never read for
mutation, diffed, or changed. This holds at every level: if teams is
absent, teams are untouched; if one team's members is absent, that team's
membership is untouched even though the team itself is managed.
Deletes are ownership-gated. The diff proposes deleting a live entry
missing from the policy only when that entry's collection is marked owned,
and by default nothing is owned: a run creates and updates but never
deletes. Declaring owned: true on an org makes warden own every
resource collection it reconciles there; owned: [team, repo, ...] limits
ownership to the listed change-set resource types. A programmatic
diffOptions.isOwned predicate, when supplied, overrides the declaration.
Owned deletes still run the guardrails (removalDeltaCap etc.) before any
apply.
One authoring note: the CLI ships a small built-in YAML reader that handles
block-style mappings and sequences, string/bool/number scalars, and comments.
No flow style
({ } / [ ]), no multi-line scalars (| / >), no anchors. For anything
richer, such as the multi-line dependabot.content, use JSON.
A complete policy
Copy this, delete what you don't want managed, and edit it. Every field in the
schema is shown, and the loader validates and forwards every slice. Comments
name the cycle that consumes each slice.
orgs:
my-org: # keyed by org login; one entry per org# ── Ownership ── consumed by the diff of every cycle ────────────────────# Gates deletes. Absent/false (default): nothing is owned, no deletes are# ever planned. true: every reconciled collection is owned; live resources# missing from this policy are planned for deletion. A list limits# ownership to those change-set resource types (e.g. team, repo, member,# branch-protection, org-variable). Guardrails still apply.owned: false# ── Org settings ── cycle: org-settings ─────────────────────────────────# PATCH /orgs/{org} is a partial update: only declared keys are sent.settings:
description: Engineering org # public org profile descriptionemail: eng@example.com # public org emailwebsiteUrl: https://example.com # org website (GitHub's `blog` field)membersCanCreatePublicRepositories: falsemembersCanCreatePrivateRepositories: truemembersCanCreateInternalRepositories: false # Enterprise onlydefaultRepositoryPermission: read # none | read | write | adminrequireTwoFactorAuthentication: true # surfaced for drift; read-only on most plans# ── Org membership & roles ── cycle: membership ─────────────────────────# Adds / re-roles declared members. Removal of undeclared members is# ownership-gated (never happens from the CLI); when removals are enabled,# the member-aware guardrails (adminFloor etc.) gate the apply.members:
- login: alicerole: admin # member | admin; default member
- login: bob# ── Teams ── cycle: teams ───────────────────────────────────────────────# Keyed by team slug. Emits team, team-member, and team-repo entries.teams:
backend:
description: Backend services teamprivacy: closed # secret (default) | closedparentTeamSlug: engineering # nest under another teampreviously: platform-be # rename hint: collapses# delete(platform-be)+create(backend)# into one update; never sent to GitHubmembers: # absent -> membership not managed
- login: alicerole: maintainer # member (default) | maintainerrepos: # absent -> repo access not managed
- name: apipermission: push # pull | triage | push | maintain | admin# ── Repositories ── keyed by repo name (no org prefix) ──────────────────repos:
api:
# ── Repo settings ── cycle: repo-settings ───────────────────────────description: Public API servicewebsiteUrl: https://api.example.comprivate: truehasIssues: truehasProjects: falsehasWiki: falsedefaultBranch: mainallowSquashMerge: trueallowMergeCommit: falseallowRebaseMerge: truedeleteBranchOnMerge: truetopics: # replaced as a whole list (PUT)
- service
- api# ── Classic branch protection ── cycle: branch-protection ───────────# One entry per branch pattern. The live-state probe only resolves# literal branch names; wildcard patterns exist on GitHub but are not# discovered (see the cycles doc).branchProtection:
- pattern: mainrequirePullRequestReviews: truerequiredApprovingReviewCount: 1dismissStaleReviews: truerequireCodeOwnerReviews: falserequireStatusChecks: truerequiredStatusCheckContexts:
- cirequireBranchesToBeUpToDate: truerestrictPushes: falseallowForcePushes: falseallowDeletions: falserequireLinearHistory: true# ── Repo rulesets ── cycle: rulesets ────────────────────────────────# The modern branch-protection replacement. bypassActors / conditions /# rules use GitHub's native snake_case API shape, forwarded verbatim.rulesets:
- name: protect-maintarget: branch # branch | tag | pushenforcement: active # active | evaluate | disabledconditions:
ref_name:
include:
- "~DEFAULT_BRANCH"# omit exclude entirely: the diff# treats absent and [] as equal.# A non-empty flow-style value# still needs JSON.rules:
- type: pull_request# ── Security features ── cycle: security-features ───────────────────# GHAS-gated features that your plan lacks surface as reported failed# entries, not a crashed run.security:
advancedSecurity: false # GHAS license requiredsecretScanning: truesecretScanningPushProtection: truevulnerabilityAlerts: truedependabotSecurityUpdates: true# ── Deployment environments ── cycle: environments ──────────────────environments:
- name: productionwaitTimer: 10# minutes, 0-43200preventSelfReview: truereviewers: # users/teams by numeric GitHub id
- type: Team # User | Teamid: 42deploymentBranchPolicy: # object configures; null disables;protectedBranches: true # absent -> not managedcustomBranchPolicies: false# ── Repo Actions secrets/variables ── cycle: secrets-variables ──────secrets: # PRESENCE only; values never touched
- name: DEPLOY_KEYrotationRef: OPS-123 # informational; never sent or diffedvariables: # values are not secret: fully reconciled
- name: SERVICE_TIERvalue: gold# ── Dependabot config file ── cycle: dependency-hygiene ─────────────# Ensures .github/dependabot.yml exists and matches `content` exactly.# content is multi-line and the built-in YAML reader has no multi-line# scalars (and does not process \n escapes) — author this slice in JSON.dependabot:
content: "..."# exact file text; use JSON here# ── Org rulesets ── cycle: rulesets ─────────────────────────────────────rulesets:
- name: org-default-protectiontarget: branchenforcement: activeconditions:
ref_name:
include:
- "~DEFAULT_BRANCH"rules:
- type: pull_request# ── Org Actions secrets/variables ── cycle: secrets-variables ───────────secrets:
- name: ORG_DEPLOY_TOKEN # presence only; provision out-of-bandvariables:
- name: ENVIRONMENTvalue: productionvisibility: all # all (default) | private | selected# (org-level create only)# ── Repo provisioning ── cycle: repo-baseline ───────────────────────────# Ensures declared repos EXIST (creates missing ones, optionally from a# template). Never updates or deletes a repo.repoBaselines:
- name: new-servicetemplate: my-org/service-template # "owner/repo"; omit for an empty repoprivate: true # default true# ── Fine-grained PAT sweep ── cycle: token-governance ───────────────────# GitHub App auth required. Revokes a grant's ORG ACCESS on violation;# PATs themselves cannot be rotated or created via the API.tokenPolicy:
revokeExpired: true # default truemaxLifetimeDays: 90# 1-366; older grants revokedmaxIdleDays: 60# staler grants revoked# ── Pending PAT requests ── cycle: token-approval ───────────────────────# GitHub App auth required. Approves a request only when EVERY requested# permission (flattened to group:scope) is allowed; otherwise `default`.tokenApproval:
allowedPermissions:
- repository:contents
- repository:metadatadefault: manual # deny | manual (default manual)# ── Machine users ── consumed by `report --identity` ────────────────────# Operator-declared service-account logins; the identity report flags any# that consume an org seat and suggests migrating them to GitHub Apps.machineUsers:
- ci-bot
- deploy-bot
The smallest useful policy is one org with one managed slice, for example just
settings.defaultRepositoryPermission; everything else then stays unmanaged.
Field reference
In the tables below, Required / default describes what the loader or cycle
enforces, and Cycle names the reconcile cycle (from --cycles) that
consumes the field.
orgs (top level)
Field
Type
Required / default
Cycle
Meaning
orgs
map of org login → org config
required
all
Organizations to manage. Each cycle runs once per org.
orgs.<org>.owned
bool or list of resource type strings
absent → nothing owned, no deletes planned
all (diff layer)
true = every reconciled collection in this org is owned and live resources missing from the policy are planned for deletion; a list (e.g. [team, repo]) owns only those change-set resource types. A programmatic diffOptions.isOwned overrides it. Guardrails still gate the apply.
orgs.<org>.settings (org settings)
Field
Type
Required / default
Cycle
Meaning
description
string
not managed
org-settings
Public description on the org profile.
email
string
not managed
org-settings
Public org email.
websiteUrl
string
not managed
org-settings
Org website (GitHub's blog field).
membersCanCreatePublicRepositories
bool
not managed
org-settings
Whether members can create public repos.
membersCanCreatePrivateRepositories
bool
not managed
org-settings
Whether members can create private repos.
membersCanCreateInternalRepositories
bool
not managed
org-settings
Internal repos (Enterprise only).
defaultRepositoryPermission
none | read | write | admin
not managed
org-settings
Base repo permission for all members.
requireTwoFactorAuthentication
bool
not managed
org-settings
Surfaced for drift reporting; GitHub treats the key as read-only on most plans.
orgs.<org>.members[] (org membership)
Field
Type
Required / default
Cycle
Meaning
login
string
required
membership
GitHub login.
role
member | admin
member
membership
Org role.
orgs.<org>.teams{} (teams, keyed by slug)
Field
Type
Required / default
Cycle
Meaning
description
string
not managed
teams
Team description.
privacy
secret | closed
not managed (GitHub default secret)
teams
Team visibility.
parentTeamSlug
string
not managed
teams
Parent team for nesting.
previously
string
not managed
teams
Former slug; a rename hint for the guardrail layer only, never written to GitHub.
members[]
list
absent → membership not managed
teams
{ login (required), role: member (default) | maintainer }.
Branch name. Live probing resolves literal names only; wildcard rules are not discovered.
requirePullRequestReviews
bool
not managed
branch-protection
Require PR reviews before merge.
requiredApprovingReviewCount
int
not managed (falls back to live, then 1)
branch-protection
Approvals required.
dismissStaleReviews
bool
not managed
branch-protection
Dismiss stale reviews on new commits.
requireCodeOwnerReviews
bool
not managed
branch-protection
Require code-owner review.
requireStatusChecks
bool
not managed
branch-protection
Require status checks.
requiredStatusCheckContexts
list of string
not managed
branch-protection
Required check contexts.
requireBranchesToBeUpToDate
bool
not managed
branch-protection
The strict flag.
restrictPushes
bool
not managed
branch-protection
Restrict who can push (empty user/team lists).
allowForcePushes
bool
not managed
branch-protection
Allow force pushes.
allowDeletions
bool
not managed
branch-protection
Allow branch deletion.
requireLinearHistory
bool
not managed
branch-protection
Forbid merge commits.
The GitHub PUT for this endpoint is a full replacement, so the apply path does
a read-modify-write: it echoes every undeclared live field back (including
enforce_admins, which the schema does not expose) and overlays only what you
declared.
rulesets[] at org (orgs.<org>.rulesets) and repo (repos.<name>.rulesets) level
GitHub-native shape, e.g. { ref_name: { include, exclude } }.
rules
list of object
not managed
rulesets
GitHub-native shape: [{ type, parameters? }].
A managed ruleset is authored as a unit: its declared rules / conditions /
bypassActors are the source of truth for that ruleset. Undeclared rulesets
are never touched.
repos.<name>.security (security features)
Field
Type
Required / default
Cycle
Meaning
advancedSecurity
bool
not managed
security-features
GHAS (security_and_analysis.advanced_security). Needs a GHAS license.
{ type: User | Team, id: <numeric GitHub id> }. Compared by id, so use the ids the API returns.
deploymentBranchPolicy
object or null
not managed
environments
{ protectedBranches, customBranchPolicies } (at most one true); explicit null disables the policy.
secrets[] (Actions secrets, org and repo level)
Field
Type
Required / default
Cycle
Meaning
name
string
required
secrets-variables
Identity key within its scope.
rotationRef
string
not managed
secrets-variables
Informational rotation pointer (ticket / KMS ref). Never sent to GitHub, never diffed.
warden manages secret presence only. It never reads or writes secret
values; a declared-but-missing secret surfaces as an error telling you to
provision it out-of-band, and an undeclared live secret is removed only when
ownership-gated.
variables[] (Actions variables, org and repo level)
Field
Type
Required / default
Cycle
Meaning
name
string
required
secrets-variables
Identity key within its scope.
value
string
absent → presence-only
secrets-variables
Reconciled fully (create/update) when declared.
visibility
all | private | selected
all
secrets-variables
Org-level create only; ignored for repo variables.
repoBaselines[] (repo provisioning)
Field
Type
Required / default
Cycle
Meaning
name
string
required
repo-baseline
Repo that must exist (no org prefix).
template
string (owner/repo)
absent → empty repo
repo-baseline
Create from this template when missing.
private
bool
true
repo-baseline
Visibility of a newly created repo.
Existence-only: this cycle creates missing repos and never updates or deletes
one. Per-repo settings belong under repos.
tokenPolicy (fine-grained PAT governance)
Field
Type
Required / default
Cycle
Meaning
revokeExpired
bool
true
token-governance
Revoke org access of an expired grant still listed.
maxLifetimeDays
int (1–366)
not enforced when absent
token-governance
Grants older than this lose org access.
maxIdleDays
int
not enforced when absent
token-governance
Grants unused for longer than this lose org access.
Violations are modeled as updates ("revoke org access"), not deletes, so a
routine sweep does not trip the removal guardrail; the expiry check uses
GitHub's own expired flag. GitHub App auth is required here. The API cannot
create or rotate a user's PAT; revoking its org access is the only
enforcement lever.
tokenApproval (pending PAT requests)
Field
Type
Required / default
Cycle
Meaning
allowedPermissions
list of string
absent → nothing auto-approved
token-approval
Permission names as group:scope (e.g. repository:contents). A request is approved only when every requested permission is listed.
default
deny | manual
manual
token-approval
What happens to a request that is not auto-approved.
Like the token sweep, this cycle works only with GitHub App auth. Admins can
only approve or deny; the requested repo scope cannot be changed.
machineUsers[] (service-account inventory)
Field
Type
Required / default
Consumer
Meaning
machineUsers
list of string
not managed
report --identity
Operator-declared machine logins. Seat-consuming ones are flagged with a recommendation to migrate to GitHub Apps (Apps consume no seat). Not a reconcile cycle.
What the policy does not declare
Guardrail thresholds.removalDeltaCap caps deletes per resource type.
For each type in the plan (teams, members, environments, org variables, …),
the deletes of that type may not exceed 25% of that type's own live entries
in the collections the policy declares, and live entries of one type never
dilute another's fraction, so many live repos cannot make an environment
wipe look small. A type with no live count to measure against is measured
against its own planned non-create entries instead. Set the threshold with
--removal-cap-fraction <value> on reconcile (a number in (0,1]) or
programmatically via runReconcile({ guardrails: { removalDeltaCap } }); a
value outside (0,1] is rejected rather than silently disabling the cap.
This paragraph is the canonical description of the cap, referenced from
CLI.md and CYCLES.md. adminFloor (at least 2 org
admins must remain, by default), requiredAdmins, and requireSelf are
configured programmatically (runReconcile({ guardrails })), not in the
policy file. The CLI runs with the defaults; --allow-guardrail-override
applies anyway when one trips.
Secret values. Only secret presence is declared; values are provisioned
out-of-band.
Auth. App/installation IDs, tokens, and private keys come from
environment variables named on the command line, never from the policy. See
SETUP.md.