AI agents opening pull requests with safe controls
AI agents opening pull requests need strict repository scope, protected branches, reviewer routing, test evidence, and an audit record for every change.

AI agents opening pull requests through an API can save real engineering time, but only if the repository treats every generated change as an untrusted contribution with a traceable author. The useful boundary is simple: an agent may prepare a proposed change; humans and repository controls decide whether it belongs in the codebase.
I have seen teams make this unsafe by concentrating on the model's code quality and ignoring the permissions around it. The ugly failures are usually mundane. A task aimed at a staging service lands in a production repository. A broad token quietly grants write access to every project. An agent opens ten near-identical pull requests after a timeout. Someone merges one because the title sounds plausible.
A safe setup does not depend on the agent being careful. It limits where the agent can act, forces the change through normal review, and leaves a record good enough to investigate later.
Repository scope must be explicit and machine-enforced
An agent should receive permission for a named set of repositories, not a wildcard organization scope that someone intends to narrow later. Repository scope answers a concrete question: which codebases may this process read, write branches in, and open pull requests against?
Keep the allowlist outside the agent's instruction text. Prompts can guide behavior, but they do not enforce authorization. The component that holds the repository credential or makes the API request must reject a repository that is not on the allowlist.
For each allowed repository, define the permitted base branches and permitted write namespace. A useful record looks like this:
repositories:
- name: acme/payments-api
base_branches: ["main", "release/2025.1"]
write_branch_prefix: "agent/"
pull_request_drafts: true
- name: acme/docs
base_branches: ["main"]
write_branch_prefix: "agent/"
pull_request_drafts: false
This fragment prevents a common failure: an agent receives a request to "fix the checkout copy," finds a similar file in a repository it can search, and writes there because its credential can. The allowlist turns that into a denied request instead of a cleanup job for somebody else.
Scope also includes repository operations. Most agents need to read files, create a branch, push commits, read checks, and create or update a pull request. They rarely need to change repository settings, register webhooks, alter protected branch rules, add deploy keys, manage members, or merge changes. Do not grant those permissions because they came bundled with a convenient broad token.
Use a separate bot identity rather than a developer's personal access token. A personal token makes attribution blurry, survives job changes badly, and often carries permissions nobody remembered granting. A bot identity gives you one actor to suspend when behavior goes wrong.
Read access deserves the same discussion as write access. An agent that can inspect every private repository may expose source code or configuration through its own logs, task context, and responses. Give it the smallest useful repository set, even if the agent never receives a direct write credential.
A branch prefix is an execution boundary, not a naming preference
The agent should create branches only below a dedicated prefix such as agent/, and the repository server should enforce that restriction. A convention written in a prompt will be broken eventually, whether by a malformed tool call, a retry bug, or an agent trying to satisfy an overly broad task.
Protect main, release branches, environment branches, and any branch that deploys automatically. The agent must not push to them. It must not be able to force-push them. It must not be able to change the rules that protect them.
Make branch names deterministic enough to investigate and unique enough to avoid collisions. Include a task reference plus a short random suffix or execution identifier:
agent/OPS-1842-retry-payment-7f3a
Do not let the agent use issue titles directly as branch names. Titles can include secrets, customer names, unsafe characters, or misleading language. Generate the branch name in the controller, then pass it to the agent as an immutable value.
The base commit also needs an explicit rule. When the controller begins a run, it should resolve the approved base branch to a commit SHA and record it. The agent creates its branch from that SHA, not from whatever main happens to mean after a long code-generation run. This does not eliminate drift, but it makes drift visible and reproducible.
A branch may contain only the commits associated with its assigned task. That means no opportunistic formatting sweep, no unrelated dependency bump, and no attempt to "clean up" nearby code because it looked odd. Generated changes often appear confident, and reviewers will miss unrelated edits when they sit inside an otherwise reasonable patch.
Set a change budget before the work starts. A budget can cap changed files, total lines, or paths outside the requested component. The cap is not an assessment of risk by itself. It is a tripwire that tells the agent to stop and ask for a new task rather than quietly converting a small repair into a repository-wide rewrite.
Pull request creation needs a checked API transaction
A successful HTTP response does not prove that the agent opened the right pull request. The controller must verify the repository, head branch, base branch, commit SHA, and returned pull request identifier before it reports success.
For a GitHub-style API, the meaningful request fields are the proposed title, head, base, body, and draft status. The exact endpoint varies by forge, but the safety checks do not:
{
"title": "OPS-1842: retry transient payment gateway failures",
"head": "agent/OPS-1842-retry-payment-7f3a",
"base": "main",
"body": "Task: OPS-1842\nBase commit: 4b2c...\nTests: unit payment retry suite\nLimits: no configuration changes",
"draft": true
}
Before sending that request, query the branch and confirm that its tip SHA equals the commit the run recorded. After receiving a response, fetch the pull request and compare its head, base, and state against the request. Record the platform's immutable pull request number or node identifier, not only its URL.
Retries need special handling. Network timeouts create the classic duplicate pull request problem: the server may have created pull request 418, but the client did not receive the response and tries again. Keep an idempotency record in your controller with the task ID, repository, branch, base SHA, and pull request number. On retry, look for the existing branch and open pull request before issuing a create call.
Do not use a task title as the sole idempotency value. A recurring request such as "update generated documentation" will collide with a prior run. The execution ID should identify one run, while the task ID helps people connect related work.
Draft pull requests make a good default for agent work. They tell reviewers that the change exists but has not passed the creator's own declared completion gate. An agent can mark a pull request ready only after required commands completed and the run record contains their result. If your repository does not use drafts, apply a label such as agent-created through a trusted controller, not through text the agent composed.
Reviewer assignment must follow ownership and risk
The first reviewer should come from the repository's ownership rules, not from an agent guessing who seems knowledgeable. GitHub's CODEOWNERS documentation describes a file-to-owner mapping that can request review for changed paths. GitLab offers comparable approval and code-owner mechanisms. Those files are useful routing data, but they do not automatically make review mandatory unless branch protection or merge rules require it.
That distinction matters. A repository can show a requested code owner and still allow a merge without that person's approval, depending on its settings. Treat routing and enforcement as separate controls. Check both with a deliberately unauthorized test pull request before trusting the policy.
Use the changed file list after the final commit, not the paths the agent planned to modify. A generated patch may reach into a shared library, deployment directory, or migration folder late in the run. The reviewer calculation must see what actually changed.
Add a responsible human when the task affects areas where ownership rules are too broad or absent. This person owns the intent of the task. A code owner may confirm that the implementation fits a component; the responsible person can confirm that the requested behavior is right for the product. Do not assign a dozen people just because the diff crossed boundaries. Large reviewer lists create the familiar result where each person assumes another reviewer handled the hard part.
Some paths should force a stricter route. Examples include database migrations, authorization code, build and release definitions, dependency manifests, infrastructure configuration, and generated artifacts. The right response is not always "block the agent." Often it is "require the owner who understands the consequence." A migration can pass unit tests and still make rollback impossible.
Assign reviewers by API only after the pull request exists and verify the resulting assignment. If an owner group cannot receive requests, the controller should mark the pull request as blocked rather than silently substituting a random developer. Silent fallback turns an ownership rule into decoration.
Tests describe evidence, while approval decides acceptance
An agent should state exactly what it ran, what it did not run, and why. "Tests passed" is a useless claim without commands, exit status, and the commit SHA they tested. Store that evidence outside the pull request prose as well, because an agent can edit prose later.
Use a small structured report for each run:
{
"run_id": "run_01J...",
"repository": "acme/payments-api",
"head_sha": "8c71...",
"commands": [
{"command": "npm test -- payment-retry", "exit_code": 0},
{"command": "npm run lint", "exit_code": 0}
],
"not_run": ["integration suite requires payment sandbox approval"]
}
The controller should reject a ready-for-review transition when the evidence names a different SHA than the branch tip. This catches a subtle but frequent sequence: the agent runs tests, makes one more "small" correction, and opens the pull request without rerunning anything.
Required status checks should remain repository-side. The agent must not have permission to waive checks, approve its own pull request, alter branch protection, or merge. A check can establish that a known command passed. It cannot establish that a new authorization rule is correct, that the requirement was understood, or that the task should have been attempted in that repository.
Do not let a generated summary substitute for a diff review. Good summaries help reviewers orient themselves, but they are claims made by the author. Reviewers need the actual file changes, tests, relevant issue context, and any deliberate omissions.
Every created change needs an audit trail that can survive an incident
A pull request URL is not an audit record. It disappears when repositories move, branches are deleted, access changes, or someone edits the description. Keep an append-only event record that lets an investigator answer who initiated a run, which process made each call, which repository changed, and what the system returned.
Record these fields at minimum:
- a run ID and the original task or ticket reference
- the acting bot identity and authenticated agent process identity
- repository, base branch, base SHA, head branch, and every created commit SHA
- API request type, immutable pull request identifier, timestamps, and result status
- reviewer requests, approvals, check results, closure, merge, or rejection events
Do not put plaintext credentials, full source files, or arbitrary task prompts into the audit log by default. Investigators need reliable action facts, not another uncontrolled copy of sensitive material. If you keep patch content, record a content digest and apply your normal retention and access rules.
The distinction between an activity log and a decision log is worth preserving. An activity log says that an API call created pull request 418. A decision log says who approved that agent session, who changed its authorization, and who revoked it. During an incident, both matter. You need to know what happened and why the actor had authority at the time.
Sallyport can keep agent sessions and individual HTTP or SSH calls in journals projected from its encrypted, hash-chained audit log, and sp audit verify checks the chain offline over ciphertext. That suits a setup where the agent asks for an action but never receives the repository credential itself.
Hash chaining makes later modification detectable; it does not make a weak event record complete. Log the repository and commit identities at the action boundary. A perfectly verified record of "HTTP request sent" will not tell you whether the request created the wrong pull request.
Keep credentials outside the agent and separate creation from merge
The agent should never hold a broad repository token in its prompt, environment, workspace file, or tool output. Once the token enters that context, it can leak through logs, shell history, error messages, copied transcripts, or an instruction that persuades the agent to print it. Redaction after the fact does not reliably put a secret back under control.
Use an action gateway or narrow controller that accepts a specific request: create a branch in this repository, push these commits to this allowed prefix, create a draft pull request against this approved base branch, or request these reviewers. The gateway injects credentials and returns the result. It should reject calls that do not fit the declared scope.
Creation rights and merge rights are different privileges. A service that can create a pull request may be allowed to propose thousands of bad changes. A service that can merge can put one bad change into production. Do not combine them because it makes a demo smoother. Keep the merge action with protected branch rules and an accountable human, even when the agent generated a flawless patch.
Per-run authorization is also better than a permanently trusted agent process. Agent tools can spawn subprocesses, be reused in unexpected jobs, and remain alive after the task that justified access. Give a process one bounded session, record the approval, and make revocation immediate.
A failure drill exposes gaps that policy prose hides
Run a controlled failure drill before relying on automated pull request creation. Use a test repository or a disposable branch policy, and give the agent a task that tries to cross each boundary. The point is to verify denial and recording behavior, not to admire a happy-path demonstration.
Start with an allowed repository and a prohibited repository. Confirm that the controller can create the expected draft pull request in the first and rejects the second before any branch appears. Then attempt a push to main, a force push to an agent/ branch, and a pull request against an unapproved release branch. Inspect repository events, not only controller messages.
Next, simulate a timeout after the create request reaches the repository API. Restart the run and confirm that it finds the original pull request rather than opening another. Change the branch after recording test results and verify that the ready transition stops on the SHA mismatch.
Finally, revoke the agent session during a run and attempt another API call. The call should fail, the denial should appear in the decision record, and no credential should be visible in the agent output. If any of these tests rely on a human noticing a chat message, the control does not exist yet.
The first practical action is to inventory the token your agent uses today. List every repository it can touch, every branch it can update, and whether it can merge or alter settings. Most teams find that the token is broader than the job. Reduce that scope before asking the agent to produce one more pull request.
FAQ
What permissions should an AI agent have to open pull requests?
An agent should use a dedicated bot identity with only repository and API permissions needed to create branches, push to an allowed namespace, and open pull requests. It should not receive maintainer, administration, or merge permissions. The credential boundary matters as much as the prompt.
Should AI agents be allowed to push directly to the main branch?
Give the agent a branch prefix such as agent/ and reject its pushes anywhere else. Protect default and release branches so only your normal merge path can update them. This prevents an agent from bypassing review by choosing a convenient target.
How large should an AI-generated pull request be?
A pull request should contain one coherent purpose with a narrow diff and clear tests. If a task needs unrelated refactoring, dependency upgrades, and a behavior change, split it. Review quality collapses when a reviewer has to reconstruct several decisions at once.
How do I assign reviewers to AI-created pull requests?
Use repository ownership rules for the base assignment, then add a responsible human for changes that affect generated files, migrations, permissions, deployment definitions, or security-sensitive code. CODEOWNERS is useful for routing, but it does not replace review requirements. A required reviewer must come from branch protection or merge rules.
What should be logged when an agent creates a pull request?
Log the agent session, actor identity, repository, commit SHA, branch names, request parameters, pull request URL, timestamps, approval events, and merge outcome. Store the original task reference and a digest of the patch if your retention rules allow it. A title and URL alone are not a usable audit record.
Is it safe to let an AI agent create pull requests automatically?
It can be safe when the agent lacks merge rights, works only in allowed repositories, and cannot push to protected branches. Require human review, passing checks, and a clear record of every action. Treat the agent as an untrusted contributor with fast hands, not as a maintainer.
Can an AI agent open pull requests through an API?
Use the repository platform's API to create the branch from an explicit base commit, push commits, and create the pull request against an approved base branch. Check the response, record the returned identifier, and stop if any request fails. Do not scrape the web interface or infer success from a generated URL.
Should an agent update an existing pull request or create a new one?
The agent should propose a fresh pull request when the intended result changed, the original branch has been superseded, or reviewers need a separate decision. It should update an existing pull request only when the task and ownership remain the same. Reusing a pull request to hide a new scope is a review failure.
How do I prevent duplicate pull requests from an agent?
Use idempotency data in your own controller: record a task ID, repository, base SHA, branch name, and created pull request number before retrying. On retry, query for the branch and existing open pull request before creating another. Most repository APIs will accept repeated requests; that does not make duplicates harmless.
Do passing tests make AI pull requests safe to merge?
No. A clean diff says little about whether the agent chose the right behavior, touched the correct repository, or used an approved dependency. Tests and review rules must remain mandatory, and humans need enough context to judge the requested change.