Agents in the Code Host: Triggering on Text Anyone Can Write
An issue is text, anyone can open one, and if that text starts an agent it is now an instruction running with your permissions.
The value is that the work lands where the team already looks
An agent wired into your code host — reading issues, opening pull requests, commenting, reacting to reviews — is not adding a capability so much as removing a boundary. The work appears in the same place as everyone else's work, in the same format, subject to the same process.
That matters more than it sounds. An agent operating in its own interface produces output that somebody has to transfer into the team's workflow, and transfer steps are where things get dropped. An agent that opens a pull request has already done the transfer, and the team's existing habits — review, CI, branch protection, merge queues — apply without anyone deciding they should.
The risk arrives from the same property. Once the code host can start agent work, the code host's inputs become instructions, and its inputs are open to a wider set of people than your repository is.
Triage is the best fit and the safest one
Of everything in this category, issue triage has the best ratio of value to risk. Reading a new issue, checking whether it duplicates an existing one, identifying the likely component, adding labels, asking the reporter for the missing reproduction steps — all of it is reading and annotation, none of it changes code, and it is exactly the work that decays when a team is busy.
It is also self-correcting. A wrong label is trivially fixed and visible to everyone. Compare that with an agent that pushes a fix based on a misunderstanding of the issue, where the error is discovered later and costs more.
Pull request comments as a task queue
The other natural fit. A review comment is a well-specified task with context attached: it names a file, a line, and what is wrong. "Address the review comments on this branch" is a genuinely good instruction, because the specification work has been done by the reviewer.
Two conditions make it work. The agent should push changes as new commits rather than rewriting history, so the reviewer can see what changed since their comment. And it should reply to each comment saying what it did, rather than silently pushing — otherwise the reviewer has to diff the branch to find out whether their point was addressed or ignored.
The dangerous chain
Here is the thing to understand before enabling anything that triggers on repository events.
An issue is text. On many repositories, anyone can open one. If an issue's text can start an agent, and that agent runs with credentials that can push branches, comment, or trigger workflows, then a stranger's text has become an instruction executed with your repository's permissions. That is not a hypothetical shape; it is the same trust-propagation problem described in implicit trust propagation, with an unusually wide entry point.
The instruction does not have to be obvious. It can be embedded in a code block, in a stack trace, in what looks like log output pasted by a helpful reporter.
Do not try to detect malicious instructions in issue text. Constrain who can cause an agent to run: a maintainer applying a label, a maintainer commenting a command, a member of a named team. Content filtering is a losing game; an authorisation check is a solved one.
Trigger on a maintainer action, always
The practical rule that follows. An agent should never start work because an issue was opened. It should start because someone with write access did something deliberate: applied a label, commented a command, assigned it.
That one constraint removes the entire class of problem above, costs a click, and has the pleasant side effect of making agent activity intentional rather than ambient. It also gives you a name attached to every run, which is what you want when reviewing what happened.
on:
issues:
types: [labeled]
permissions: # default to nothing, add only what is needed
contents: write # push a branch
pull-requests: write # open the PR
issues: write # comment back
jobs:
run:
if: >
github.event.label.name == 'agent:fix' &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'),
github.event.sender.type == 'User' &&
github.event.issue.author_association)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run the agent
env:
ISSUE_BODY: ${{ github.event.issue.body }} # data, not a command
run: ./scripts/agent-task.sh
Two details in that file matter. Permissions are declared explicitly and narrowly rather than inherited. And the issue body is passed through an environment variable rather than interpolated into a shell command, because interpolating untrusted text into a command line is its own vulnerability entirely separate from anything the agent does with it.
Fork pull requests are a separate hazard
A workflow triggered by a pull request from a fork runs code the fork author controls. Giving that workflow secrets, or write permissions, hands both to a stranger. This is a well-known hazard in CI generally and it gets reintroduced regularly by agent integrations that want to comment on incoming pull requests.
The safe pattern is the standard one: the workflow that runs on untrusted code has no secrets and no write access, and anything requiring privileges runs separately on the result. If your agent integration cannot work that way, it should not run on fork pull requests at all.
Split the token by what it is doing
An agent that triages issues needs to read issues and write labels and comments. It does not need to push code. An agent that opens pull requests needs to push a branch and does not need to modify repository settings or manage releases.
Configuring separate credentials per job is slightly more work than one token with broad scope and it is the difference between a mistake that adds a wrong label and one that force-pushes a branch. Grant per workflow, minimally, and never reuse a personal token with your own full permissions — which, on many repositories, is the quiet default.
Make agent activity visibly agent activity
Every comment, commit and pull request from an agent should be identifiable as such: a distinct bot identity, a consistent label, a trailer in the commit message. Not for propriety — because the reader's calibration should differ, and because you will want to filter later.
It also makes the record answerable. "How much of what merged last quarter was agent-authored" is a question every team eventually asks, and the answer depends entirely on having marked it at the time. The conventions for doing that are in normalising agent commits.
The noise problem is real
An agent commenting on every issue and every pull request quickly becomes something people filter out, and once filtered, the useful comments go with the rest. This is the fastest way to waste the integration.
Keep it quiet by default. Comment when there is something to say — a duplicate found, a question that blocks progress, work completed. Do not comment to acknowledge, to say work has started, or to summarise what the thread already says. A bot with a low comment rate gets read.
An agent must not approve its own work
Worth stating explicitly because the tooling makes it possible. If an agent can open a pull request and also approve it, and branch protection is satisfied by an approval, then the approval requirement has been removed without anyone changing the setting.
Branch protection should require a human approval, distinct from the author, for anything an agent opens. And an agent review pass, which is genuinely useful, should post comments rather than an approval — it is input to review, not a substitute for it. The gate design that supports this is in CI gating for agent pull requests.
What to allow without a human, and what not
Without: reading, searching, labelling, deduplicating, asking a reporter for missing information, summarising a long thread, posting review comments, opening a draft pull request.
With: converting a draft to ready, merging anything, closing an issue as resolved, modifying repository settings or protections, publishing a release, triggering a deployment.
The line is whether the action is recoverable and whether it terminates something. Labels are recoverable. A closed issue is technically recoverable and socially not — the reporter has been told their problem is solved, and if that was wrong the damage is to their willingness to report the next one.
Watch what accumulates
A final operational note. Integrations like this tend to be set up once and then run for a year without review, by which time the trigger conditions, token scopes and workflow permissions no longer match what anyone intended.
Put a calendar reminder on it. Read the workflow file, check the token scopes, look at who has triggered runs, and confirm the fork behaviour is still what you think. Fifteen minutes, twice a year, on a piece of automation holding write access to your repository is a reasonable trade.
The commit that arrives with no branch
A specific failure worth guarding against: an integration configured to push directly to the default branch because that was simpler to set up. Everything in this article — review, protection, the human approval — is bypassed by that one convenience, and it tends to be introduced during a quick experiment and never revisited.
Branch protection on the default branch is the control that makes the mistake impossible rather than discouraged, and it is worth confirming explicitly rather than assuming, because a token with administrative permissions can often push through protection without anyone noticing that it did.
Scheduled agent jobs deserve extra suspicion
A workflow that runs nightly with repository credentials has all the properties of the unattended case: no human watching, no interactive approval to lean on, a wide window before anyone looks. It is also the configuration most likely to be forgotten.
Give scheduled jobs the narrowest permissions of anything in your repository, have them open drafts rather than ready pull requests, and make them post a summary somewhere a person actually reads. A nightly job producing work nobody sees is a job that will keep running long after it stopped being correct.
What it looks like set up well
Triage runs on every new issue with read and label permissions only. Fix attempts run when a maintainer applies a label, with contents and pull-request write and nothing else. Fork pull requests get a workflow with no secrets. All agent output carries a bot identity and a label. Branch protection requires a human approval that the agent cannot supply. Nothing merges, closes or deploys without a person. And someone reads the workflow file twice a year.
That configuration is boring, and boring is the objective for automation that holds write access to the place your code lives.
One closing thought on why this is worth the care. An integration with your code host is the one place where an agent's mistakes are permanent and public — a comment on a stranger's issue, a branch in a repository other people watch, a closed ticket someone was waiting on. Everything else an agent gets wrong stays inside your machine until you decide otherwise. This does not.
Start with triage and earn the rest
A sequencing suggestion for teams adopting this. Begin with read-and-label work, which is useful immediately, changes nothing, and gives the team a period of watching an agent operate in their repository with no risk attached.
That period is worth more than it sounds: people calibrate, the noise level gets tuned, the identity and labelling conventions settle, and the objections that would have derailed a bigger rollout surface while the stakes are nothing. Then add branch-opening behind a maintainer trigger, and only later consider anything that merges. Teams that start at the other end spend their credibility before they have any evidence to spend it on.
Wiring an agent into your code host puts its work where the team already looks and subjects it to the process you already have, which is most of the value. The hazard is that issue and comment text becomes an instruction executed with repository credentials, so trigger on a deliberate maintainer action rather than on an event, pass untrusted text as data rather than interpolating it, declare workflow permissions narrowly, keep secrets away from fork pull requests, and never let an agent's approval satisfy branch protection on its own work.