A Policy Gate Checklist for AI-Generated Code in Pull Requests

Thirty-one percent of our pull requests tripped the AI-detection gate in its first month. Nine were genuine. That ratio is why I spent a quarter of my life building a proper evaluation harness instead of trusting a dashboard number.

Short answer up front, because it saves you three months: statistical AI-content detectors applied to source code are a poor blocking gate on their own. In our sample they ran at 26% precision. What works is stacking four signal families (statistical, stylistic, provenance, and commit metadata) behind a single policy decision, and accepting that the gate exists to route a review, not to deliver a verdict.

I run DevSecOps at a fintech with roughly 140 engineers, nine repositories that carry regulatory weight, and a change advisory board that has opinions about anything named "gate." We had two concrete drivers. First, our largest vendor engagement shipped a batch of integration code that looked machine-written, and nobody could say so with evidence. Second, our auditors started asking, in writing, whether AI-assisted code in the payments repo had an identifiable human accountable for it. That question does not go away if you ignore it.

What follows is the methodology, the measured results, the pipeline config, and the honest limitations. If you want to know how these detectors work before you buy one, the AI code detector writeup is a reasonable primer. This post is about the operational side.

What we measured, and what "AI-generated" even meant

The definition problem is where most of these programs die. A developer who types // sort this list and accepts a twelve-line completion is not in the same category as someone who pastes a 400-line service class out of a chat window. Our policy drew the line at 60% of added lines judged machine-originated with minimal human edit, measured per pull request across files over 40 added lines. Anything smaller we treat as noise, which means we make no claim about the long tail of two-line completions. Nobody can, credibly.

The window ran 90 days across nine repos. 1,847 pull requests flowed through. We drew a stratified sample of 300 (stratified by repo, author tenure, and diff size) and two senior engineers reviewed each one independently, with disagreements adjudicated by me. That adjudication happened 23 times, which tells you something about how fuzzy the category is even for humans staring at the same diff.

Sample attributeValue
Pull requests in window1,847
Hand-reviewed sample300
Judged substantially machine-originated41 (13.7%)
Judged partially AI-assisted78 (26.0%)
Reviewer disagreements adjudicated23
Median added lines per PR94

Thirteen point seven percent is our number and it is not yours. A greenfield startup using Copilot on every file will be higher. A maintenance-mode COBOL team will be near zero. Do not import my figure into your board deck.

How to detect AI-generated code in pull requests, three signal families

The commercial market sells one thing and calls it detection. In practice there are at least four families of signal, and they fail in completely different places.

Statistical signals

Token-level perplexity and burstiness, borrowed from text plagiarism research, applied to source code. These measure how predictable each token is under a language model. LLM output sits in a narrower predictability band than human code, which is why the raw scores separate at all. The problem is that boring human code also sits in a narrow band. Twelve lines of getters and setters written by a tired developer at 6pm look statistically identical to twelve lines generated by Claude. Naming conventions change the score. So does boilerplate density. This family gave us the most flags and the worst precision.

Stylistic and structural signals

AST-level features that have nothing to do with token probability: comment-to-code ratio, docstring verbosity relative to function complexity, defensive check density, uniformity of error handling, whether every method has an identical shape. Human code in a five-year-old repo is lumpy. Some files are obsessively documented, others have a single comment reading // do not touch. Machine-generated additions tend to be uniformly well-mannered, which is a real and observable tell.

def reconcile_ledger(entries: list[dict], account_id: str) -> dict:
    """
    Reconcile ledger entries for a given account.

    Args:
        entries: A list of ledger entry dictionaries.
        account_id: The account identifier to reconcile.

    Returns:
        A dictionary containing the reconciliation results.
    """
    if entries is None:
        raise ValueError("entries must not be None")
    if not account_id:
        raise ValueError("account_id must not be empty")

    balance = 0.0
    for entry in entries:
        balance += entry.get("amount", 0.0)

    return {"account_id": account_id, "balance": balance, "count": len(entries)}

Nothing about this function is wrong. It is also five times longer than the problem requires, the docstring restates the signature, and the two ValueError guards protect against callers that cannot exist in this codebase. Our reviewers flagged this shape consistently. The classifier I trained on their judgments flagged it too.

Provenance signals

