AI Agent Governance — One Engineer, Over a Hundred Repositories and the Decisions No Agent Makes
By Vladimir Mikhalev · Docker Captain · CNCF Ambassador · IBM Champion
Thirty days, over a hundred public repositories, 3,380 commits to main. AI agents wrote most of the 3,072 that carry my name. The commit log cannot tell my work from theirs, so AI agent governance cannot rest on who made a change. It rests on decision rights.
The public argument about agents is about capability: how close the models are to doing everything. The exposure inside a repository is quieter, and it sits in defaults that decide on the agent’s behalf, written for teams with reviewers to spare. GitHub’s documented recipe for merging dependency updates automatically will merge a pull request whose checks are red, as long as none of them is marked required, and nothing marks a check required by default. The fleet’s own workflow has said so since September, the command’s source confirms it, and on 18 October I watched it merge a red pull request in two seconds.
This is the other half of the problem I wrote about this month in AI reviews that read nothing: an agent’s account of its own work is not evidence. A default nobody chose is not a decision.
What AI agent governance looks like when every commit has one author
Required reviews and required checks are the standard answer. The first assumes a second person. The second assumes a rule in every repository that names the right job and is kept current as jobs change. This fleet has one maintainer, me, plus my agents, and it carries neither, by decision 16: branch rules block force-push and deletion, and nothing else. In that state, this is the recipe GitHub documents for merging Dependabot’s pull requests:
# dependabot-auto-merge.yml, shortened from the GitHub documentation recipe: safe only where a branch rule requires the checksname: Dependabot auto-mergeon: pull_request
permissions: contents: write pull-requests: write
jobs: dependabot: runs-on: ubuntu-latest if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --merge "$PR_URL" # nothing required: gh merges at once, red checks included env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}The documentation pairs it with advice to require status checks on the target branch, and with that rule in place the recipe is sound. Without it, the GitHub command line does not wait. Its source merges at once in three merge states, and two of them matter here: clean, and unstable, which GitHub defines as mergeable with a non-passing commit status.
A dependency update whose verification failed is still mergeable, because nothing that failed was required. It merges the moment the job runs.
I reproduced it on 18 October in a private throwaway repository with no branch rules and one check that fails on every pull request. With the check red, GitHub reported the merge state as unstable, and the documented command merged the pull request two seconds later without ever switching auto-merge on. Then I added one ruleset that requires that check and opened a second pull request. The same command switched auto-merge on and waited, and nothing merged.
So the ruleset is the right fix wherever someone maintains it. Across more than a hundred repositories it is a string that has to match a job name in each one, and a renamed job blocks every merge in that repository until somebody edits the rule.
The workflow below carries the dependency inside itself. It cannot run before the verification finishes, because the verification finishing is what starts it.
# 18 October 2026, GitHub CLI 2.97.0, a private repository whose only check fails on every pull requestpull request 1, no rule requires the check before: mergeable=MERGEABLE mergeStateStatus=UNSTABLE verify=FAILURE gh pr merge --auto --merge -> merged at 15:52:09 UTC, auto-merge never enabledpull request 2, a ruleset requires verify before: mergeStateStatus=BLOCKED verify=FAILURE gh pr merge --auto --merge -> auto-merge enabled at 15:52:42 UTC, not merged# dependabot-automerge.yml, shortened from the workflow every template here carries: the verification on that commit decidesname: Dependabot auto-mergeon: workflow_run: workflows: ["Deployment Verification"] types: [completed]
permissions: contents: read
jobs: merge: if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest permissions: contents: write pull-requests: write actions: write steps: - env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} SHA: ${{ github.event.workflow_run.head_sha }} VERIFY: ${{ github.event.workflow_run.path }} run: | pr="$(gh api "repos/$REPO/commits/$SHA/pulls" \ --jq '.[] | select(.state=="open" and .user.login=="dependabot[bot]") | .number' | head -1)" [ -n "$pr" ] || exit 0 gh pr merge "$pr" --repo "$REPO" --squash --delete-branch # a push made with this token starts no workflows, so verify main as merged gh workflow run "$(basename "$VERIFY")" --repo "$REPO" --ref mainHere the decision belongs to the repository’s own verification, run on that commit. A red run means the job does nothing. The update waits for a person. The last line closes a second gap: a merge pushed with the job’s own token starts no workflows, so main carried a merge nobody had verified. On 17 September that was 14 repositories out of 94, up from four two days earlier.
In a decade of production infrastructure, the controls that held never trusted who made a change; they checked what the change did. Authorship is not a control. Verification is.
Why this keeps happening
Governance for agents is borrowed from governance for people. A human team scales review by adding reviewers. An agent multiplies the changes and adds no one, so the second pair of eyes that every default assumes is the one resource that shrinks. What is left is to approve faster or to let the agent approve.
Either way, the audit trail records a person approving everything.
Credentials make it worse. An agent running under an engineer’s account inherits whatever that account can do: spend, publish, delete. Nothing in the account knows whose hand is on the keyboard. Decision 7 took one class of key out of reach. The model credentials come from workload identity federation, so no API key exists to leak, and the only thing that can spend them is a workflow on one branch of one repository.
The good defaults are invisible too. Since 14 July 2026, Dependabot waits until a release has been on its registry for three days before proposing it, unless a configuration file shortens the wait or switches it off. Security updates are exempt and open at once. A default that needs no configuration is also a default nobody notices being turned off, which is why a fleet rule here fails any repository that shortens it.
A default is a decision made for a team that is not yours.
Risk and blast radius
Change management is what an auditor reads first. The change-management criterion of SOC 2 expects each change to be authorized, tested and approved before it is implemented. When an agent commits under an engineer’s account and auto-merge decides, the record shows the engineer authorizing every change. The engineer saw none of them. The auditor’s finding lands on the control.
The usual answer is to give each agent an account of its own. That fixes attribution, which an auditor will ask for: the record would say which changes an agent wrote. It does not fix authorization. An agent account with write access merges what the engineer’s account merges, and the documented recipe merges its red pull requests the same way. Attribution answers who. The control has to answer whether.
The supply chain is the second exposure, and it is measured. This fleet merged 309 dependency updates in those 30 days, a median of 4.3 minutes after each was proposed. Fast is right when the verification decides and the release is three days old. Without the cooldown, a poisoned release could reach main within minutes of being published, and with a red check that is not required, it would reach main anyway. The blast radius of a template fleet is every server that pulls a template, not the repository. The recipe travels, too. Copied into a customer’s repository by a consultant or a forward-deployed engineer, it inherits that repository’s branch rules, or their absence, and nobody on either side has looked.
Three exposures never show up in CI at all: what an agent spends, what it publishes and what it destroys. Those are the three decisions kept out of its reach here.
Ways to decide whether a change merges, compared
| Who decides the merge | Needs a second person | When the verification is red | Human time per update | Fits |
|---|---|---|---|---|
| A required human review | yes | blocked, if the reviewer reads the checks | minutes of reading, every time | teams with reviewers to spare |
| Auto-merge with required checks | no, but someone keeps the required list current | blocked | none | repositories whose branch rules require the checks |
| Auto-merge without required checks | no | merged at once: the state is unstable | none | nowhere |
| A merge triggered by the verification’s own success | no | nothing happens, the update waits | only when the run is red | one maintainer with agents, as here |
| The agent merges on its own report | no | whatever the agent says | none | nowhere |
The table is about who holds the decision. Four of the five merge within minutes either way.
Framework: three decisions held by mechanism, the rest decided by checks
Layer 1: write the three decisions down as configuration (week 1)
Money, publication and anything that cannot be undone stay with a person, and each is held by a mechanism rather than by trust. History is the cheapest to hold: one ruleset on every default branch in the fleet, with no force-push and no deletion. Money takes a budget whose action attaches deny-all to the role that creates resources once spending crosses a ceiling, which the AWS bootstrap sets at thirty dollars a month. Publication needs no code. Nothing reaches this site without my go-ahead, and the newsletter and the podcast follow only from that. Credentials reach a tool from the keychain on standard input, never as an argument a process list would show.
# rules.sh, with the GitHub CLI: the one ruleset every default branch here carriesgh api --method POST "repos/${OWNER:?}/${REPO:?}/rulesets" --input - <<'JSON'{ "name": "default branch: no rewrite, no delete", "target": "branch", "enforcement": "active", "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, "rules": [{ "type": "non_fast_forward" }, { "type": "deletion" }]}JSON
# a credential reaches the CLI on standard input from the macOS keychain, never as an argumentsecurity find-generic-password -s github-fleet -w | gh auth login --with-tokenOwner: the person whose account the agents commit under, with the security lead signing off the list of decisions that stay human.
Layer 2: replace Dependabot auto-merge with the repository’s own verification (week 2)
Replace the auto-merge flag with the workflow above: triggered by the verification’s completion, merging only on its success, write permissions on the one job that merges, and a second verification on main as merged. Then write the cooldown into the configuration. A later edit that shortens it becomes a visible diff instead of a silent default.
# .github/dependabot.yml: Dependabot's three-day cooldown stated, so shortening it shows up in reviewversion: 2updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly cooldown: default-days: 3Owner: the platform team, which also owns the rule that fails a repository for turning the cooldown down.
Layer 3: prove every gate with a planted violation, and publish the misses (week 3 onward)
A check an agent cannot argue with is a check that has been shown its own violation. The reference architectures here plant 101 changes that CI must refuse and 30 manifests the running cluster must refuse, against 7 it must admit, and a daily sweep shows every fleet rule a planted breach first. A check that stays quiet, or refuses for the wrong reason, fails the build. What still gets through goes into the ledger with the rule it produced. Five entries came from 15 October alone; one of them is three gates that stayed green because a pipe swallowed their exit status.
# plants.sh, in bash: each line of plants.tsv is a file, the text to replace, its replacement, and the check that must name itset -uo pipefailmissed=0while IFS=$'\t' read -r file old new check; do [ -n "$file" ] || continue cp "$file" "$file.orig" body="$(cat "$file")" if [[ "$body" != *"$old"* ]]; then echo "stale plant: $file no longer contains its text" missed=1 else printf '%s\n' "${body/"$old"/"$new"}" > "$file" out="$(./run-checks.sh 2>&1 || true)" if grep -qF -- "FAILED $check" <<<"$out"; then echo "caught by $check: $file" else echo "MISSED by $check: $file" missed=1 fi fi mv "$file.orig" "$file"done < plants.tsvexit "$missed"Owner: SRE, with the security lead reading every miss and every stale plant.
Tradeoffs
Holding decisions by mechanism costs speed exactly where speed is dangerous. A dependency update waits three days for its release to age, then a median of 4.3 minutes for verification. One opened while verification is broken stays open until a person looks, and two sat for four and seven days in a private repository before the heartbeat learned to look there. The model jobs cannot run from a laptop. OpenSSF Scorecard’s code-review check stays at zero, and the evidence page says so beside the score.
The alternative has a number too: 309 updates in 30 days, at five minutes of human reading each, is more than 25 hours a month spent approving lockfiles, and that review becomes a rubber stamp by the second week. The machine spends a median of 4.3 minutes on each and asks a person only when it says no.
The closing argument
Agents will keep getting faster at writing changes, and arguing about how capable they are does not change what they are allowed to do. In this fleet they write, test and merge anything a check can judge, under my name. They do not spend past thirty dollars, publish or rewrite history. Capability is the vendor’s problem. Decision rights are yours.
Sources
- GitHub Docs, Automating Dependabot with GitHub Actions (the auto-merge recipe and its advice to require status checks), accessed October 2026
- GitHub Docs, API reference for pull requests (the merge states, with unstable defined as mergeable with a non-passing commit status), accessed October 2026
- GitHub CLI, source of the pull request merge command (the merge states in which the auto-merge flag merges at once), accessed October 2026
- GitHub Changelog, Dependabot version updates introduce default package cooldown, 14 July 2026
- AICPA, Trust Services Criteria with revised points of focus (change management), accessed October 2026
- GitHub, the auto-merge workflow every template carries and the fleet’s conformance rules (the cooldown floor and stored cloud keys), accessed October 2026
- heyvaldemar.com, Evidence, who decides what, Decisions and the Ledger (the current counts, as of 19 October 2026)
Discussion
If you run AI agents against a repository with one reviewer or none, or you have a reason to trust auto-merge without required checks, drop a comment below. Counterarguments are welcome and the comment thread is where I respond first. For longer back-and-forth with senior practitioners, join the discussion on Discord.