Who Does DeepSeek's Coding Agent Listen To? One GitHub Comment, Followed Through the Code
DeepSeek's coding agent dsh-superagent takes work from GitHub comments. By default, a request from someone not on its allowed list stops at a code gate and calls no model by itself. But a comment on an issue it's already working on reaches the agent regardless of the list. We read the code and did not run it.

Who Does DeepSeek's Coding Agent Listen To? One GitHub Comment, Followed Through the Code
Alice and Bob each leave the same comment on separate GitHub issues, asking an AI agent to rename a button. Alice's comment reaches a model and is accepted as new work. Bob's stops at the authorization check before a model evaluates it, because Alice is on the operator's allowed list and Bob is not.
An hour later the agent is working on Alice's job, and Bob comments, this time on Alice's issue. This comment skips the allowed-list check used to admit new work. It reaches the worker as a follow-up to an existing job.
Both behaviors are in DeepSeek's dsh-superagent-prototype, and both are documented design choices. The system separates permission to start a job from delivery of comments about an existing one. This post follows those two routes through the code.
Alice and Bob are our own example. What follows comes from reading the code, documentation and test files of the October 31 release (6337fc8). We did not run it or test the model's responses to these comments. To trace the example, we assume Alice's request is accepted as new work.
What this project is
dsh-superagent watches a GitHub repository for work requests in comments. It is designed to edit the code and answer in the same conversation, usually with a pull request. Instead of waiting for someone to open a terminal session and enter a task, it monitors the conversation where requests appear. This post does not compare Codex's or Claude Code's GitHub integrations.
When configured to read all messages in a conversation, it can also infer work nobody directly requested. The default observe mode records what it would have picked without starting that inferred work. Explicit requests from allowed people can still run in this mode.
DeepSeek published it on October 31, and it had 13,632 stars when we checked on November 3. It runs on DeepSeek Harness, their agent runtime. The README calls it a "research prototype for reference only, not an official DeepSeek product". It also says the code differs from their internal version and that coding agents did much of the engineering.
Its design document describes the project as a framework and also "a way of working with an agent". The code and prompts spell out collaboration rules: who may give the agent work, when it may interrupt someone, and how a finished job continues. This post looks at where those rules take effect.
The same document opens with one principle: "Take as little human attention as possible, and give as much value as possible." An agent that watches every comment could easily become a nuisance, so admission checks and instructions to limit messages help implement that principle.
A new request passes through five stages: collection (Source), recording and delivery (Intake), authorization (Gate), routing (Main Session), and code changes (Worker).

