Appearance
GitHub agents
GitHub agents work both directions. They wake from repo events, read the latest PR and CI state, then write back reviews, comments, checks, or pushes. Pull events from Cursor, so you don't expose a public webhook URL.
Connect GitHub in Cursor for the repos you care about (Settings or cursor.com/dashboard). Sign the host in with agent-sdk login. cursorAccount reuses that connection for events and API calls. You don't set up a GitHub App or a PAT.
Start from a template when one fits: security reviewer or issue triager.
Review a pull request
Wake when a PR is ready for review. Fetch the latest diff in a tool, then post a verdict.
ts
// bot/channels/github.ts
import { defaultGitHubAuth, githubChannel } from "@grok/agent-sdk/channels/github";
export default githubChannel({
// Skip the agent's own comments so replies don't re-trigger it.
botName: "my-agent",
cursorAccount: { repos: ["acme/checkout"] },
onPullRequest: (ctx, pr) => {
if (pr.draft) {
return null;
}
if (pr.action !== "opened" && pr.action !== "ready_for_review") {
return null;
}
// `auth` starts a model turn running as the sender.
return { auth: defaultGitHubAuth(ctx) };
},
});ts
// bot/tools/submit_review.ts
import { parseGitHubPrContinuationKey } from "@grok/agent-sdk/channels/github";
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";
export default defineTool({
description:
"Post the review decision on this session's pull request. Call once.",
inputSchema: z.object({
verdict: z.enum(["approve", "request_changes"]),
summary: z
.string()
.describe("One or two sentences explaining the verdict."),
}),
async execute({ verdict, summary }, ctx) {
if (ctx.session.purpose === "eval") {
return { posted: false, reason: "eval" };
}
const ref = parseGitHubPrContinuationKey(ctx.session.continuationKey ?? "");
if (ref === undefined) {
throw new Error("submit_review requires a GitHub pull request session");
}
let event: "APPROVE" | "REQUEST_CHANGES" = "REQUEST_CHANGES";
if (verdict === "approve") {
event = "APPROVE";
}
const octokit = await ctx.host.github.getOctokit();
await octokit.rest.pulls.createReview({
owner: ref.owner,
repo: ref.repo,
pull_number: ref.number,
event,
body: summary,
});
return { posted: true, pr: `${ref.owner}/${ref.repo}#${ref.number}` };
},
});The wake tells you the PR changed. Fetch title, files, and checks inside the turn or a tool.
Answer a mention
onComment replaces the default @botName gate. Return a turn when the comment is for this agent, then post the reply from a tool.
ts
onComment: (ctx, comment) => {
if (!comment.body.includes("@my-agent")) {
return null;
}
return { auth: defaultGitHubAuth(ctx) };
},ts
// bot/tools/reply.ts
import { parseGitHubPrContinuationKey } from "@grok/agent-sdk/channels/github";
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";
export default defineTool({
description: "Post a comment on this session's pull request.",
inputSchema: z.object({
body: z.string().describe("Markdown to post on the PR timeline."),
}),
async execute({ body }, ctx) {
if (ctx.session.purpose === "eval") {
return { posted: false, reason: "eval" };
}
const ref = parseGitHubPrContinuationKey(ctx.session.continuationKey ?? "");
if (ref === undefined) {
throw new Error("reply requires a GitHub pull request session");
}
const octokit = await ctx.host.github.getOctokit();
await octokit.rest.issues.createComment({
owner: ref.owner,
repo: ref.repo,
issue_number: ref.number,
body,
});
return { posted: true };
},
});Resolve owner, repo, and number from the session. Do not take a PR URL from the model. A later mention on the same PR resumes this conversation.
Triage a failing check
Start a model turn when CI fails. Run host code when the next step is already decided.
ts
onCheckSuite: (ctx, suite) => {
if (suite.conclusion !== "failure") {
return null;
}
// `auth` starts a model turn to read logs and comment.
return { auth: defaultGitHubAuth(ctx) };
},ts
onCheckSuite: (ctx, suite) => {
if (suite.conclusion !== "failure") {
return null;
}
// `task` runs host code instead of a model turn. No chat session.
return { task: () => labelFailure(ctx, suite) };
},ts
async function labelFailure(ctx, suite) {
const pr = suite.pullRequests[0];
if (pr === undefined) {
return;
}
const octokit = await ctx.github.getOctokit();
await octokit.rest.issues.addLabels({
owner: ctx.repository.owner,
repo: ctx.repository.name,
issue_number: pr.number,
labels: ["ci-failed"],
});
}Events
Declare a hook for each event that should wake the agent: onPullRequest, onComment, onIssue, onCheckSuite, onCheckRun, onWorkflowRun, onStatus, and the catch-all onEvent.
Each hook returns one of three outcomes:
| Return | Use when |
|---|---|
{ auth } | The model should reason about the event. The turn shows up in the playground. |
{ task } | Host code can handle the wake. No chat session. |
null | Skip the event. |
Channels covers the shared session behavior.
Choose watched repositories
The examples above list their repositories in the channel with cursorAccount: { repos: [...] }. A hosted deployment receives events for the repositories set at deploy time, so pass each one to agent-sdk deploy:
text
agent-sdk deploy --dir . --cursor-events-repo acme/checkoutTo keep the list out of source, set cursorAccount: true and pass the repositories on the command line instead. agent-sdk serve takes repeatable --repo flags:
ts
export default githubChannel({
cursorAccount: true,
});text
agent-sdk serve --dir . --cursor-events \
--repo acme/checkout --repo acme/payments
agent-sdk deploy --dir . \
--cursor-events-repo acme/checkout \
--cursor-events-repo acme/paymentsThe --cursor-events-repo flags on a deploy form its complete list and replace the previous one, so pass every repository on each redeploy. A deploy without the flag watches the agent's GitHub source repository instead. Use agent-sdk deployment update to change or clear the list without redeploying.
PR and agent affinity
Every wake for the same repo and PR number resumes the same session. The channel keys turns as github-pr:acme/checkout#42. Opens, comments, and CI results share that conversation.
Call GitHub from deterministic code via ctx
cursorAccount puts one credential on both surfaces:
Channel hooks:
ctx.github.getOctokit(),ctx.thread.post(),ctx.repository, andctx.conversationTools:
ctx.host.github.getOctokit()
ts
onPullRequest: async (ctx, pr) => {
if (pr.action !== "opened") {
return null;
}
const octokit = await ctx.github.getOctokit();
const { data } = await octokit.rest.pulls.get({
owner: ctx.repository.owner,
repo: ctx.repository.name,
pull_number: pr.number,
});
if (data.draft) {
return null;
}
await ctx.thread.post(`Reviewing ${pr.url}`);
return { auth: defaultGitHubAuth(ctx) };
},Re-read the PR here when the hook must decide. Tools use the host client for the same API during the turn.
Batch events
A change to a single request typically fans out multiple events, sometimes hundreds. A push can fire synchronize, several check runs, and comments in a few seconds. By default, individual channel hooks or webhooks do not debounce for you.
Coalesce per PR. Flush the latest wake. Skip a flush when a turn for that PR is already running.
ts
const pending = new Map();
function enqueue(pr, flush) {
const key = `${pr.owner}/${pr.repo}#${pr.number}`;
const existing = pending.get(key);
if (existing !== undefined) {
clearTimeout(existing);
}
pending.set(
key,
setTimeout(() => {
pending.delete(key);
flush();
}, 3_000)
);
}Persist that buffer across restarts and drop the queue when the PR closes.
Test with replay
Use Agent SDK to replay a PR you can read.
text
agent-sdk github replay https://github.com/acme/checkout/pull/42 --dir . --action opened
# Replay everything the channel listens for, with CI failing
agent-sdk github replay acme/checkout#42 --dir . --events '*' --conclusion failure
# Snapshot payloads as fixtures instead of POSTing
agent-sdk github replay acme/checkout#42 --dir . --events '*' --dry-run --fixtures-dir fixtures/githubPractices
Re-read GitHub in the turn or a tool. Do not stuff the event payload into the prompt.
Set
botNameso replies do not re-wake the agent.Watch an explicit repo list (
cursorAccount.repos). Repos under one channel must share one GitHub owner.Cancel leftover work on
pull_request.closed.
Other hosts
The same hooks work on GitHub Enterprise Server. Point the channel at your server and add a repo webhook:
ts
export default githubChannel({
api: {
apiBaseUrl: "https://github.example.com/api/v3",
},
credentials: {
token: { secretName: "GITHUB_TOKEN" },
webhookSecret: { secretName: "GITHUB_WEBHOOK_SECRET" },
},
});Add a repository webhook for https://<your-host>/<slug>/v1/channels/github with the same secret. Event pull and github replay target GitHub.com, so test Enterprise Server with saved fixtures.
Prefer event pull on github.com. Use the channel route directly when you already terminate GitHub webhooks yourself. Set a webhook secret on both sides and name it, as above, rather than reading it: locally the channel reads that environment variable, and on Cursor hosting the platform verifies X-Hub-Signature-256 with the deployment secret of that name at ingestion, so an unsigned or mis-signed delivery gets a 401 back to GitHub before any compute starts. A literal or function value keeps verification inside the handler only. The Webhooks guide covers the shared mechanism.
Name the API credentials the same way. Every credentials field — token, installationToken, appId, privateKey, webhookSecret — takes { secretName: "NAME" }: locally the channel reads that environment variable, and on Cursor hosting the deployment secret of that name. deploy then lists the names the agent expects and warns about any that are not set yet, before the first API call fails inside the guest. architecture: "v2" requires the named form (a numeric appId is configuration and stays allowed); omitting a field keeps its env fallback. architecture: "v1" still accepts literals and () => process.env.X getters.
Related
- Build a PR reviewer: the full read and write loop, one step at a time
- Webhooks and custom channels: the HTTP mechanism under this pack
- Evals: turn replay snapshots into regression fixtures
skills/github/SKILL.md: the companion skill for coding agents