The pull request looked finished before morning stand-up.
An overnight agent had opened the change set.
A follow-up step posted LGTM within nine seconds.
That stamp came from an unpinned free inference pool.
Nobody recorded a model id or a review policy.
The writer and the reviewer shared one endpoint.
The migration dropped a column still serving billing.
The review lane stayed green in CI.
The production database then failed the next checkout.
This field guide is for that class of failure.
It is not a product tour.
It is a stop list for reviewer authority.
What actually broke
The team did not lack models.
The team lacked a reviewer authority rule.
A free inference path is useful for drafts.
It is a weak source of ship-or-block truth.
Agents treat a green comment as completed review.
Pipelines then copy that assumption into merge rights.
The pattern shows up in three steps.
- A generate step writes the diff.
- A review step calls the same free pool.
- CI treats the comment as an approval.
The second step is not independent.
It is a mirror with extra tokens.
Correlated errors survive both hops.
Scope of this guide
This article covers reviewer identity, not host binding.
It does not rehash webhook placement on cheap hosts.
It does not rehash invented caches or missing config.
It targets one decision only.
Who may say the change is safe to merge.
Use it when an agent can comment on pull requests.
Use it when merge queues read those comments.
Skip it when humans still click the merge button.
Red flags
Refuse the lane when any item below is true.
- The reviewer model id is missing or set to
auto. - The writer model id equals the reviewer model id.
- The review prompt never names the files under test.
- The review finishes faster than the test suite.
- The endpoint is a free pool with no pin.
- The agent grades its own plan, tests, and diff.
- License, PII, or migration risk sits in the changed files.
- A bot account can merge without a human gate.
A free pool can still draft comments.
Those comments are notes for people.
They are not signatures on a release.
Why free inference fails as a reviewer
Free model access has a real job.
It is a sandbox for prompts and throwaway agents.
It is not a notary and not a code owner.
Unpinned free endpoints move under the operator.
Output shape can change without a git tag.
Self-review hides shared blind spots.
The writer and the judge miss the same column drop.
A merge gate needs independence first.
Independence needs a pinned reviewer, a separate policy, and a human path.
Speed is not evidence of care.
A nine-second LGTM did not read the migration.
It completed a template.
Artifact: fail the job on unsigned review
The following Node script is a proposal.
It is not a production security product.
Teams should run it against recorded review metadata.
The agent must emit that JSON before merge.
Missing metadata is a fail, not a skip.
Save this as scripts/review-authority-guard.mjs.
#!/usr/bin/env node
/**
* Proposal: fail CI when review authority is unsigned.
* Reads JSON metadata from the agent review step.
* Exit 2 on policy failure. Exit 0 when the stamp is
* ignorable or a human/pinned reviewer is present.
*/
import { readFileSync } from "node:fs";
const HIGH_RISK_PATHS = [
/migration/i,
/schema\.(sql|prisma|rb)/i,
/(^|\/)\.github\//,
/(^|\/)auth\//i,
/license/i,
/\.env/i,
];
const ALLOWED_REVIEWERS = new Set(
(process.env.ALLOWED_REVIEWER_MODELS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
);
function loadMeta(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function riskFiles(files) {
return files.filter((f) => HIGH_RISK_PATHS.some((re) => re.test(f)));
}
function reasons(meta) {
const out = [];
const writer = meta.writer_model_id || "";
const reviewer = meta.reviewer_model_id || "";
const files = meta.changed_files || [];
const mergeOnReview = Boolean(meta.merge_on_lgtm);
const freeReviewer = Boolean(meta.reviewer_is_free_pool);
const human = Boolean(meta.human_approver);
const durationMs = Number(meta.review_duration_ms || 0);
if (!reviewer) out.push("missing_reviewer_model_id");
if (reviewer === "auto") out.push("unpinned_auto_reviewer");
if (writer && reviewer && writer === reviewer) {
out.push("writer_equals_reviewer");
}
if (freeReviewer && mergeOnReview) {
out.push("free_pool_controls_merge");
}
if (ALLOWED_REVIEWERS.size && reviewer && !ALLOWED_REVIEWERS.has(reviewer)) {
out.push("reviewer_not_in_allowlist");
}
if (riskFiles(files).length && !human) {
out.push("high_risk_paths_without_human");
}
if (mergeOnReview && durationMs > 0 && durationMs < 5000) {
out.push("review_faster_than_five_seconds");
}
if (meta.self_grade === true) out.push("self_graded_diff");
return out;
}
const path = process.argv[2];
if (!path) {
console.error("usage: review-authority-guard.mjs <meta.json>");
process.exit(2);
}
const meta = loadMeta(path);
const failed = reasons(meta);
if (failed.length) {
console.error("review authority guard failed:");
for (const r of failed) console.error(`- ${r}`);
process.exit(2);
}
console.log("review authority guard: no merge-gate violations");
Failing fixture
Save this as fixtures/review-meta.fail.json.
The ids are labels, not vendor model names.
{
"writer_model_id": "draft-pool",
"reviewer_model_id": "draft-pool",
"reviewer_is_free_pool": true,
"merge_on_lgtm": true,
"self_grade": true,
"human_approver": false,
"review_duration_ms": 1800,
"changed_files": [
"db/migrations/20260909_drop_billing_column.sql",
"src/billing/invoice.ts"
]
}
Passing fixture
Save this as fixtures/review-meta.pass.json.
{
"writer_model_id": "draft-pool",
"reviewer_model_id": "reviewer-pinned-2026.09",
"reviewer_is_free_pool": false,
"merge_on_lgtm": false,
"self_grade": false,
"human_approver": true,
"review_duration_ms": 420000,
"changed_files": [
"db/migrations/20260909_drop_billing_column.sql",
"src/billing/invoice.ts"
]
}
Commands
export ALLOWED_REVIEWER_MODELS="reviewer-pinned-2026.09"
node scripts/review-authority-guard.mjs fixtures/review-meta.fail.json
echo $?
# expected: 2
node scripts/review-authority-guard.mjs fixtures/review-meta.pass.json
echo $?
# expected: 0
Pipeline fragment
Wire the guard after the agent step.
Do not let merge proceed if the file is absent.
# proposal: GitHub Actions fragment
- name: Require review metadata
run: test -f artifacts/review-meta.json
- name: Guard review authority
env:
ALLOWED_REVIEWER_MODELS: ${{ vars.ALLOWED_REVIEWER_MODELS }}
run: node scripts/review-authority-guard.mjs artifacts/review-meta.json
The script does not call any model.
It only reads metadata the agent already wrote.
If the agent omits fields, the job fails closed.
Test plan for the guard
Run these cases before trusting the exit codes.
Label them as unexecuted until a repo records results.
- Fail fixture returns exit
2and listswriter_equals_reviewer. - Fail fixture also lists
free_pool_controls_merge. - Fail fixture lists
high_risk_paths_without_human. - Pass fixture returns exit
0with an allowlisted reviewer. - Missing
reviewer_model_idreturns exit2. -
reviewer_model_idset toautoreturns exit2. - High-risk path plus
human_approver: truecan pass. - Low-risk docs-only change may pass without a human.
- Absent
artifacts/review-meta.jsonfails the workflow. - Empty
ALLOWED_REVIEWER_MODELSstill blocks self-grade merges.
Do not claim latency numbers from this plan.
The five-second rule is a heuristic only.
Decision table
| Situation | Free model as writer | Free model as reviewer | Merge on LGTM | Action |
|---|---|---|---|---|
| Prompt spike, branch deleted later | Yes | Notes only | No | Allow |
| Unit-test authoring on a feature branch | Yes | No | No | Allow |
| Same model writes and grades the diff | Yes | Yes | Yes | Fail job |
| Migration, auth, or workflow files | Optional | No | No | Require human |
| License or PII classification | No | No | No | Use pinned policy |
| Production router or feature flag | No | No | No | Keep off free pools |
| Draft comment for a human reviewer | Yes | Yes, as notes | No | Allow |
The table is the policy.
The script is only the alarm.
A green comment that violates the table is noise.
Better alternatives
Do not replace the free pool with a louder model.
Change the authority, not the adjectives.
- Keep free inference on draft branches only.
- Pin a reviewer model id in repo variables.
- Split writer and reviewer providers when review is automated.
- Require
human_approveron paths in the risk list. - Store review metadata next to the diff, not in chat.
- Let tests, typechecks, and linters remain the merge gates.
- Treat agent comments as annotations for people.
- Block bot merge rights on
CODEOWNERSprotected files.
A checklist in a comment is still useful.
A checklist is not an approval record.
Humans convert notes into stamps.
Where scratch inference still belongs
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode provides free model access and a free server option.
Those lanes fit spikes, harnesses, and discarded branches.
They do not fit unsigned merge rights or stored verdicts.
Teams already emitting agent metadata can point this guard at that file.
The policy still holds if every product name is removed.
Scratch work that stays off main is in bounds.
- Prompt experiments that never touch default branch.
- Generating fixtures for the guard itself.
- Throwaway hosts for tool-boundary drills.
- Drafting review checklists a human will edit.
The free server option follows the same line.
It is a scratch host for agents under test.
It is not a place to persist review verdicts.
It is not a place to store merge tokens.
Exit criteria
Leave the free-reviewer path when any exit fires.
Treat these as hard stops, not style notes.
- A bot can merge without a listed human.
- Writer id and reviewer id match on an agent PR.
- Review metadata is absent on consecutive agent PRs.
- A high-risk path shipped with only a free-pool stamp.
- The free endpoint changed comment format without a pin.
- Legal or security review now covers the repository.
After an exit, freeze merge on agent pull requests.
Restore a human reviewer on the protected paths.
Re-enable agent comments as notes only.
Do not restore merge_on_lgtm for the free pool.
Limitations
This guard reads JSON the agent supplies.
A hostile agent can lie in that file.
The script does not detect prompt injection.
It does not prove model quality.
It does not replace SAST, tests, or code owners.
Five-second reviews are a heuristic, not science.
Allowlists drift if nobody updates repo variables.
Placeholder model ids in fixtures are not vendor names.
No quota, hardware, or benchmark claim is made here.
Do not treat a passing guard as a security audit.
Do not publish the metadata file if it contains secrets.
Redact tokens before the artifact is uploaded.
Who should not use this approach
- Repos without agent-authored pull requests.
- Teams that already require two human reviewers.
- Pipelines that never grant merge rights to bots.
- Orgs that forbid sending diffs to any hosted model.
- People hunting for a quality benchmark of free inference.
Those groups need nothing from this policy.
They already have a stronger gate or a stricter ban.
Closing rule
A free model can write a useful note.
It cannot sign a release.
If the reviewer shares the writer's endpoint, ignore the stamp.
Fail the job.
Send the diff to a human.
Keep the free pool on the draft.
Top comments (0)