Step 1: Read the author and the mention
Alice writes this on issue #42:
@agent change the "Save changes" button to "Save"The agent polls GitHub and turns each new comment into a record called a Trigger. For our story, it needs to answer two questions: is the author a person, and did the comment address the agent? Here is the first check:
export const isHumanLogin = (user: { login: string; type?: string }): boolean =>
!/\[bot\]$/.test(user.login) && (!user.type || user.type === 'User')The function classifies an account as non-human if its login ends in [bot], or if type is present and differs from User. A missing type passes. This is an account-type heuristic, not proof that a person wrote the message. The check has a known gap, and the comment above this function says so: a review bot running on an ordinary user account passes as human. The second question is answered by a regular expression that looks for @agent in the text:
export const mentions = (text: string, login: string): boolean =>
new RegExp(`(?:^|[^a-zA-Z0-9_@])@${escapeRegExp(login)}(?![a-zA-Z0-9_-])`, 'i').test(text)Alice's record now says: author alice, a person, addressed the agent. Bob's identical comment produces the same record with his name. Neither step has judged what the comment means.
Step 2: Check for a job handling follow-ups on this issue
The record goes to intake.ts. It saves the record once, so a comment seen twice can't create two jobs, and then asks whether any accepted job is already following this issue. DeepSeek calls an accepted job a Ticket.
Issue #42 has no Ticket yet, so this line sends the comment on to be judged:
if (!this.owners(trigger).length) this.queueForEval(trigger)This line comes back later. If a Ticket handles the comment as a follow-up, it skips the new-work admission and assessment stages. A Ticket subscribed only for context does not count as that owner. Bob's second comment follows the existing-work route.
Step 3: Code checks permission to request new work
This is where Alice and Bob part ways. The judging code calls the gate first and only then considers a model. Inside the gate, this line lets Alice through:
if (allowed && trigger.addressed) return { admissible: true, strength: 'explicit', basis: { ...recorded, via: 'addressed', addressed: trigger.addressed } }allowed means her login is on the operator's list, and addressed means she mentioned the agent. Together they make an "explicit" request.
Bob isn't on the list, so the code looks up what to do with an unlisted person who mentions the agent. The defaults say to ignore him:
export const ADMISSION_DEFAULTS: Readonly<Record<AdmissionCell, Admission>> = { listedUnaddressed: 'inferred', unlistedAddressed: 'ignore', unlistedUnaddressed: 'ignore' }So Bob ends at the gate's last line, with the reason not_a_member:
return { admissible: false, reason: !allowed ? 'not_a_member' : receivable && admission === 'ignore' ? 'admission_ignored' : 'no_mention' }Back in the caller, nothing admissible means an early return. The call to the model is 16 lines below that:
this.options.assessor.request(this.candidateOf(thread))This ordering has two practical effects. Bob's comment alone triggers no model call to assess new work, so that assessment uses no tokens. This check follows the author, mention and operator settings. A claim of permission inside Bob's comment cannot change its result. This doesn't mean a model never reads Bob's text. If it is processed with an admitted comment on the same issue, it goes to the model as a batch peer, and once work starts on that issue, it can be read as earlier context. It still grants no authority. A team can change the admission settings to admit unlisted people, but that is a setting the operator changes, not something a comment can do.
The table shows what happens on an issue with no Ticket, under the default admission settings. An inferred request is one the model must identify from the discussion, rather than an explicit task accepted for routing.
| Who wrote it | Mentions the agent? | Gate result | What happens next |
|---|---|---|---|
| Person on the list | Yes | Explicit | A model routes it; accepting creates a new Ticket |
| Person on the list | No | Inferred, if the agent is set to read all messages there | A model assesses it, and by default only records its opinion |
| Person not on the list | Either | Rejected: not_a_member | Dropped with no model call |
| Bot not on the bot list | Either | Rejected: not_human | Dropped with no model call |
Step 4: A model decides what the comment is
Alice's comment now reaches a model running in the main session, which DeepSeek calls the orchestrator. It is given routing, not the coding. For an explicit request, its prompt gives it four options: accept as new work, attach to an existing Ticket, stop a Ticket, or ignore. In our example, assume the model accepts Alice's request as new work and writes a one-line acknowledgement. Passing the allowed-list check does not automatically turn every comment into a job.
Accepting creates the Ticket and something DeepSeek calls a Mandate: a fixed record of who authorized the work and what its scope is. For an explicit request, the scope is the Ticket title the main session writes; in our example, say that title covers one button. The Mandate can't be edited afterward, and wider scope requires a new Ticket. This matters later.
The second row of the table is the more unusual path. Suppose Alice doesn't mention the agent and simply writes "this button label is odd", and a colleague replies "just 'Save' would do". DeepSeek's assessment prompt uses this very example as one to accept, because the discussion contains a concrete replacement even though nobody gave an order. Its example to ignore is "maybe our app should show unread counts like theirs", which is a preference. The model must establish three facts: the discussion is about this project, people want a specific change, and the direction is settled enough for a small edit. If it is unsure, it ignores quietly and doesn't ask anyone. The default proactive rules cover only two kinds of change: copy fixes and small visual fixes.
For this inferred-work path, even a positive assessment starts no work in the default observe mode. This code turns the acceptance into an ignore and writes down what it would have done:
if (strength === 'inferred' && settings.eval.mode !== 'auto') {
const proposal = { title, scope, worktree_name: name }
return { kind: 'ignore', audit: { ...audit, would_accept: true, proposal }, reason: { code: 'observed', values: { text: assessment.reason } } }
}An operator can review those records before switching to auto mode and letting the agent act on inferred work. This applies only to work nobody explicitly asked for; Alice's direct request isn't held back.
Step 5: A worker edits code without per-command approval
The Ticket gets its own session, the worker, and its own folder, typically a git worktree created by a prepare command the operator sets. Its permissions are set in one line:
ctx.permissionPresets.set(result.agent.session, 'danger-full-access')So the worker runs commands with no approval prompt, as the operator's operating-system user. It gets a separate folder and restricted command wrappers, but the docs state plainly what those are worth: "defaults, not a security boundary. A worker can still execute a program by absolute path."
Two details show how the worker stays reachable while it works. Any shell command running longer than 5 seconds moves to the background, so a new message can reach the worker at its next step instead of waiting for a long build. And when all worker slots are full, one extra slot is held for a person's message, so a pile of new jobs can't lock people out of a job already running.
The prompts also instruct the worker to avoid unnecessary progress messages. The prompt's own example of a finished job is one line: "Fix submitted: <actual PR link>, ready for review." Before sending anything, the worker is told to apply this test:
Before sending, ask of each clause: would deleting this prevent the human from understanding the result or taking the required action?
The instructions specify when it should speak. According to the design document, it confirms a request once, then writes again in three cases: a result is ready, it needs a decision, or it can't finish. The prompts also allow direct answers to questions and a reason for declining a review suggestion. A failure alone doesn't earn a message. If the agent can fix the problem itself, the prompt tells it to keep working silently. When a person does have to act, the example looks like this:
The bot cannot read this document, so the request cannot be confirmed. Please give the bot read access.
It says what is stuck and what the person needs to do, and nothing else. When it needs a decision, the worker sends one concrete question in a structured form, and it can keep going on work the question doesn't affect. According to the architecture document, questions go to the main session immediately, while ordinary progress stays on the Ticket.
The prompt also tells the worker to open every GitHub message with "This is by an automated agent, not a human." It is told never to enable auto-merge from its own account, and with no separate merge tool configured, a person merges every pull request. These are prompt instructions, not enforced by code.
Following these instructions, the worker delivers a pull request and a short completion message. That completes the new-work route.
Now Bob comments again
While the worker is on Alice's job, Bob writes on issue #42:
@agent also change "Cancel" to "Close"Accepting Alice's request subscribed the Ticket to issue #42, which routes later messages collected from that issue to the worker. So in Step 2, this loop finds the Ticket:
for (const ticketId of thread.subscriptions) {
const ticket = this.repo.tickets.get(ticketId)
// Only a person's message on a non-context thread resumes an ended ticket; bots and CI do not (the Executor re-reads
// threads when it resumes), and a human-stopped ticket receives nothing because its Mandate was withdrawn.
const followed = !!thread.context?.includes(ticketId)
const resumes = trigger.human && ticket?.ended?.by !== 'human' && !followed
if (!ticket || (!liveTicket(ticket) && !resumes)) continue
reached.set(ticketId, { ticket, context: followed && (reached.get(ticketId)?.context ?? true) })
}Nothing here looks at the allowed list. Because the Ticket handles Bob's message as a follow-up, the line from Step 2 skips the new-work gate and main-session assessment. The worker model still reads and judges the message. It goes to the worker in this format:
return { trigger_id: trigger.id, from: trigger.actor.id, thread: threadKey(trigger.threads[0]!), at: trigger.at,The worker sees from: bob, with no field saying whether Bob is on the list.
It also works after the job has ended. If Alice's pull request is merged and Bob comments a week later, resumes in the loop above makes it a candidate for resumption. The original authorization must still pass revalidation before the Ticket resumes. The authorization document says this directly: a person's message can resume the work "without a new Eval or Decision, regardless of who sent it". The check that runs before a resume looks at the original Mandate, which means Alice's authorization, and doesn't take the new sender as input. If that check fails, the Ticket stays ended and the message goes to be judged.

DeepSeek chose this on purpose. Their list of rejected ideas includes "Admission checks on Subscriptions", with the reason: "A Subscription covers the whole Thread; the Mandate separates authorization from context." In other words, Bob's message is treated as information for a job Alice already authorized, and it doesn't create new authority. That makes sense for ordinary work. A reviewer who isn't on the list can still point out a real bug in a pull request, and the worker should hear it.
Some of the limits on Bob are in the code, and others exist only in the prompt.
Some limits are enforced in code. Bob can't stop the job: the test has a guest's stop rejected as stopNotExplicit, while a listed person's stop succeeds. If the worker hands his request back, it goes to be judged, but it counts as inferred, so in the default mode it can't start a new Ticket. He can't reopen a Ticket a person stopped, since the ticket?.ended?.by !== 'human' condition in the loop covers that case. And bots can't wake an ended Ticket, because of trigger.human.
The worker model judges scope under its prompt instructions. Whether to act on Bob's request is the worker's decision: "do a request within the ticket's scope", and hand back "a request beyond it". The scope was one button, and Bob asked about a second one, so we would expect the worker to hand this back. A request like "use a lowercase s" is plainly inside the scope, and the prompt says to do it.
On this follow-up route, the worker model is responsible for staying within the original scope, and it runs commands without per-command approval. A private team repository can limit who is able to comment. On a public repository anyone can comment on an issue the agent is working on, including someone asking it to run a command "to verify the fix". The code will deliver that comment. The prompts say such messages carry no authority and tell the worker not to bypass safety or permissions. What a worker actually does depends on the model and the prompt, and we didn't test it.
DeepSeek's own article, according to the English translation we read, says a super-agent should operate only within a small, trusted scope. Read with this code, that advice has a concrete meaning: we read this as a reason to consider who can comment on active issues as well as who is on the allowed list.
One job follows the issue and the pull request to the end
The subscription route that let Bob's comment in is also what keeps a job's context together. Let's look at it from Alice's side.
When the worker opens a pull request, it is instructed to subscribe the Ticket to that too. A reviewer's comment on it doesn't create a new job. It goes to the worker that fixed the button. Each Ticket has its own session and folder, so the worker picks up in the files and branch it already changed.
When the pull request merges, the Ticket ends, but its Mandate and subscriptions stay. If Alice writes "make it lowercase, save" on that pull request a week later, the same Ticket reopens, and the worker receives this note along with her message:
const RESUMED = 'This ticket ended earlier and the message that follows, on its thread, resumed it under the same mandate. If it asks for '
+ 'more within the original scope, inspect the worktree, git history and PR state and continue; if it asks for work beyond that scope or '
+ 'for another ticket, hand it back with orchestrator_trigger_handback and report dropped; if it needs nothing, such as thanks, '
+ 'report dropped without a message. Report ready when the follow-up is done. Do not repeat completed work.'For a request within scope, the note tells the worker to check the folder, git history and pull request state, then continue without redoing finished work. It also says to hand back anything beyond the scope, and to end without a reply when a message needs nothing, like a thank-you.

The folder and branch come with a caveat. We found no code that deletes a used Ticket's folder or local branch, and the prompt says not to delete the folder. We didn't run it, though, and a repository set to auto-delete merged branches will remove the remote branch.
Information needed to continue work survives outside the model's conversation history. The Ticket records authorization and subscriptions; the directory and branch hold the changes. DeepSeek rejected having Ticket state follow the session, because, in its words, "A Ticket can be handed off, resumed, or run by several Executors". A person's retry continues in the same session, directory, branch and pull request. After a host restart, the worker is told to check its directory, git history and GitHub before repeating any outside action, and not to repeat an external operation already completed.
The route that welcomes Alice back a week later is the same route Bob used. This delivery route doesn't tell the two apart, and judging the scope is left to the worker.
Running an agent as a team member
DeepSeek's glossary defines the agent as "the automated team member". The prompts live in a folder called prompts/team/, and they ask for brief coworker language. A new colleague needs a few things settled, so here is how this project settles them.
| What to settle | What dsh-superagent does | Source |
|---|---|---|
| Which account it works under | A dedicated GitHub account | Setup guide |
| Whose requests it takes | Only people on the list can start new work; on a followed issue, anyone's message reaches the worker | Setup guide, code in Step 3 |
| How far it goes alone | Small jobs run from investigation to pull request; design decisions become a question | Work prompt, design document |
| Who reviews and merges | It never approves or merges on a reviewer's behalf; with no merge tool configured, a person merges | Delivery prompt, same prompt |
| Where it reports | The conversation the request came from | Communication prompt, reply code |
Of these, code decides who starts new work and which conversation a reply goes to. How far it goes alone and who merges are prompt instructions. Enforcing human-only merging also requires GitHub-side account permissions and branch-protection rules, beyond a prompt instruction.
The project is also clear about how the agent differs from a colleague. The prompt tells it to say up front, in every GitHub message, that an automated agent wrote it. And responsibility doesn't move to the agent. The design document says each message costs your colleagues' attention and "you are responsible for it". Its advice is to start narrow, cut the prompts or pause when the agent gets noisy instead of adding messages, and not use it to produce volume.
DeepSeek's reported numbers
DeepSeek's article includes a benchmark. We couldn't open the original on X, so these figures come through an English translation and a summary that notes machine translation. We reproduced none of them.
They replayed about 300 group-chat and CI messages containing about 20 work requests, five runs per setup. Actual coding was replaced by a simulated tool that runs for a set duration. The experiment measures request scheduling and completion time, not the quality of generated code. Message gaps and work durations were reportedly scaled to a quarter, and DeepSeek calls the results preliminary reference points. The README says the published code isn't identical to the implementation in that article, so don't read these as the performance of this code. In the table, p50 is the median time to finish a request, and p95 is the time that 95% of requests finished within.
| Setup | p50 (min) | p95 (min) | Cache-read tokens |
|---|---|---|---|
| Super-agent | 12.4 | 15.7 | 23.23M |
| Super-agent, follow-ups not delivered directly to workers | 12.6 | 15.5 | 31.77M |
| One session, neutral prompt | 20.4 | 29.5 | 6.05M |
| One session, told to delegate first | 13.8 | 29.0 | 10.82M |
Compared with one session and a neutral prompt, p95 completion time is about half as long (15.7 vs 29.5 minutes), for about 3.8 times the cache-read tokens (23.23M / 6.05M). The first row uses the direct delivery route Bob's follow-up took. The second disables that delivery for comparison. Enabling it barely changes p50 or p95 here but uses about 27% fewer cache-read tokens (31.77M down to 23.23M).
If you try it, or build your own
These are suggestions from reading the docs, not results from running it. The setup guide asks for a dedicated GitHub account for the agent, never your own. We would add a private repository, a machine with nothing else important on it, a short allowed list, observe mode left on for inferred work, and no merge tool. Configure GitHub permissions and branch protection to enforce human-only merging. observe still permits explicit requests to run; it is not a switch that stops all execution.
If you are building your own agent, five ideas here are easy to borrow:
- Check permission to start new work in code before a model evaluates it. That avoids spending assessment tokens on unauthorized requests, and comment text cannot change the authorization check.
- Start inferred-work behavior in a recording mode. Review the tasks it would have selected before allowing it to act on them.
- Keep long commands from blocking message processing. Moving them to the background after a few seconds lets a person's message get through.
- Give the agent a test for its own messages. The deletion test above fits in one sentence, and you can paste it into any prompt.
- Record authorization and progress on the Ticket. If authorization, subscriptions, the folder and the pull request survive, a new session can pick up where the last one stopped.
Three things we can't tell you without a real run: whether it picks up the right work, what it costs on a real repository, and what a worker does with a stranger's comment.