How to set up AI governance for any repo
A template that tells AI coding agents the rules, then checks them at each commit, pull request and tag. It works for React, TypeScript and Python repos.
TL;DR
- Tell the agent the rules in
AGENTS.md. Ban libraries by name, and name the replacement. - Check the rules at every stage: each commit, each pull request and each tag.
- Gate the release. Who merges is a choice of trust. Start with a person, and let agents merge once journey checks pass. Only a tag deploys.
- The same setup works for React, TypeScript and Python repos. Only the tools change.
Why asking is not enough
AI coding agents write code fast. They also pick the library they saw most in training. They push straight to main. They forget the rules you gave them three prompts ago.
Asking nicely does not work. Write the rules where the agent reads them. Then make a check fail when the agent breaks them.
I built a template repo that does this for a team’s React apps. It has three layers:
Tell the agent The docs
- AGENTS.md
- CONTRIBUTING.md
- LINKS.md
Proof: the agent reads them before it starts
Check every stage Hooks and CI
- Commit hooks
- Lint, zero warnings
- Governance tests
Proof: a blocked commit or a red build
Gate the release People
- Review
- Merge
- Tag to deploy
Proof: a person approves each merge
Layer 1: Tell the agent
Most AI coding tools read AGENTS.md at the repo root. Keep it short and blunt. Agents follow tables and checklists better than prose.
The template’s AGENTS.md has five parts:
- Critical rules. Read
CONTRIBUTING.mdfirst. Never commit to a protected branch. Build and lint must pass. - Approved libraries. One library for each job, such as one chart library and one data grid.
- Banned libraries. Each banned library, why it is banned, and what to use instead.
- Branching. One change per branch, named
feat/orfix/, merged by pull request. - A pre-commit checklist. A tick list the agent runs through before each commit.
The banned list matters most. Ask an agent for “a bar chart”, and it reaches for the most popular free library. If your company licenses a different one, the agent must be told. “Use approved libraries” is too vague. “Recharts is banned, use Highcharts” is a rule the agent can follow.
CONTRIBUTING.md holds the full workflow, from branch to merge. Commit messages follow the conventional format, such as feat: add a sales chart or fix: correct the date filter. Short supporting docs give the folder layout, the official docs for each library, and how code reaches each environment. With these, the agent cites real docs and keeps to one folder layout.
This is a template, so every line a team must change is tagged [MODIFY]. A team lead searches for the tag and fills in each one. When none are left, the setup is done.
If you use more than one agent tool, keep one copy of the rules. Claude Code imports AGENTS.md with a one-line CLAUDE.md. Other tools read AGENTS.md directly.
Layer 2: Check at every stage
The docs are a request. The checks are what enforce them. Each stage stops a different kind of mistake, and the cheap checks run first.
| Stage | What runs | What it stops |
|---|---|---|
| Commit | Git hooks, on the machine | A commit on main, a badly formed message, secret files, AI attribution lines |
| Pull request | CI: lint, governance tests, then the build | Banned libraries and imports, bad branch names, old versions, missing docs |
| Merge | Branch protection and a person's review | An agent merging its own work |
| Tag | The tag build, which only deploys | A deploy that nobody chose |
At each commit
Git hooks run on every commit, for people and agents alike. Set the hooks path once, and it covers every repo on the machine. A repo with its own hooks path must call the global hooks from it.
- In the template, a
commit-msghook rejects AI attribution lines. It also checks the conventional format. - A
pre-commithook blocks a commit onmain. It also blocks secret files, such as.envand private keys. A template such as.env.examplestill gets through.
Hooks are fast, but they run on one machine. CI runs on every pull request, whatever machine the code came from.
The hooks work the same for every coding agent, because git does not know who made the commit. My own machine uses the same approach, with its own checks. Read one local setup for every coding agent.
At each pull request
CI runs in a fixed order: install, lint, test, then build. Lint and the governance tests come first. They are cheap, and they catch most agent mistakes before a full build starts.
- Lint with zero warnings. A warning fails the build. So an agent cannot leave unused imports or disabled rules behind.
- Governance tests. These test the repo itself, not the app. They fail when a required doc is missing or a banned library appears. They also check versions, branch names and commits to a protected branch.
The tests check imports as well as the dependency list. An agent might remove a dependency but leave the import. It might also pull a library in through another package.
Keep the banned lists as plain data at the top of the test file. Teams change the lists, not the test logic.
Write each failure message for the agent, because the agent reads the CI log.
Hard to act on
CI log
FAIL governance.test.ts AssertionError: expected false, got true at governance.test.ts:42 at runTest (runner.js:118) at runSuite (runner.js:96)
Easy to act on
CI log
FAIL governance.test.ts Recharts is banned. Use Highcharts instead. To fix: 1. Remove it from package.json 2. Replace its imports in src/ 3. Run the tests again
Skip what does not apply
A check that fails on a good build teaches people to ignore red. So each check runs only where it makes sense:
- Tag builds only deploy, so the governance tests are skipped there.
- Branch names are checked only on pull request builds.
- A detached HEAD in CI skips the protected branch check, because the branch cannot be read.
Layer 3: Gate the release
Agents can open pull requests, and only a tag deploys. Who merges depends on how much you trust the checks. The flow below is the starting point.
- 1BranchThe agent works on feat/ or fix/
- 2Pull requestCI checks run
- 3ReviewA person reads it
- 4MergeA person merges to main
- 5TagThe name picks the environment
- 6DeployThe pipeline ships it
A release goes out in rings, one environment at a time. A small group gets it first and finds problems early, like a canary in a mine. The release goes to more people only when that ring is healthy. The tag name picks the environment: X.Y.Z-alpha.N for early testing, X.Y.Z-uat.N for UAT and X.Y.Z for production.
- 11.4.0-alpha.1Early testing by a small group
- 21.4.0-uat.1UAT, where users test it
- 31.4.0Production, for everyone
A problem in any ring means a fix and a new tag, such as 1.4.0-alpha.2.
Teams with an external pipeline, such as Azure Pipelines or GitHub Actions, follow the same flow. The merge triggers the pipeline, and the pipeline does the deploy.
Turn on branch protection in the repo settings too. The protected branch test is a backstop, not a replacement for it.
Who merges is a choice
Start with a person merging every pull request. As the checks prove themselves, move routine merges to agents. The condition: the end-to-end journey checks pass, not only the unit tests. Read why we gate agent work on evidence for the trust levels.
TypeScript and Python repos
Nothing in the three layers depends on React. A TypeScript service or a Python repo keeps the same docs, the same stages and the same release gate. Only the tools change:
React or TypeScript
| Check | Tool |
|---|---|
| Lint | ESLint --max-warnings 0 |
| Types | TypeScript strict mode |
| Tests | Vitest |
| Packages | package.json |
| Runtime | .nvmrc |
Python
| Check | Tool |
|---|---|
| Lint | Ruff, any finding fails |
| Types | mypy --strict |
| Tests | pytest |
| Packages | pyproject.toml |
| Runtime | .python-version |
The commit hooks and the release gate do not change at all. Neither do the docs. A recommended layout keeps them in the same place in every repo:
React app
my-app/ AGENTS.md CONTRIBUTING.md DEPLOYMENT.md LINKS.md PROJECT_STRUCTURE.md README.md src/ components/ features/ lib/ tests/ governance.test.ts .env.example .nvmrc package.json
TypeScript service
my-service/ AGENTS.md CONTRIBUTING.md DEPLOYMENT.md LINKS.md PROJECT_STRUCTURE.md README.md src/ routes/ services/ lib/ tests/ governance.test.ts .env.example .nvmrc package.json
Python package
my-package/ AGENTS.md CONTRIBUTING.md DEPLOYMENT.md LINKS.md PROJECT_STRUCTURE.md README.md src/ my_package/ api/ services/ tests/ test_governance.py .env.example .python-version pyproject.toml
Protect the machine
The checks above protect the code. They do not stop a shell command that deletes data. An agent runs shell commands, so guard those too.
Give the agent a deny list for the worst commands, such as a recursive force delete or dropping a database. Add a guard that reads each command before it runs and says what to do instead. It should also refuse --no-verify, which skips the commit hooks. The local setup article shows how.
Set it up in your own repo
- Copy
AGENTS.md,CONTRIBUTING.mdand the supporting docs. - Copy the governance test, and add Vitest or pytest.
- Search for
[MODIFY]and fill in each one: branches, approved and banned lists, versions and the deploy target. - Set lint to fail on any warning.
- Install the commit hooks once, for every repo.
- Wire CI to run lint, then the tests, then the build.
- Turn on branch protection, and deploy only from tags.
- Test it. Ask the agent to “add a chart with Recharts”, and watch the build go red.
What I learned
- Docs alone do not hold. The agent reads
AGENTS.mdand follows it most of the time. The checks cover the rest. - Ban by name, and give the replacement. A vague rule gives the agent nothing to follow.
- Write failure messages for the agent. It is the one reading the CI log.
- Keep the config as data. Teams change the lists, not the test logic.
- Merge is a choice of trust. Start with a person at merge and release. Let agents merge on green once the journey checks earn it. A tag still deploys.