Keep your GitHub org and repos in a declared state, with guardrails and drift correction.
Full documentation lives at intentius.io/github-warden, with deep dives on these pages.
You declare the desired state of your org and repos in one YAML file
(selective-by-omission: an absent field is never read, diffed, or touched);
warden diffs it against live GitHub, runs safety guardrails, and either
prints the plan (dry-run, the default) or applies it. Deletes are planned
only in orgs you mark owned. Run it locally, on a schedule, or as a GitHub
Action.
From a checkout, Claude Code picks up the skill in
.claude/skills/github-warden automatically. Other agents can install it with
npx skills add INTENTIUS/github-warden, or by copying the skill directory
into ~/.claude/skills/. Then paste this prompt, filling in the placeholders:
Use the github-warden skill in this repo to help me set up governance for my
GitHub org <ORG>: author a governance policy file covering the settings I care
about (ask me which), explain whether I need a GitHub App or a token for those
cycles, then run a dry-run reconcile and walk me through the plan it prints.
Do not apply anything.
The skill holds the agent to dry-run until you've reviewed the plan; deletes
stay off entirely until you mark an org owned in the policy.
- A clone of this repo (
git clone https://github.com/INTENTIUS/github-warden). The agent skill, the annotated policy examples, and the pipeline templates live in it, and setup ends with a governance pipeline running in your org, so you'll have the repo around anyway. The CLI is published to npm as@intentius/github-wardenfor those pipelines. - Auth. Org-level and token cycles need a GitHub App installed on your org
(App ID plus installation ID plus private key); repo-level reconcile and
auditcan run with a pre-minted installation token instead. See Auth and the App setup checklist. - Node 22+.
About ten minutes gets you to a first dry-run plan. The quickest probe needs no clone at all:
# Dry-run: reads only, prints a plan, changes nothing.
npx @intentius/github-warden reconcile --config .github/governance.yml --token-env GH_TOKEN --mode dry-run| Command | What it does |
|---|---|
reconcile |
Run governance cycles: diff desired vs live, guardrail-check, dry-run or apply. |
audit |
Audit managed repos for security/correctness posture (chant's audit engine). |
report |
Aggregate cycle drift (+ optional audit + identity) into a compliance snapshot. |
github-warden reconcile --config <path> [auth] [--mode dry-run|apply] [--cycles a,b,c] [--allow-guardrail-override] [--removal-cap-fraction <value>]
| Flag | Meaning |
|---|---|
--config <path> |
Governance config (YAML or JSON). Required. |
--mode dry-run|apply |
Default dry-run. |
--cycles <name[,name...]> |
Subset of cycles to run (default: all). |
--allow-guardrail-override |
Apply even when guardrails trip. |
--removal-cap-fraction <value> |
removalDeltaCap threshold in (0,1] (default 0.25). |
github-warden audit --config <path> [auth] [--fail-on none|merge-worthy|any]
Audits every repo declared in the config. Exits 4 when findings exceed --fail-on.
github-warden report --config <path> [auth] [--cycles a,b] [--audit] [--identity] [--out compliance.json] [--fail-on none|attention]
Runs the selected cycles in dry-run and prints a unified compliance
snapshot. Optional passes add an --audit sweep and an --identity
(service-account hygiene) check, while --out writes a committable JSON
artifact. With --fail-on attention the command exits 4 when anything needs
attention; nothing is ever mutated.
0 success · 1 guardrail block (apply) · 2 arg/config error · 3 runtime error · 4 audit/report threshold exceeded.
Each cycle reconciles one governance domain. Pass --cycles to run a subset, or
omit it to run all.
| Cycle | Reconciles |
|---|---|
branch-protection |
Classic branch protection rules (PUT …/branches/{b}/protection). |
org-settings |
Org settings — default repo permission, member repo-creation, public metadata. |
repo-settings |
Repo settings — visibility, features, merge settings, default branch, topics. |
membership |
Org members & roles (admins / members). Add/re-role; ownership-gated removal. |
teams |
Teams, team membership/roles, and team→repo permissions; rename-without-loss. |
rulesets |
Repo + org rulesets (the modern branch-protection replacement). |
security-features |
GHAS, secret scanning, push protection, Dependabot alerts + security updates. |
environments |
Deployment environments — required reviewers, wait timers, branch policies. |
secrets-variables |
Actions secrets (presence only — never values) and variables. |
dependency-hygiene |
.github/dependabot.yml presence + exact-content consistency. |
repo-baseline |
Ensure declared repos exist (create from a template if missing). |
token-governance |
Fine-grained PAT sweep — revoke org access for expired / over-lifetime / idle grants. |
token-approval |
Auto approve/deny pending fine-grained PAT requests against policy. |
Note: the GitHub Action exposes
reconcileandaudit.reportis CLI-only. The token cycles (token-governance,token-approval) and several org-level APIs require a GitHub App installation (they are not callable with a plain PAT).
Every field is optional; declare only what you want managed. A ready-to-edit
starter lives in examples/governance.yml.
The CLI's built-in YAML reader is block-style only — no flow style
({ } / [ ]), no multi-line scalars (| / >), no anchors. Use JSON for
anything richer, such as the multi-line dependabot.content.
orgs:
my-org:
# Org-level settings
settings:
defaultRepositoryPermission: read
membersCanCreatePublicRepositories: false
# Org members and roles
members:
- login: alice
role: admin
- login: bob # role defaults to "member"
# Teams (tree + membership + repo access)
teams:
backend:
privacy: closed
members:
- login: alice
role: maintainer
repos:
- name: api
permission: push
# Ensure these repos exist (create from a template if missing)
repoBaselines:
- name: new-service
template: my-org/service-template
private: true
# Org-level rulesets / secrets / variables
rulesets:
- name: protect-default
target: branch
enforcement: active
conditions:
ref_name:
include:
- "~DEFAULT_BRANCH"
rules:
- type: pull_request
secrets:
- name: ORG_DEPLOY_TOKEN # presence only; value provisioned out-of-band
variables:
- name: ENVIRONMENT
value: production
# Fine-grained PAT governance (GitHub App required)
tokenPolicy:
maxLifetimeDays: 90
maxIdleDays: 60
revokeExpired: true
tokenApproval:
allowedPermissions:
- repository:contents
default: deny
# Flag seat-consuming machine users (surfaced by `report --identity`)
machineUsers:
- ci-bot
- deploy-bot
repos:
my-repo:
description: My service
hasWiki: false
topics:
- api
- go
branchProtection:
- pattern: main
requirePullRequestReviews: true
requiredApprovingReviewCount: 1
requireStatusChecks: true
security:
secretScanning: true
secretScanningPushProtection: true
vulnerabilityAlerts: true
environments:
- name: production
waitTimer: 10
reviewers:
- type: Team
id: 42
# dependabot.content is a multi-line string — author that slice (or the
# whole config) in JSON; the built-in YAML reader has no `|` scalars.Before any apply, warden runs safety checks and refuses dangerous changes
(override with --allow-guardrail-override):
| Guardrail | What it refuses or protects |
|---|---|
removalDeltaCap |
Refuses an apply whose deletes of any one resource type exceed 25% (default; --removal-cap-fraction) of that type's live managed entries in the declared collections (typo protection). Live entries of one type never dilute another's fraction. See POLICY.md for the canonical description. |
adminFloor |
Refuses if fewer than 2 org admins would remain. |
requiredAdmins / requireSelf |
Keep named admins (and the managing identity) from being removed. |
| rename-without-loss | A previously alias collapses a delete+create into an update, so a rename doesn't count as a deletion. |
Warden supports two mutually exclusive auth modes, and the token takes precedence when both are given.
- A pre-minted token via
--token-env GH_TOKEN(e.g. fromactions/create-github-app-token). - A GitHub App via
--app-id-envplus--installation-id-env, with the private key inGOVERNANCE_APP_PRIVATE_KEY(orGITHUB_APP_PRIVATE_KEY).
A GitHub App is required for org-level token policy/approval and several org administration APIs. The App setup checklist walks through creating it, the per-cycle permissions, and installation.
Warden also ships as a native GitHub Action, so a governance workflow needs no npm install step. The snippets below cover the common jobs; every input maps onto a CLI flag.
# Dry-run reconcile on every PR.
- uses: intentius/github-warden@v1
with:
command: reconcile
config: .github/governance.yml
mode: dry-run
app-id: ${{ vars.WARDEN_APP_ID }}
installation-id: ${{ vars.WARDEN_INSTALLATION_ID }}
private-key: ${{ secrets.WARDEN_PRIVATE_KEY }}
# Apply on push to main.
- uses: intentius/github-warden@v1
with:
command: reconcile
config: .github/governance.yml
mode: apply
app-id: ${{ vars.WARDEN_APP_ID }}
installation-id: ${{ vars.WARDEN_INSTALLATION_ID }}
private-key: ${{ secrets.WARDEN_PRIVATE_KEY }}
# Audit all managed repos — fail if merge-worthy findings exist.
- uses: intentius/github-warden@v1
with:
command: audit
config: .github/governance.yml
fail-on: merge-worthy
app-id: ${{ vars.WARDEN_APP_ID }}
installation-id: ${{ vars.WARDEN_INSTALLATION_ID }}
private-key: ${{ secrets.WARDEN_PRIVATE_KEY }}The action accepts these inputs.
| Input | Required | Default | Description |
|---|---|---|---|
command |
no | reconcile |
reconcile or audit |
config |
yes | — | Path to governance config (YAML/JSON) |
mode |
no | dry-run |
dry-run or apply (reconcile only) |
cycles |
no | all | Comma-separated cycle names (reconcile only) |
app-id |
yes | — | GitHub App ID |
installation-id |
yes | — | GitHub App installation ID |
private-key |
yes | — | GitHub App private key PEM — pass as a secret |
fail-on |
no | none |
none, merge-worthy, or any (audit only) |
allow-guardrail-override |
no | false |
Apply even when guardrails trip (reconcile only) |
The governancePipeline export (from the package root) generates a
.github/workflows/governance.yml that dry-runs on PRs touching the config
and applies on a schedule and on manual dispatch, pinned to a warden Action
SHA; the CI guide covers it in detail.
just release [patch|minor|major] bumps package.json, tags vX.Y.Z, and
pushes. The pushed tag triggers .github/workflows/publish.yml, which gates on
the test suite and then runs npm publish --provenance with id-token: write.
The package is published as @intentius/github-warden (scoped, under the
intentius npm org) via GitHub OIDC trusted publishing, with no npm token
involved, the same way the chant lexicons publish.
Unit tests (npm test) are fully mocked. Above them sit two gated e2e layers
(both under e2e/, both self-skipping — see e2e/README.md
for the cycle-by-cycle coverage table):
The hermetic compose smoke drives every cycle's full loop against a stateful
mock GitHub (dependency-free node, run via docker compose or directly). Each
cycle applies its policy slice and then re-plans to convergence, with
out-of-band drift corrected and deletes exercised under owned. The suite
also trips the removalDeltaCap block and walks the permission-gated 403 NOTE
path, and auth runs the real App JWT and installation-token flow against the
mock.
just e2e-up # or: node e2e/mock-github/server.mjs &
GITHUB_WARDEN_E2E_URL=http://localhost:8188 npm run test:e2e
just e2e-downThe real-App e2e is the non-hermetic layer: a gated, self-provisioning suite
that exercises every cycle against a real GitHub org via a real App
installation; nothing else validates the live API contract (especially the
App-only token cycles). Neither npm test nor PR CI includes it.
WARDEN_E2E_APP_ID=… WARDEN_E2E_INSTALLATION_ID=… \
WARDEN_E2E_PRIVATE_KEY="$(cat key.pem)" WARDEN_E2E_ORG=my-test-org \
npm run test:e2eThe suite provisions everything it needs: a throwaway repo
(warden-e2e-<run>) plus one Actions variable and one sealed-box-encrypted
secret, with the repo deleted on teardown, so nothing pre-existing is
required. When the WARDEN_E2E_* vars are unset, the whole suite self-skips.
Phase 1 always runs. Per cycle, it runs fetchLive + diff against the
provisioned repo/org and asserts that every HTTP call was a GET (fetchLive
never mutates) and that the diff composes a change set, which catches API
drift.
Phase 2 is opt-in via WARDEN_E2E_APPLY=1 and performs one apply through a
cycle (setting a repo topic), verified by re-fetch and cleaned up by the repo
teardown.
The App installation needs repository administration (create/delete repos)
and Actions secrets + variables read+write, plus the read scopes the cycles
touch. CI runs it nightly and on demand via .github/workflows/e2e.yml using
WARDEN_E2E_* repo secrets (never on PRs).
Once the App is created, installed on the test org, and its .pem downloaded
(the web-only steps), wiring the secrets and triggering a run is automated:
just e2e-setup <test-org> <app-slug> ./warden-e2e.pem # discovers app/installation id, sets the 4 secrets
just e2e-run # dispatch Phase 1 (add `true` for Phase 2)The provider-agnostic reconcile core (change-set model, generic collection diff,
guardrail framework) lives in @intentius/chant/reconcile;
github-warden builds the GitHub-specific cycles, live-state types, and
member-aware guardrails on top of it.