Build a Model-Agnostic GitHub Coding Agent with the Claude Agent SDK

Build a Model-Agnostic GitHub Coding Agent with the Claude Agent SDK

Agents Claude-agent-sdk Github Llm Developer-tools
Photo of Dale Nguyen
Dale Nguyen
4 min read

We run CodeMagpie — a GitHub App that writes code and reviews PRs when you @mention it. Three agents share one backend:

  • Reviewer@codemagpieai review → inline review comments on a PR.
  • Implementer@codemagpieai create on an issue → opens a fresh-branch PR.
  • Resolver@codemagpieai resolve on a review thread → pushes a fix.

The interesting part isn't the agent loop — the Claude Agent SDK gives you that for free. It's that none of this is locked to a single model. In production our agents don't run on Claude at all — and the swap is one environment variable. This post shows the architecture and the one trick that makes it model-agnostic.

The shape of an "online" agent

A coding agent that lives on GitHub is not a chat loop. It's a webhook handler with a durable job queue in front of an agent runtime. The interactive diagram below walks through each stage — step through it to see how a single @mention travels from GitHub to a merged PR.

Request lifecycle diagram: GitHub webhook → queue → agent → git. Enable JavaScript to view.

Why the queue? Webhooks must return 200 in seconds, but an agent run takes minutes. You ack the webhook immediately, persist the run, and let a worker pull the job. The queue also gives you retries, a dead-letter path, and a natural place to enforce per-user quota before you spend a single token.

The agent core

This is where the Claude Agent SDK earns its keep. Instead of hand-rolling the tool-use loop — call the model, parse tool_use blocks, execute, feed results back, repeat until done — you describe the tools and let the SDK drive. The Implementer, reduced to its essentials:

import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const fsTools = createSdkMcpServer({
  name: "workspace",
  tools: [
    tool("write_file", "Create or overwrite a file in the repo", {
      path: z.string(),
      content: z.string(),
    }, async ({ path, content }) => {
      await writeFileInWorkspace(path, content);
      return { content: [{ type: "text", text: `wrote ${path}` }] };
    }),
    // read_file, list_dir, run_tests ...
  ],
});

async function handleCreate(issue: Issue, repoDir: string) {
  const run = query({
    prompt: `Implement this issue, then stop:\n\n${issue.title}\n\n${issue.body}`,
    options: {
      model: process.env.ANTHROPIC_MODEL,   // ← not pinned to Claude
      cwd: repoDir,
      mcpServers: { workspace: fsTools },
      maxTurns: 40,                          // hard cap so a confused run can't burn budget
      systemPrompt:
        "You are a senior engineer. Make the smallest change that resolves the " +
        "issue. Write files, run tests, then summarize what you did.",
    },
  });

  for await (const msg of run) {
    if (msg.type === "result") return msg; // final summary → PR body
  }
}

The SDK handles the loop, the tool-result plumbing, and context management. You bring the tools (a sandboxed workspace, git, the GitHub API) and the stop condition. After the run finishes you open a PR from a fresh codemagpie/ branch — never push to the user's branch from the Implementer.

Here's that division of labor — the parts the SDK owns, the parts you supply, and what happens once the run ends:

Ownership diagram: what the Claude Agent SDK handles vs. what you provide vs. what happens after the run. Enable JavaScript to view.

The model-agnostic trick

Here's the whole secret. The Claude Agent SDK — like the underlying Anthropic SDK — talks to an HTTP endpoint defined by ANTHROPIC_BASE_URL. Point that at any Anthropic-compatible gateway and the SDK neither knows nor cares what's behind it.

In production we point it at a third-party endpoint that speaks the Anthropic Messages API:

# Claude (default) — nothing to set
ANTHROPIC_API_KEY=sk-ant-...

# Any Anthropic-compatible provider, same code path
ANTHROPIC_BASE_URL=https://<anthropic-compatible-host>
ANTHROPIC_AUTH_TOKEN=$PROVIDER_API_KEY
ANTHROPIC_MODEL=<provider-model-id>

Our actual Python client is four lines — and the TS Agent SDK reads the same two env vars:

anthropic.Anthropic(
    base_url=os.environ["ANTHROPIC_BASE_URL"],  # provider host
    api_key=os.environ["PROVIDER_API_KEY"],
)

Centralize this in one module so every agent resolves its model the same way and they can't drift apart. Swapping the whole fleet to a new model — Claude, a hosted open model, or a self-hosted model behind an Anthropic-compatible proxy like LiteLLM — is a config change and a redeploy, not a code change. That's the payoff: you pick the model per environment, benchmark candidates against the same agent, and never rewrite the loop.

One caveat worth stating plainly: "Anthropic-compatible" has to actually mean it. Tool-use semantics and streaming behavior vary between providers. Keep an eval set of real issues and re-run it whenever you change the backing model — a model swap can silently change how often the agent writes files versus spins on read-only exploration.

The guardrails that matter once it's public

A coding agent reachable from the internet needs more than a good prompt:

  • Verify the webhook. HMAC-check x-hub-signature-256 and reject anything that fails. Without this, anyone can trigger runs.
  • Dedupe deliveries. GitHub redelivers webhooks. Key each run by delivery id so a retry doesn't open two PRs.
  • Quota per user. Enforce a rate limit (we use 5 runs/hour/uid) before dispatch, not inside the agent.
  • Cap the loop. maxTurns plus a nudge to start writing past the halfway point stops a confused run from exhausting its budget on a no-op.
  • Isolate the workspace. Each run gets a fresh clone in a throwaway directory, and the Implementer only ever pushes to its own branch.

Takeaways

The agent loop is a solved problem — the Claude Agent SDK gives you tool-use, context management, and a clean stop condition out of the box. The two decisions that actually shape your system are upstream (a webhook + queue so runs survive and stay rate-limited) and sideways (ANTHROPIC_BASE_URL, so the model is a deployment detail, not an architectural one). Build those two right and "which model" becomes the easiest thing to change.

Get new posts in your inbox

No spam — just new posts and learning pages when they ship.