DEV Community

Casey Li
Casey Li

Posted on

The Merge Bit Stays With the Owner

Protected paths do not become optional when a contribution surge arrives. A free model can draft a note about a diff. It cannot own the merge bit, dismiss a scanner, or stand in for the human named in CODEOWNERS.

Early October usually thickens the inbound queue. Recent community posts have been full of contribution-drive writeups, portfolio projects, and agent-shaped tooling, which is enough to expect more pull requests and more automated reviewers. Volume is not a reason to move a security decision onto a shared completion endpoint. The clerk can sort the mail. The clerk does not sign the deed.

This piece is a field guide for that refusal. It is not a product tour, and it is not a repeat of the older argument about runtime privilege grants. A grant decides what a process may do after deploy. A merge bit decides what enters the tree that deploy is built from. Those failures rhyme, and they still need different locks. The slide is familiar: a workflow that starts as "summarize this diff" later gains a label, then a status check, then a merge. Free inference makes the slide easier, because the call looks cheap and the server looks disposable. Cheap is not the same as accountable.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option belong only on the drafting side of the workflow below. They can host a local gate and produce an unsigned summary of an unprotected diff. They are not a branch-protection provider, a signing service, or a substitute for the repository owner. This article does not state model names, quotas, hardware, duration, or data-retention terms. Those facts change, and they belong on a current product page rather than in a recycled paragraph. If the page and this article disagree, the page wins.

The analogy that holds is a notary stamp left on a hotel desk. Guests may borrow the pen. They may not borrow the stamp. A completion model is the pen. The stamp is branch protection, a required review from a named owner, and a deterministic check that does not read prose. A free server is a desk the pen can sit on. It does not become the notary because the desk was complimentary.

Consider a small service repository under a surge of first-time patches. Auth handlers live under auth/. Payment callbacks live under billing/. Workflow files live under .github/workflows/. Lockfiles and CODEOWNERS sit at the root. A documentation typo in docs/ can survive a machine summary, and a test rename in tests/ often can too. A one-line change in auth/session.go cannot, even when the summary sounds calm and even when the author is trying to be helpful during a public contribution week. The path is the fact. The adjective in the model output is not.

The original artifact is a local gate. It is a proposal, not a result from a run on this account, and it is not a benchmark. It refuses to treat model text as approval, and it fails closed when a protected path appears. Pair it with branch protection so the script is a preview for humans, not the only lock on the repository. The file list must come from git or from the forge API. A file list written by the model makes the gate circular.

#!/usr/bin/env python3
"""Proposal: local merge gate. Unexecuted example, not a measured run."""

from __future__ import annotations

import json
import sys

PROTECTED_PREFIXES = (
    "auth/",
    "billing/",
    "secrets/",
    ".github/workflows/",
)

PROTECTED_EXACT = {
    "CODEOWNERS",
    "go.sum",
    "package-lock.json",
    "pnpm-lock.yaml",
}

ALLOWED_MODEL_LABELS = {"summary_only", "needs_human"}


def protected_hits(files: list[str]) -> list[str]:
    hits: list[str] = []
    for path in files:
        normalized = path.strip().lstrip("./")
        if normalized in PROTECTED_EXACT or any(
            normalized.startswith(prefix) for prefix in PROTECTED_PREFIXES
        ):
            hits.append(normalized)
    return hits


def decide(files: list[str], model_label: str) -> dict:
    hits = protected_hits(files)
    if hits:
        return {
            "action": "human_owner_required",
            "hits": hits,
            "model_label_ignored": True,
        }
    if model_label not in ALLOWED_MODEL_LABELS:
        return {
            "action": "reject_model_as_approval",
            "hits": [],
            "model_label_ignored": True,
        }
    return {
        "action": "draft_comment_allowed",
        "hits": [],
        "model_label_ignored": False,
    }


def main() -> int:
    payload = json.load(sys.stdin)
    result = decide(payload.get("files", []), payload.get("model_label", ""))
    json.dump(result, sys.stdout)
    sys.stdout.write("\n")
    return 0 if result["action"] != "reject_model_as_approval" else 2


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

A maintainer can exercise the proposal with three fixtures before anyone wires it to a bot. The first fixture lists only docs/readme.md and a label of summary_only, and the expected action is draft_comment_allowed. The second lists auth/session.go with a label of approve, and the expected action is human_owner_required, with the model label ignored. The third lists docs/readme.md with a label of approve, and the expected action is reject_model_as_approval, with exit code 2. Those three cases are the regression suite. If a later edit makes the third case pass, the gate has started trusting prose, and the edit should be reverted rather than celebrated.