Does this block exist somewhere else? Public GitHub, Stack Overflow, a tutorial site, or three directories over inside your own monorepo. Provenance is the only family that produces evidence rather than a probability, which makes it the only one that survives a conversation with a lawyer. It also catches the case nobody talks about: the developer who asked an LLM for a function and got back a near-verbatim copy of an Apache-2.0 licensed snippet with the attribution stripped.

A source code plagiarism checker built on token and AST fingerprinting rather than raw string matching is the right instrument here, because renaming and reformatting destroys naive diff-based matching. We needed matching that survived a variable rename pass, and most of the tools we trialed did not.

Codequiry web results tracing a submission to a Stack Overflow question with line and token counts
Tracing code to its source: a submission matched to a Stack Overflow answer, down to lines and tokens.

Commit and metadata signals

Not detection, exactly, and not admissible on its own. Commit message shape, time-of-day clustering, sudden jumps in a developer's typical lines-per-commit, and the ratio of added-to-deleted lines on their first contribution to an unfamiliar area. We use these purely as tiebreakers on the review queue ordering. A developer who normally commits 40 lines at a time and suddenly commits 600 is worth a look, and the reason may be entirely innocent.

The stack that actually moved precision

We scored the same 300 PRs with each family separately and then with a combined policy requiring the statistical score to clear 0.75 and at least one corroborating family. Here is what came back.

Detector configurationFlags (of 300)True positivesPrecisionRecall
Statistical (perplexity/burstiness) alone1122926%71%
Stylistic/AST classifier alone743345%80%
Provenance matcher alone241979%n/a
Combined, threshold 0.75 plus corroboration523465%83%

The combined gate flagged 52 PRs to catch 34 of the 41 genuine ones. Precision went from 26% to 65%. That is the entire return on this project. Recall improved modestly because the stylistic classifier caught six cases where the statistical score stayed low, mostly short diffs full of boilerplate where perplexity tells you nothing.

Precision is the metric that decides whether your gate survives. A detector at 26% precision trains your engineers to click past the warning, and once they click past it, you have bought an audit artifact and no actual coverage.

Seven genuine cases never tripped anything at threshold. All seven were heavily edited by a human after generation, which is the boundary where the whole category stops being answerable by any tool. If your policy says "AI-generated," state plainly that you mean "substantially machine-originated as submitted," or you will lose that argument in a hallway.

Plumbing the gate into the pipeline

The scan runs as a merge-request job, not a nightly. Nightly scans produce a report nobody reads. The job emits SARIF so findings land in the same code scanning view our engineers already check for dependency CVEs, and the gate step reads a policy file that lives in the repo and goes through review like anything else.

ai-origin-gate:
  stage: verify
  image: registry.internal/cq-cli:2.4.1
  variables:
    CQ_ENGINE: "ai,web,peer"
    CQ_DIFF_ONLY: "true"
    CQ_MIN_ADDED_LINES: "40"
  script:
    - cq scan --diff origin/main...HEAD --format sarif --out cq.sarif
    - cq gate cq.sarif --policy .cq/policy.yml
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: on_success

Our first policy file had the key spelled FAIL_ON instead of CQ_FAIL_ON. The gate defaulted to advisory mode without complaining, and we spent three weeks congratulating ourselves on a zero false-positive rate. Config validation on the policy file is now step one of the job. Learn from my embarrassment.

The three-tier response

We split outcomes rather than having one gate. Below 0.45, nothing happens. From 0.45 to 0.75, the PR gets a comment and a tag on the review queue; the reviewer decides whether the author needs to explain provenance. Above 0.75 with corroboration, the merge button blocks until a second reviewer with the ai-origin-review permission clears it. Thirty-four blocks in 90 days across 1,847 PRs is a rate humans will tolerate. Two hundred blocks would have gotten the gate disabled within a fortnight, which is the most common failure mode I see in other teams' rollouts.

Codequiry AI code detection report with average AI score, highest file score and a risk distribution
AI code detection: probability scores per file, flagging submissions likely written by ChatGPT, Copilot, Claude or Gemini.

Direction of the diff matters for the plumbing. Scanning only added lines keeps runtime under 90 seconds on our largest repo and avoids re-flagging code that was reviewed and accepted last quarter. Full-repo rescans happen quarterly, on a schedule, and go to the architecture group rather than individuals.

Results after 90 days

