Get your agent to react when a PR merges, not wait for you

Your agent only moves when you type. How to let a merged pull request, or any event, tell it instead, and seven ways it stays silent.

· Jeremy

When my agent's code is accepted, I am the one who has to tell it. A signed message, one that proves who sent it, can do the telling instead.

My agent's code change was merged a minute ago, and the agent has no idea. So I type it: "It's merged, fetch it and carry on." Another day it is "Go look, a test failed." In most setups the program that wrote the code cannot hear that it was accepted, or that it broke, unless I tell it. I am its messenger.

On GitHub that change is a pull request, or PR, a change waiting for someone to approve it. On 9 October I wired a test PR into two agents, ChatGPT and Claude Code, so that closing it would wake them. It worked, after seven different kinds of silence, and the short list of what to do sits under "Start Here".

A close and a merge send the same message, with a yes-or-no field called merged that tells them apart. Test PRs 2 to 7 were closed without merging; PR 8 was merged later that day, and its message said so.

It Is a Webhook. Who Hears It?

When Anthropic, the company behind Claude Code, shipped a way to push events into a running session, a commenter on Hacker News (a forum where programmers argue) summed it up in four words: "so its a webhook."

They were right, and that is the useful part. A webhook is a message a website sends to a web address you choose when something happens, here a message saying a PR closed. What it leaves out is who is on the other end: a program that is awake, willing to accept the message, and able to show it to the agent.

Our test PR 6 shows the whole chain working. A background program of ours, a daemon from agentproto, the agent software I work on, noticed the close and passed it on. Within the minute, the words "PR 6 fermée", French for "PR 6 closed", were sitting in a ChatGPT chat.

A timeline from 15:29:00 to 15:30:00 UTC. PR 6 closes at 15:29:02 and the daemon stamps the event at 15:29:52, a span of 50 seconds. The ChatGPT chat shows "PR 6 fermée" before 15:30:00. Below, three bars show the delay between a close and the daemon's stamp: PR 5 about a minute, PR 6 50 seconds, PR 7 about 55 seconds.

From close to chat in under a minute, most of it spent waiting for the daemon's next look. The three delays below are samples, not a distribution.

The wait before the stamp is our daemon, which asks GitHub whether anything changed. This is closer to a clock than to a live stream: it ticks about once a minute.

Four Doors

Where the message lands decides what the agent can do with it.

The first door is ChatGPT. OpenAI, the company behind ChatGPT, lets it subscribe to events from any server that speaks MCP Events (a draft extension of MCP, the standard way for outside software to hand tools and messages to an agent). An automation, a stored instruction ChatGPT runs when an event arrives, does the rest. Ask it to create an automation triggered by the event and it works. Ask it to "use the plugin" and it hunts for a tool it does not have. This is the door behind PR 6.

The second is a channel, a research-preview feature that pushes a message into a Claude Code session while the session is open. We built a small bridge so a session could subscribe to the same events. In a recorded run, the agent subscribed to one PR, the PR closed, and the event arrived in the conversation. The agent wrote "Event received!" and stopped, because we had only asked it to wait. Anthropic's documentation says events arrive only while the session is open.

The third is a hook, a small script Claude Code runs at set moments. A hook is a delay, not a wake-up: nothing reaches the agent until the session does something, and I did not test an idle one. Ours reads a file where a small listener of ours drops each verified event. The hook then slips the events the session has not seen into its next prompt. It worked in the terminal and in the Code tab of the Claude desktop app.

Restart the session after adding a hook: in our run, a hook added to a project loaded only when a session started.

The fourth is a sentinel: a watcher inside the daemon with three parts, what to watch, when to stop and what to do. What to do can be to wake an agent session or to send a signed webhook. The webhook version is what fed ChatGPT. The session version exists in our design and I have no run that proves it, so I will not tell you it works. It is the most flexible of the four, because a watcher can look well beyond GitHub.

Claude's Chat and Cowork modes in the desktop app, and claude.ai in the browser, have no door at all.

A table of four doors and a fifth row for no door. ChatGPT automation: wakes an automation when the event arrives, seen in a run on PR 6. Channel: wakes a live Claude Code session only while it is open, seen in one run at about 12:51. Hook: reaches the next prompt of a session, worked in the terminal and the Code tab, idle session not tested. Sentinel: wakes a session or sends a signed webhook, the webhook fed ChatGPT, session wake not run. Claude Chat, Cowork and claude.ai have no door.

Each door and what our runs showed for it. In the channel run the event arrived at about 12:51.

Seven Silences

Doors are the easy part. The hard part is that every failure looks the same.