# Proposal commands. Run them locally; this draft does not report output.
printf '%s\n' '{"files":["docs/readme.md"],"model_label":"summary_only"}' | python3 merge_gate.py
printf '%s\n' '{"files":["auth/session.go"],"model_label":"approve"}' | python3 merge_gate.py
printf '%s\n' '{"files":["docs/readme.md"],"model_label":"approve"}' | python3 merge_gate.py
Enter fullscreen mode Exit fullscreen mode

The debugging workflow starts from the merge-base, not from a chat transcript. Fetch the base branch, write the name-only diff to a file, and feed that file to the gate with a closed label. If the forge UI shows a different set of paths, trust neither the model nor a stale status check. Recompute. If the action is human_owner_required, assign the owner and stop. Re-prompting the model with a request to ignore the path is the failure the gate exists to catch.

git fetch origin main
git diff --name-only origin/main...HEAD > /tmp/files.txt
python3 - <<'PY' | python3 merge_gate.py
import json
files = [line.strip() for line in open("/tmp/files.txt") if line.strip()]
print(json.dumps({"files": files, "model_label": "summary_only"}))
PY
Enter fullscreen mode Exit fullscreen mode

Prefix matching is a blunt instrument, and that bluntness is a limitation rather than a hidden strength. A copy of session logic dropped into pkg/util/session_copy.go will not hit auth/. A rename that deletes auth/session.go and adds the same bytes under internal/legacy/session.go can also slip past a naive prefix list. Generated clients, submodules, and patch files that contain a second diff are further misses. The correct response to a miss is a wider owner rule and a human read, not a question to the model about whether the miss "seems fine." The script also does nothing about secrets pasted into the prompt itself. A summary call that receives the full diff can still leak a token into a log on a shared free tier. Strip secrets before any drafting call, or do not send the diff at all.

Red flags show up as workflow shape. A job that holds a token able to merge is already past the line, whatever the prompt says about being careful. A step that asks a model to reinterpret a secret-scan hit is the same failure in a softer voice. A protected-path list that the model is allowed to edit is a stamp that can reprint itself. A review template whose only success string is APPROVE is a stamp with a shorter handle. Latency hiding inside a required check is another flag: when the free call stalls, people bypass the check, and the bypass becomes the process. So does a dashboard that counts model approvals as review coverage. The count will look healthy during a contribution week, and the tree will not be.

Better alternatives are older than chat completions, which is their virtue. CODEOWNERS plus branch protection keeps auth/, billing/, workflows, and lockfiles with named humans. Secret scanning should fail closed, with a waiver path that is a committed, reviewed file, not a sentence in a model reply. Deterministic tests, license checks, and lockfile diffs can be required status checks, because they return the same answer on Tuesday and on Saturday. The model, if used at all, writes an unsigned comment on unprotected docs or tests, and the comment is stored as a suggestion. The forge token stays in the forge's own secret store, scoped so it cannot merge. A free server option is a reasonable place to run the gate script, because the script's inputs are file names and a closed label set. It is a poor place to keep that token, and a poor place to store the only copy of the owner map.

Exit criteria are practical, and they should be written down before the surge, not during it. Stop this split the moment model text is copied into a review approval, a scanner dismissal, or a release note that claims a human signed off. Stop if the checker and the host disagree on the file list for more than a single stale run. Stop if queue time on the free option breaks the review SLA often enough that people route around it. Stop if current terms, capacity, or data-handling notes no longer match what the team assumed; this article cannot refresh those terms, and it never stated a quota that could be treated as a promise. Stop if a single maintainer would treat a green draft_comment_allowed as permission to skip reading the diff. The script never granted that permission. Stop, too, if the same assistant is later asked to reserve an advisory, publish a patch, or decide that a report is in or out of embargo. That is a different decision, and it does not become safe just because the merge gate was narrow.

Some teams should not use the approach at all. A compliance program that needs an attestation of who approved a change will not get that attestation from a free completion call, however tidy the JSON looks. A solo maintainer who wants a model to "just handle the October queue" is the reader most likely to skip the owner bit, and the surge is exactly when that skip is expensive. Repositories that mix production signing keys into the same tree as docs should fix the tree layout before they add any model. Agents that open pull requests may read the gate's JSON. They may not treat a missing hit as a merge, and they may not be given a credential that could do so.

The narrow lane is still available for teams that want a drafting assistant during a noisy week. Confirm the present limits and data terms, then keep the assistant on unsigned summaries and keep the checker on file names. Readers who want to try that lane on the free model access and free server option mentioned above should look those terms up before attaching any repository credential.

Contribution volume can justify a drafting assistant. It never justifies handing the merge bit to a free model, a free server, or a paragraph that sounds sure of itself. The stamp goes back in the drawer. The pen can stay on the desk.

Top comments (0)