I will not claim the gate made us faster. It did not. Median merge time for flagged PRs went up by 6.2 hours, which is the cost of a second reviewer. What changed is that arguments about authorship stopped happening in Slack threads and started happening in a review record with line-level evidence attached. Our vendor-code acceptance process now requires an AI-origin report as a deliverable, and two contract renegotiations went considerably better with one.

The number I watch is not the flag count. It is the false-positive dismissal rate: the share of flagged PRs a reviewer clears without any author conversation. That started at 58% and fell to 19% by month three, mostly because we tuned the stylistic classifier on our own repos and added a suppression file for generated-but-committed artifacts (protobuf output, OpenAPI stubs) that we should have excluded from day one.

Codequiry per-file AI analysis showing AI versus human probability for each file with written indicators
Drilling into one submission: per-file AI and human probabilities, each with the stylistic indicators behind the score.

Where the gate breaks

Four honest limitations, in descending order of how much they cost us.

Heavy human editing defeats everything. If a developer takes generation output and reworks it for an hour, the statistical and stylistic scores both collapse toward human baseline, and they should. We have no tool that meaningfully separates that case, and I would distrust any vendor claiming otherwise.

Language coverage is uneven. Our Java and TypeScript numbers look like the table above. Our Terraform and Groovy pipeline code is much weaker, because the stylistic classifier had far less training signal and the open-source corpus for those languages is thinner. Anything domain-specific (DSLs, custom query languages) is close to a coin flip.

Team style converges onto the model. Six months of consistent Copilot use, and the humans start writing in the model's register. That raises baseline scores for everyone, and the threshold you set in January is wrong by July. We re-baseline quarterly against a rolling human-authored corpus from the same repos. It is unglamorous maintenance work and it is the difference between a gate that keeps working and one that quietly becomes noise.

We have not tested this past about two thousand PRs per quarter. Whether these calibrations hold at ten times the volume, with a larger and more heterogeneous contributor base, I genuinely do not know.

An evaluation matrix for AI code detection vendors

We evaluated five tools over eleven weeks, including two open-source projects and one feature inside a larger code quality platform. The criteria below are weighted by what actually moved our numbers, not by what demos well. Steal the weights and adjust.

CriterionWeightWhy it mattered to us
Corroboration across signal families25%Single-signal detectors capped out at 45% precision in our sample
Provenance corpus breadth (web, GitHub, peer)20%Only source of evidence-grade findings; also catches license contamination
CI integration (SARIF, exit codes, diff-only scanning)15%A tool without diff scanning is a nightly report nobody runs
Language and parser coverage15%Verify your actual language mix, including the boring ones
Configurability of thresholds and suppression10%Generated artifacts will flood you without a suppression path
Audit trail and report export10%Regulated environments need evidence a human can read in two years
Cost at your PR volume5%Matters, but not enough to justify a 26%-precision tool

Two notes from the evaluation. First, the open-source options are legitimate: MOSS remains the reference implementation for peer similarity in academia, and JPlag is well maintained. Where they fall down for an engineering org is the operational side. No web-source corpus, no AI-origin scoring, no API you would want to script against at 3am, and no support contract when the auditor asks who stands behind the finding. The Codequiry vs MOSS comparison covers that gap in more detail than I can here.

Second, we wanted one system covering both the AI-origin question and the provenance question, because routing two vendors' outputs into a single policy decision is exactly the kind of glue that rots. Codequiry was the only one in our shortlist scoring meaningfully on both, with a REST API and CLI we could wire into the merge-request job without a custom adapter. The dashboard is aimed at educators, which is a little odd for a fintech, but the submission-and-report model maps cleanly onto "contractor delivery batch," and the API is the part we actually use.

Codequiry API keys page with a masked key, signed webhook configuration and API resources
The API surface: an account key, signed webhooks for finished checks, and docs for wiring scans into CI.

What I would tell a peer starting this: buy for the corroboration and the provenance corpus, not for the headline accuracy number. Ask every vendor for precision and recall on a sample of your own code, hand-labeled by your own reviewers. The ones who can produce that within a week are the ones worth talking to. The ones who send a slide with "99% accuracy" and no methodology are telling you something useful about themselves.

If you want to skip the build and start with the detection half, run your next merge request batch through the code plagiarism checker alongside an AI-origin scan and compare the two flag sets. The overlap is smaller than you would guess, and that gap is the actual state of your codebase.