safe-settings
Onboard github/safe-settings — a Probot GitHub App that reconciles repo settings, branch protection, rulesets, labels, teams, environments, and collaborators from YAML files in a single admin repo.
Use this skill when an org wants to manage repository policy declaratively at scale, instead of clicking through each repo's settings page or running gh api one-shot scripts.
When to reach for safe-settings vs alternatives
| Situation | Tool |
|---|---|
| Single repo, one-time policy lockdown | /gh-bootstrap (gh CLI, imperative) |
| Many repos, central policy that must stay in sync, drift detection, dry-run on PR | safe-settings (this skill) |
| Per-task GitHub ops (PRs, issues, CI status) | /gh |
safe-settings is the right answer when policy is shared across repos and you want PRs + dry-run gating on policy changes themselves. It's overkill for a single repo.
What gets configured (by safe-settings, after this skill runs)
- Repo metadata (description, homepage, topics, visibility, default branch, feature toggles)
- Merge button settings (squash / merge / rebase, delete-on-merge, auto-merge, update-branch)
- Security toggles (vulnerability alerts, automated security fixes)
- Branch protection (classic) and rulesets (modern)
- Labels (with rename via
oldname, exclusion patterns) - Milestones
- Collaborators (per-user, with
include/excluderepo lists) - Teams (with permissions, visibility)
- Environments and deployment rules
- Custom properties
- Autolinks (Jira-style references)
The same YAML schema is layered org → suborg → repo, with repo-level files taking precedence.
Protocol
1. Pick the admin repo and confirm scope
safe-settings reads its config from a single admin repo in the org. Two common layouts:
| Admin repo | When |
|---|---|
<org>/.github |
Quickest — the org already has this repo for community health files. Set ADMIN_REPO=.github. |
<org>/admin (dedicated) |
Cleaner for large orgs where a dedicated policy team owns the repo. Set ADMIN_REPO=admin. |
Confirm with the user:
- Org name (
gh api user --jq '.login'if it's a user,gh api orgs/<org>to verify org) - Admin repo choice (
.githubis the common default) - Sub-org grouping needed? (e.g. group repos by team for different policy)
- Will deployment be GitHub Actions cron (simplest) or hosted Probot app (real-time webhooks)?
2. Scaffold the admin repo layout
The standard layout under the admin repo:
<admin-repo>/
└── .github/
├── settings.yml # org-level baseline — applies to all repos
├── suborgs/ # optional — overrides for groups of repos
│ └── <suborg-name>.yml
└── repos/ # optional — per-repo overrides
└── <repo-name>.yml
Files to copy from assets/:
assets/settings.yml→.github/settings.yml— minimal, opinionated org baseline (see below)assets/suborg.yml→.github/suborgs/example.yml— commented sub-org templateassets/repo.yml→.github/repos/example.yml— commented per-repo override template
The org baseline in assets/settings.yml mirrors the gh-bootstrap defaults: squash-only merging, PR-title commit subjects, delete-on-merge, plus a default-branch ruleset with merge queue and required reviews. Read references/schema.md for the full key catalog and how the layering works.
3. Choose the deployment path
GitHub Actions cron (recommended starting point)
- Lowest setup cost — no hosting, no webhook tunnel
- Reconciles every N hours via
npm run full-sync - Drift gets fixed within the cron interval, not in real time
- Copy
assets/full-sync.yml→<admin-repo>/.github/workflows/safe-settings.yml
Hosted Probot app
- Real-time response to webhook events (push to
.github/settings.yml, repo created, etc.) - Needs hosting (AWS Lambda, Docker, Heroku, k8s) and a public URL for webhooks
- Use this once the cron path is proven and the org wants instant reconciliation
- See
references/deploy.mdfor the deployment matrix and env-var reference
The skill defaults to the GHA cron path. Only switch to hosted if the user asks.
4. Create the GitHub App
Both deployment paths need a GitHub App installation. Walk the user through the manifest flow:
- Clone safe-settings locally:
git clone https://github.com/github/safe-settings cp .env.example .env, setGH_ORG=<org>npm install && npm run dev- Open the printed URL → "Register a GitHub App" → install on the org
- Save these values:
- App ID →
vars.SAFE_SETTINGS_APP_ID(repo or org variable) - Private key (downloaded
.pem) →secrets.SAFE_SETTINGS_PRIVATE_KEY(paste the file contents) - Client ID →
vars.SAFE_SETTINGS_GITHUB_CLIENT_ID - Client secret →
secrets.SAFE_SETTINGS_GITHUB_CLIENT_SECRET - Webhook secret →
secrets.SAFE_SETTINGS_WEBHOOK_SECRET(only needed for hosted Probot)
- App ID →
The GitHub App needs admin access to the org's repos to manage settings. The manifest flow requests the right permissions automatically — don't hand-roll the permission list.
5. Wire and test
For the GHA path:
# Set the variables and secrets on the admin repo (or org-wide).
gh variable set SAFE_SETTINGS_GH_ORG --body "<org>" --repo "<org>/<admin-repo>"
gh variable set SAFE_SETTINGS_APP_ID --body "<app-id>" --repo "<org>/<admin-repo>"
gh variable set SAFE_SETTINGS_GITHUB_CLIENT_ID --body "<client-id>" --repo "<org>/<admin-repo>"
gh secret set SAFE_SETTINGS_PRIVATE_KEY --body "$(cat private-key.pem)" --repo "<org>/<admin-repo>"
gh secret set SAFE_SETTINGS_GITHUB_CLIENT_SECRET --body "<secret>" --repo "<org>/<admin-repo>"
Trigger the first sync manually via workflow_dispatch:
gh workflow run safe-settings.yml --repo "<org>/<admin-repo>"
gh run watch --repo "<org>/<admin-repo>"
Check the run output. The first sync logs every change it would apply; subsequent runs are quiet unless something has drifted.
6. Validate guardrails
safe-settings comes with two safety nets — make sure both are wired:
- Dry-run on PRs — when someone opens a PR against the admin repo modifying settings, the app posts a status check showing what would change. Don't merge if the check fails.
- Validators —
assets/settings.ymlincludes avalidator.patternfor repo names. Customize it for the org's naming convention so safe-settings refuses to apply settings to mis-named repos.
For a more rigorous setup, add enforcement: evaluate on new rulesets to test them in shadow mode before flipping to active. See references/schema.md.
Idempotency
safe-settings is reconciliation-shaped — running full-sync on an unchanged config is a no-op. The skill itself is also idempotent:
- File scaffolding: diff against
assets/templates and ask before overwriting any existing file - Variables/secrets:
gh variable setandgh secret setare idempotent (last write wins); the skill should ask before overwriting an existing value - The GHA workflow file: same diff-and-ask treatment
Re-running the skill on a partially-set-up admin repo should fill in only the missing pieces.
What this skill is NOT for
- Setting policy on a single one-off repo with no org behind it — use
/gh-bootstrap - Running PR / issue / CI ops — use
/gh - Local git work — use
/plate - Authoring new safe-settings rules upstream — this skill consumes the released app, doesn't develop it
- Migrating from the older
github/settingsapp — safe-settings is the supported successor; the skill assumes you're starting fresh or already offgithub/settings
Gotchas
- The admin repo path matters. By default safe-settings looks for config under
.github/inADMIN_REPO; if you put it elsewhere, setCONFIG_PATHto the directory. - The GitHub App needs admin access to manage settings on member repos. A user-installed token won't cut it — it has to be the App, installed on the org.
full-syncvia GHA touches every repo in the org on every run. For huge orgs (>1000 repos) the run can take an hour and burn Actions minutes — use the hosted Probot path or scope by suborg.- The same YAML key can mean different things at different layers.
branchesat org level applies to every repo; at repo level it overrides only that repo. The merge order is deep — readreferences/schema.mdbefore writing complex overrides. - When you change the org-level
settings.yml, every repo gets the change on next sync. There is no "stage to a few repos first" toggle short of usingenforcement: evaluateon rulesets. force_create: truein arepos/<name>.ymlwill create the repo if it doesn't exist. This is powerful and dangerous — use it deliberately.- safe-settings does not unset every API field by default. To remove a label, listing it under
labels.excludeis required; to remove a collaborator, you typically delete the entry and run sync. Read the schema before assuming "remove from YAML" means "remove from GitHub". - The merge queue is a
merge_queuerule inside arulesetsentry — not a top-level toggle. Same shape as/gh-bootstrap's ruleset payload, just expressed in YAML and applied org-wide.