PR 5 closed at 14:42. The daemon saw it a minute later and ChatGPT accepted three events, one real and two of our own probes. The chat showed nothing, and so did the task history. Asked "rien reçu ?" ("nothing received?"), ChatGPT answered that it had received three events and described each one. The automation's instruction had been "signal reception", which logs inside ChatGPT and posts nothing.

That was one cause. The afternoon produced seven separate reasons for "nothing happened", or for looking as if nothing had:

  • an action that logs and posts nothing (PR 5);
  • a required delivery header we left out, so ChatGPT's side answered with an error code, 400 ("Missing MCP subscription ID");
  • a reply to ChatGPT's subscribe request that ChatGPT rejected with "unexpected error";
  • a list of available events cached when ChatGPT first connected to our server, which went stale;
  • the wrong phrasing, "use the plugin";
  • a subscription on a PR that was already closed, which has no moment of closing left to announce;
  • a model that said the event id had not been passed to it.

A chain of six stages: subscribe, PR closes, daemon, ChatGPT, automation, chat. Under each stage sit the causes that went wrong there. Subscribe has three: the subscribe reply rejected, a stale event list, and wrong phrasing. PR closes has a subscription on a PR already closed. ChatGPT has the missing header answered with a 400. Automation has the model saying the id was not passed. Chat has the silent action that posts nothing.

The seven causes placed on the path of one event, numbered in the order of the list above.

The header was the instructive one. OpenAI documents it, so the miss was ours. Our daemon logs no delivery outcome. The 400 stayed invisible until we replayed a signed event against the real receiving end. We had tested only our own receiver. A 200 means "accepted" and nothing more. On OpenAI's developer forum, one developer reported several events answered with a 200 and a task that never ran.

Seven causes might be our own mess, and the header was. Strangers have the same symptom with other causes. On GitHub, a developer called rokrokss reported a problem with a ChatGPT dot, OpenAI's name for its always-on agent. The dot "receives deliveries and starts a run, but the run never sees the event data". A Work chat on the web, with the same account and event, does.

Ours was the reverse. The automation claimed it had not been given the event id. For PR 7 we asked it to copy its raw input, and it returned the whole message, id included. The model had not looked. A model's account of what it received is not evidence, so ask for the raw input before you rewrite your message.

Past GitHub, and What Is Still Missing

Nothing here depends on GitHub: whatever can send a signed message, or be watched by a program that polls, can reach an agent. The same plumbing could watch a form that someone fills in so the agent contacts them back, or an API that sells concert tickets, though I have built and measured neither.

One warning from building this: when an agent hands work to helper agents, what a helper finishes or asks has to reach the agent in charge of them, or you become the relay. On 9 October the Claude Code session supervising the workflow run that was writing this article was meant to tell me when the run needed me. Its watcher timed out on the wrong folder, the run waited for my answers, and I found out only by asking "so ?". I was the messenger again, this time between two agents of my own.

The field is also unsettled. Each vendor has its own protocol, and the specification behind OpenAI's is a draft. OpenAI's documentation says that for event types without replay, an event missed during an interruption cannot be recovered through the protocol. Our hook reads the last day of events from a file on disk, so an event that arrived while no session was open still reaches the next prompt.

Start Here

First, what rests on what. I measured one afternoon, on a throwaway repository, with a working run of each of three doors. I did not measure cost, an idle session, a sentinel waking a session, or latency beyond three samples. In the ChatGPT and channel runs, the agent's whole reaction was a message, because a message is all we asked for. The first four rules come from that afternoon.

  1. Ask for a visible action, such as posting a message in the chat. A bare "signal reception" showed nothing.
  2. Test against the real receiving end, and read a 200 as "accepted". Put a witness at every step, something that shows the event reached the next one.
  3. When the agent says it received nothing, ask it to copy its raw input before you suspect your message.
  4. Match the door to the agent: an automation for ChatGPT, a channel for a live Claude Code session, a hook for its next prompt.

The next three rest on reasoning, my experience and the documentation.

  1. List the moments you tell your agent "this PR merged, check that". Start where you are the messenger: wire the first one.
  2. Filter before you wake, or the agent drowns in events. Send each trigger to the agent in charge, the one that coordinates the others (on this very piece, mine missed one).
  3. Check the signature before the agent sees a message. Anthropic's documentation calls an open channel a way for anyone who can reach your receiving end to put text in front of Claude, and our bridge drops anything without a valid signature.

The point of the whole list is to stop being the messenger.

At 15:29:02 on 9 October a test PR was closed. Before 15:30 the words "PR 6 fermée" were in a ChatGPT chat, and I had not typed them.