This guide walks you through Ward from scratch. By the end, you'll have secured a repository, added configuration files to it, and scaled that setup across an entire system of repos — all without cloning anything.
- A GitHub token with scopes:
repo,read:org,workflow - Ward installed (see Install)
Set your token:
# Option A: environment variable
export GH_TOKEN="ghp_your_token_here"
# Option B: use GitHub CLI (Ward picks it up automatically)
gh auth loginVerify Ward is working:
ward doctorYou should see green checkmarks for config loading, token detection, and API connectivity. If something is red, fix it before continuing.
Ward reads all its settings from a single file: ward.toml. This file lives wherever you run Ward from — typically your home directory or a dedicated config repo.
# Create a minimal ward.toml
ward initThe wizard asks a few questions. For this tutorial, let's say our GitHub org is called acme-engineering. You can also skip the wizard and write it by hand:
# ward.toml — lives in your current directory
[org]
name = "acme-engineering"
[security]
secret_scanning = true
push_protection = true
dependabot_alerts = true
dependabot_security_updates = trueThat's it. Four settings, one file. Ward now knows what your repos should look like.
Where does ward.toml live? In your current working directory by default. Check with
ward config path. Override withward --config /path/to/ward.toml <command>.
Let's say you have a repo called payment-service. Check its current state:
ward repos inspect payment-serviceThis shows the repo's metadata and which security features are currently enabled or disabled. No changes are made — this is read-only.
Before Ward touches anything, see what it would do:
ward security plan --repo payment-serviceOutput looks like this:
payment-service:
✓ secret_scanning already enabled
→ push_protection will be enabled
→ dependabot_alerts will be enabled
✓ dependabot_security_updates already enabled
Lines with → are pending changes. Lines with ✓ are already correct. Nothing has changed yet.
Happy with the plan? Apply it:
ward security apply --repo payment-serviceWard enables the missing features and then automatically verifies by re-reading the state from GitHub. You'll see confirmation that everything matches your desired config.
Safety net: Every mutating command supports
planfirst. Get in the habit of:plan→ review →apply. Always.
Want to double-check later? Run plan again — if everything is green, you're compliant:
ward security plan --repo payment-service
# All items show ✓ — nothing to changeWard can commit files to repositories directly — no cloning needed. This is how you standardize things like CI workflows, CODEOWNERS, PR templates, and any other file your repos should have.
Ward uses GitHub's Git Trees API to create commits server-side. It opens a pull request with your changes so you can review before merging. Nothing lands on main without going through your normal PR process.
Add template settings to your ward.toml:
[templates]
branch = "chore/ward-setup" # PR branch name
reviewers = ["your-username"] # Who reviews the PR
commit_message_prefix = "chore: " # Prefix for commit messagesWard ships with built-in templates for common configurations:
ward template listBuilt-in templates:
dependabot/gradle.yml.tera → .github/dependabot.yml
dependabot/npm.yml.tera → .github/dependabot.yml
codeql/gradle.yml.tera → .github/workflows/codeql.yml
codeql/npm.yml.tera → .github/workflows/codeql.yml
dependency-submission/gradle.yml.tera → .github/workflows/dependency-submission.yml
ward commit plan --repo payment-service --template dependabotWard auto-detects that payment-service is a Gradle project (by finding build.gradle.kts) and renders the template with the correct Java version. You see the exact file content that would be committed.
ward commit apply --repo payment-service --template dependabotThis creates a PR on payment-service with branch chore/ward-setup, adding .github/dependabot.yml. Your configured reviewers are assigned automatically.
The real power: apply to every repo in a system (or all repos) in one command:
# All repos in the "backend" system get a Dependabot config
ward commit apply --system backend --template dependabot
# All repos in the manifest
ward commit apply --template codeqlWard skips repos that already have the file with matching content — it only creates PRs where changes are needed.
The built-in templates cover CI/security, but most teams also need things like CODEOWNERS, PR templates, or custom workflows. You create these as custom templates.
# See where custom templates go
ward template dir
# → ~/.ward/templates/
# Export built-in templates as a starting point
ward template exportTemplates use Tera syntax (similar to Jinja2). Any .tera file you place in ~/.ward/templates/ becomes available immediately.
Create ~/.ward/templates/codeowners.tera:
# Managed by Ward — do not edit manually
* @acme-engineering/platform-team
/.github/ @acme-engineering/devops
Then apply it:
# Preview
ward commit plan --repo payment-service --template codeowners.tera
# Apply to all repos
ward commit apply --template codeowners.teraThe file will be committed as .github/CODEOWNERS (the target path is derived from the template name, or you can configure it — see templates.md).
Create ~/.ward/templates/pull-request-template.tera:
## What does this PR do?
<!-- Brief description of the change -->
## Checklist
- [ ] Tests pass
- [ ] Documentation updated (if applicable)
- [ ] No secrets committedward commit apply --template pull-request-template.teraThis commits .github/pull_request_template.md to your repos.
Templates have access to auto-detected context variables:
| Variable | Description | Example |
|---|---|---|
default_branch |
Repo's default branch | main |
java_version |
Detected from Gradle build file | 21 |
node_version |
Detected from package.json engines | 20 |
registry_url |
JFrog registry URL (if configured) | https://... |
Create ~/.ward/templates/ci-build.yml.tera:
name: CI Build
on:
push:
branches: [{{ default_branch }}]
pull_request:
branches: [{{ default_branch }}]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '{{ java_version }}'
distribution: 'temurin'
- run: ./gradlew buildWard detects the Java version from each repo's build.gradle.kts and renders accordingly — repo A might get java-version: '17' while repo B gets java-version: '21'.
By default, Ward maps the template filename to a destination path (stripping the .tera extension and prefixing with .github/). For custom placement, you can also point templates directly using the --template flag with the full Tera template name from ward template list.
Ward currently supports creating and updating files. If you need to remove an old file (e.g., replacing ci-cool-x.yaml with ci-build.yaml), you'd remove the old one via a normal PR. Ward will handle pushing the new file.
Tip: A good pattern is: first use Ward to push the replacement file, then clean up the old one in the same PR before merging.
So far we've targeted one repo. Ward's real power is managing many repos at once through systems.
A system is a group of repositories that share a prefix. If your org has:
acme-engineering/payments-service
acme-engineering/payments-gateway
acme-engineering/payments-operations
You can group them as a system:
[[systems]]
id = "payments"
name = "Payments Platform"
exclude = ["operations?"] # regex: skip ops reposWard matches repos by prefix (payments-*) and applies your config to all of them at once.
[org]
name = "acme-engineering"
[security]
secret_scanning = true
push_protection = true
dependabot_alerts = true
dependabot_security_updates = true
[templates]
branch = "chore/ward-setup"
reviewers = ["your-username"]
commit_message_prefix = "chore: "
[[systems]]
id = "payments"
name = "Payments Platform"
exclude = ["operations?", "system"]ward repos list --system paymentsRepository Language Visibility Default Branch
payments-service Kotlin private main
payments-gateway Java private main
payments-shared-lib Kotlin private main
Notice payments-operations is excluded (matches the operations? pattern).
ward security plan --system paymentsNow you see the security status of every repo in the system at once. You'll see which repos are compliant and which need changes.
ward security apply --system paymentsWard applies your security config to all matched repos, skipping those already compliant. Same for templates:
ward commit apply --system payments --template dependabot/gradle.yml.teraThis adds dependabot.yml to every Gradle repo in the payments system. npm repos are automatically skipped (wrong project type).
Add to your ward.toml:
[rulesets.branch_protection]
enabled = true
required_approvals = 1
dismiss_stale_reviews = true
require_code_owner_reviews = true
require_linear_history = true
block_force_pushes = truePreview and apply:
ward rulesets plan --system payments
ward rulesets apply --system paymentsOperations repos might need different rules (e.g., team can bypass for hotfixes):
[[systems]]
id = "payments"
name = "Payments Platform"
exclude = ["operations?"]
[systems.rulesets.branch_protection]
bypass_teams = [{ slug = "payments-team", bypass_mode = "pull_request" }]
[[systems.rulesets.branch_protection.overrides]]
repo_patterns = ["*-operations", "*-operation"]
bypass_teams = [{ slug = "payments-team", bypass_mode = "always" }]Define who has access to your system's repos:
[[systems]]
id = "payments"
name = "Payments Platform"
teams = [
{ slug = "payments-developers", permission = "push" },
{ slug = "payments-leads", permission = "admin" },
{ slug = "platform-readonly", permission = "pull" },
]ward teams plan --system payments
ward teams apply --system paymentsHere's what a complete ward.toml looks like for a team managing two systems:
[org]
name = "acme-engineering"
# Security settings applied to all repos
[security]
secret_scanning = true
secret_scanning_ai_detection = true
push_protection = true
dependabot_alerts = true
dependabot_security_updates = true
# How PRs are created when Ward commits files
[templates]
branch = "chore/ward-setup"
reviewers = ["alice", "bob"]
commit_message_prefix = "chore: "
# Branch ruleset applied to all repos
[rulesets.branch_protection]
enabled = true
required_approvals = 1
dismiss_stale_reviews = true
require_code_owner_reviews = true
require_linear_history = true
block_force_pushes = true
# --- Systems ---
[[systems]]
id = "payments"
name = "Payments Platform"
exclude = ["operations?", "system"]
teams = [
{ slug = "payments-team", permission = "push" },
{ slug = "tech-leads", permission = "admin" },
]
[systems.rulesets.branch_protection]
bypass_teams = [{ slug = "payments-team", bypass_mode = "pull_request" }]
[[systems]]
id = "inventory"
name = "Inventory Services"
exclude = ["operations?", "system"]
teams = [
{ slug = "inventory-team", permission = "push" },
{ slug = "tech-leads", permission = "admin" },
]# See the full compliance picture
ward plan
# Or target one system
ward plan --system paymentsThe ward plan command runs security, rulesets, and templates checks all at once — giving you a unified view of what's compliant and what needs attention.
Has someone manually changed settings? Find out:
ward drift --system paymentsThis compares live GitHub state against your ward.toml and reports any differences.
Get a full inventory of security posture across all repos:
ward audit --system paymentsAdd Ward to your CI pipeline to prevent drift. See CI Integration for GitHub Actions examples. Ward exits with non-zero codes when drift is detected, making it CI-friendly.
For an interactive overview of everything:
ward tuiThe TUI gives you a live dashboard of repos, security status, and more — useful for exploring without memorizing commands.
| What | Where |
|---|---|
| Configuration | ./ward.toml (check with ward config path) |
| Custom templates | ~/.ward/templates/ (check with ward template dir) |
| Audit log | ~/.ward/audit.log |
| Cache | ~/.ward/cache/ |
# Setup & diagnostics
ward init # create ward.toml
ward doctor # check everything works
ward config show # view loaded config
ward config path # where is ward.toml?
# Inspect (read-only)
ward repos list --system X # list repos in a system
ward repos inspect my-repo # deep-inspect one repo
# Plan (dry-run, safe)
ward plan --system X # full compliance check
ward security plan --system X # security only
ward rulesets plan --system X # rulesets only
ward commit plan --repo X --template T # template preview
# Apply (makes changes)
ward security apply --system X # enable security features
ward rulesets apply --system X # apply rulesets
ward commit apply --system X --template T # commit files
ward teams apply --system X # set team permissions
# Monitor
ward drift --system X # detect config drift
ward audit --system X # full audit reportEvery mutating command follows the same cycle:
plan → review output → apply → automatic verify
You never need to remember a different workflow. Plan is always safe. Apply always verifies.
- Configuration Reference — all
ward.tomloptions - Templates Guide — custom templates and Tera syntax
- CI Integration — automate drift detection
- TUI Dashboard — interactive terminal interface