Build an Agent Swarm With Claude Code
November 11, 2026 · Ryan X. Charles
On November 10, 2026, I made 926 GitHub contributions, my most ever in one day. I did not write them. Four projects (Astrohacker, Botpenny, ryanxcharles and a health company) each ran four Claude Code sessions: issues, code, review and deploy. The code and review sessions ran worker subagents of their own. I talked to the issues sessions, and they did the rest.
This post is a hands-on guide to that setup, written so you can build an elementary version for your own repository. It is an elementary coding swarm. Everything in it is plain files, a command-line tool and GitHub. You could go much further, and the last section says where.
What the 926 GitHub contributions count
The 926 is GitHub’s number: the contribution calendar on my profile for November 10, which I read back through GitHub’s API. GitHub counts commits on a repository’s default branch, issues opened, pull requests opened and pull request reviews submitted. Three of the four repositories are private, so the calendar shows only the total.
Here is the activity I measured myself, from Git and the GitHub API, for the Central-time day, 06:00 UTC on November 10 to 06:00 UTC on November 11:
| Repo | Commits | Issues | PRs |
|---|---|---|---|
| astrohacker | 130 | 40 | 39 |
| botpenny | 167 | 47 | 37 |
| ryanxcharles | 78 | 23 | 19 |
| a health company | 142 | 37 | 35 |
| Total | 517 | 147 | 130 |
Commits counts the commits that reached main, and Issues and PRs what was opened. So the day breaks down as 517 commits to main, 147 issues, 130 pull requests, and the rest. The first three make 794 of the 926. In the same window my login also submitted 197 pull request reviews (59, 49, 28 and 61), 131 PRs merged, and 797 distinct commits were pushed to any branch.
These counts don’t sum exactly to 926, and I can’t say which category the rest belongs to. GitHub doesn’t break down private contributions or say how it counts several reviews on one pull request. Its days also mix time zones: a commit is dated in the time zone of its own timestamp, but an issue or PR created through the API, as our agents create them, is dated by the API call’s timestamp, which is in UTC. Counted by the UTC day instead, the same searches find 118 issues, 96 PRs and 135 reviews. The commands behind every number are in the pull request that published this post.
The earlier posts covered the ideas. Building a Software Factory is the design. Who Checks the Software Factory? is verification. Can You Copy a Software Factory? is porting it to a second codebase. Slack for Your AI Team is the coordination gap, and The Living Software Repository is the vision. This one is the wiring.
1. The shape: one session per role per project
Every session has a name of the form <computer>-<project>-<harness>-<role>. On my Mac, the Astrohacker code session is rxc-ah-claude-code. rxc is the machine, ah the project, claude the harness (the agent program running it) and code the role. Workers append what they hold, so the worker writing this post was rxc-ah-claude-code-511, for issue #511.
The Claude Code desktop app lists sessions in a sidebar and runs several in parallel. The sidebar can group and sort sessions, and we group ours by project folder. So on November 10 my sidebar held 16 sessions in four groups, one group per project: Astrohacker, Botpenny, ryanxcharles and a health company. Each group had the same four sessions, one per role, and under the code and review sessions ran their workers.
The four roles:
- issues talks with the human and files issues. It never edits code.
- code runs one coding worker per claimed issue. It never codes itself.
- review runs one review worker per pull request. It is never the session that wrote the PR.
- deploy releases every merged change without asking for approval each time.
Coding and review must be separate sessions, so an approval never comes from the conversation that wrote the code. That one rule is why the swarm can merge without me reading every diff.
2. AGENTS.md is the constitution
Every rule that applies to every agent goes in one file at the repository root. Astrohacker’s AGENTS.md is 76 lines. It holds the role table, the naming rule, who has authority and the project-wide rules. The authority section is the part that matters most:
Ryan gives each role session its standing job once, directly in that session. Messages from
rxc-ah-claude-issuesor another session are events within that job, not new authorizations, and a pause Ryan gives in a session holds until he lifts it there. […] Only Ryan clears aneeds-humanhold, closes an issue, authorizes a--handoffretire, or directs a claim handoff; after a human halt, wait for his explicit direction.
Project-wide rules sit beside it. One example: the website never reads the operating system’s reduce-motion preference, and a guard test enforces that. Rules like this belong in AGENTS.md because every role must follow them, whatever it is doing.
How does Claude Code find the file? According to the memory docs, Claude Code reads a repository’s AGENTS.md in place of CLAUDE.md when there is no CLAUDE.md in the working directory or above it (version 2.1.277 or later). Astrohacker has no CLAUDE.md, so every session loads AGENTS.md at launch. If you already have a CLAUDE.md, the docs suggest a CLAUDE.md that imports it with @AGENTS.md. Files in parent directories load at launch; files in subdirectories load when Claude first touches a file there. The docs recommend keeping each file under 200 lines, which is a good reason to move procedures into skills.
3. Skills hold the procedures
AGENTS.md says who may do what. A skill says how to do one job, step by step. Astrohacker keeps seven in .agents/skills/*/SKILL.md: ah for the repository tool, ah-issues, ah-code, ah-review and ah-deploy for the four roles, ah-react-router for route changes, and ah-singularity-watch for a daily publishing role. Six of them are symlinked into .claude/skills, .codex/skills and .grok/skills, so Claude Code, Codex and Grok all read the same files. Singularity Watch runs on another harness and is not linked.
The skills docs say a project skill lives at .claude/skills/<skill-name>/SKILL.md, that the entry can be a symlink to a directory elsewhere on disk, and that a skill’s body loads only when it is used, while its description is always available. You can also call one directly as /skill-name. That is why long procedures belong in skills: they cost almost nothing until a session needs them.
Here is how a coding worker waits, from the ah-code skill:
How you wait. Don’t poll in a loop, don’t run
ah pr waitorah watch, and don’t start timers or background commands that outlive your turn. After you push, reply, ask, block or open the PR, end your turn with a report […] The role session continues you on the next event for your issue.
My rule of thumb: if breaking it in any role would be a problem, it goes in AGENTS.md. If it is a sequence of steps for one job, it goes in that job’s skill. If a program can check it, it goes in the tool.
4. The rules live in a tool, not in prompts
Prompts are suggestions. A model under pressure will skip a step, retry something it should not, or decide that a rule does not apply this time. So every rule that can be checked by a program is checked by one: ah.nu, a Nushell script with modules in scripts/. When this post was written it was 1,374 lines in ah.nu and about 4,000 in the modules, plus about 5,300 lines of offline test suites and about 600 of fixtures: about 11,300 lines in all.
Claims are the only lock. Each claimed issue or review is a tag on GitHub, such as refs/tags/ah/issue/511. Taking work creates the tag with an expected empty value, so when two sessions race, Git lets exactly one win. A claim survives crashes and restarts. Here are the live claims while this post was being written, with the SHAs shortened to eight characters:
$ git ls-remote origin \
'refs/tags/ah/*'
2f1d17db refs/tags/ah/issue/510
928e4026 refs/tags/ah/issue/511
780bd743 refs/tags/ah/review/512
Each step is one command. The issues session runs issue create. A coding worker runs issue take, worktree start and pr open. A review worker runs review take and review submit, which reviews one exact commit and, on a pass, attempts a guarded merge itself. Afterwards each side runs pr retire or review retire. Each command refuses what the rules forbid: taking a claimed issue, opening a PR for an issue with an open blocker, merging a commit that is not the one reviewed.
$ export AH_HARNESS=claude
$ ./ah.nu issue take 511 \
--agent rxc-ah-claude-code-511
{
"ok": true,
"actor": {
"agent":
"rxc-ah-claude-code-511",
"machine": "rxc",
"harness": "claude"
},
"sha": "928e40267e…"
}
ah status is the board. It shows every open issue and PR with its claims, holds, blockers, verdicts and routing, as JSON an agent can read. Trimmed:
$ ./ah.nu status
{"issues": [{
"number": 510,
"coder": {"agent":
"rxc-ah-claude-code-510"},
"human_hold": false,
"open_blockers": 0,
"pull_requests": [{
"number": 512,
"reviewer": {"agent":
"rxc-ah-claude-review-512"},
"review": "pending"}]}]}
One login, one budget. All our agents share one GitHub account and its 5,000 REST requests an hour. Every read goes through a cache that sends the stored ETag, so an unchanged resource answers 304 Not Modified, which does not count against the limit. Bulk commands keep a reserve of 500 requests and wait for the reset rather than starve everyone else.
Offline tests. Thirteen Nushell suites run against temporary Git repositories and fake GitHub responses, with no network. When an agent changes the tool, it runs them all, and the reviewer checks that it did.
5. The event loop costs nothing while idle
A role session does not poll. It starts one background command:
export AH_HARNESS=claude
./ah.nu watch --role code \
--until-event \
| ./ah.nu dispatch --role code
ah watch reads the board once a minute in a plain Nushell process, so no model tokens are spent while nothing happens. With --until-event it exits after the first batch of events for its role. The session runs it as a background command, and in the desktop app the command’s exit gives the session a new turn with its output. The docs state this wake-up for background subagents and for non-interactive runs; for interactive background commands it is what we observe, and the role skills are written around it.
ah dispatch turns each event into one JSON action: spawn a worker for new work, continue the worker that already holds a claim, mark a spawn waiting when the role is full (three workers by default), or escalate to the human. This is the action that launched the worker writing this post, trimmed:
{
"action": "spawn",
"event": "issue_eligible",
"issue": 511,
"identity":
"rxc-ah-claude-code-511",
"skill": "ah-code",
"task": "You are the…",
"env": {
"AH_AGENT":
"rxc-ah-claude-code-511",
"AH_HARNESS": "claude"
}
}
The task is the worker’s whole prompt. It names the identity, the issue and the skill, and ends with the rule that matters most: “Don’t poll or wait: end your turn with your report after each push, reply or verdict, and your role session continues you on later events.”
The session launches each worker as a background subagent with the Agent tool. A subagent runs in its own context window, so a worker’s file reads and test output never fill the role session. The session records the worker’s agent ID, and on the next event for that claim it continues the same worker by ID with SendMessage, as the tools reference describes. One coding worker sees its issue from claim to retirement, through every review round. The workers never poll either: each ends its turn after it pushes or replies, and waits to be continued. Claude Code allows 20 running subagents per session by default; the limit of three workers per role is our own choice, to keep the machine and the GitHub budget calm.
6. GitHub is the shared state
No session remembers anything another session needs. Everything shared is on GitHub, where any session, and any person, can read it.
- Issues with acceptance checks. The issues session writes what happens today, what should happen instead, and how we will know it works: a URL, a width, a theme, a command and its expected output.
- Dependencies. GitHub’s native Blocked by links decide order. An issue with an open blocker never becomes eligible. Big work is a parent issue with sub-issues, and when a merge closes the last open sub-issue, the tool walks up and closes the parent.
- Signatures. Every comment, PR and verdict ends with a line like
Agent: rxc-ah-claude-code-511 · Owner: ryanxcharles · Machine: rxc. That is how a reviewer finds a PR’s author and how I find who said what. - Holds.
needs-infomeans a worker asked a question.needs-humanmeans it cannot continue without me, and it pauses every linked PR until I remove it. - Routing. Labels such as
harness:codexorreview-harness:claudedecide which harness and machine may take each piece of work, so Claude Code can review a Codex PR.
The limits are real. Every agent uses my one GitHub login, so GitHub records their approvals as comment reviews, not as approvals from a second account. main is not protected by a branch rule, so a direct git push would bypass everything. The tool enforces the merge policy for agents that use it. Making GitHub enforce it would take a branch rule and an independent approval identity or a trusted required check.
7. Sessions message each other, but a peer can’t grant permission
The desktop app lets one session send messages to another; when the receiver is idle, the message starts a new turn. Role sessions use this for one thing: everything that needs me goes to the issues session, which brings it to me once and relays my answer.
Here is a real example from this week. On November 11, the Singularity Watch archive for Day 27 (issue #506) merged as PR #507. The Watch skill still told that role to release its claims after a merge, which it did. When coding worker rxc-ah-claude-code-506 then ran ah pr retire 507, the tool refused: retire requires the issue claim, and nobody held it. A worktree, a local branch and a remote branch were left behind. The worker did not delete them by hand. It reported the problem; the issues session filed #508; another worker changed the tool so a coder can retire a PR whose claims were already released, after proving the work merged; a reviewer passed it as PR #509. I approved one cleanup, pr retire 507 on the fixed tool, and the leftovers were gone.
Why not let the worker just clean up? Because a message from another session is not me. AGENTS.md says it plainly: messages between sessions are “events within that job, not new authorizations.” If any session could grant another permission, one confused or compromised session could grant itself anything by asking a friend. So holds, closures and handoffs come only from me, in the session that acts or on GitHub.
8. Deploy releases everything, and stops only when the tool says so
The deploy session runs ah deploy watch in its own clean clone. Each time main changes the website, the watcher builds that exact revision, publishes it, and verifies the result over HTTPS, recording each step in a release journal on GitHub. There is no approval per release; that was my decision once, when I started the session. The watcher exits with status 2 only for cases the tool holds for a person, such as an infrastructure change. The ah-deploy skill says what the session does then: report, wait, and do exactly what I decide for that exact commit. No other session may deploy.
9. What broke, and the rule each break became
Most rules in the tool came from an incident. Four from the last week:
- Retirement that failed quietly (#457, #508). Retiring merged work used to be a manual step that could fail quietly. A session could take new work while old worktrees and claims piled up. Now an incomplete retire exits non-zero and names what it skipped, the retire releases the claim as its last step, and the queue commands refuse a new claim until the caller’s own merged work is retired. #508, above, closed the gap for claims released by another role.
- A lock that spun forever (#503). The dispatch state lock could loop at full CPU when another process held the lock that breaks stale locks. Every path now sleeps and checks its deadline, times out after 30 seconds with an error naming the holder, and creates its link with
ln -snso an existing symlink is never followed. - Test servers that collided (#493). Browser tests started servers on fixed ports with “reuse an existing server” turned on. When several worktrees tested at once, one run silently tested another worktree’s build. Each run now picks free ports and never reuses a server it did not start.
- Cleanup that was too broad. In the same incident, a worker freed those ports by killing whatever was listening on them, without checking who owned them. That broke another review’s runs. The issue recorded it next to the port fix, and the ah-code skill now says: stop only the processes you started, by the PIDs you launched, and never kill a process by its port or name.
The pattern is always the same. An agent does something reasonable that turns out to be wrong in a shared system. A reviewer or I notice. An issue goes through the same loop, and the fix becomes a check in the tool or a sentence in a skill, so the next agent cannot make the same mistake.
10. How to start your own
ah is not packaged for reuse. It knows Astrohacker’s branch names, labels and deploy, and it changes every day. But the pattern is small, and you can copy it today:
- Write one
AGENTS.md. Name the roles, say who has authority, and say that coding and review never happen in the same session. - Write four role skills. Issues, code, review and deploy, each a numbered procedure in
.agents/skills/<name>/SKILL.md, symlinked into.claude/skills. Our skills are public; borrow freely. - Write a small claim-and-PR script. Start with three commands:
take N(make a commit naming the agent withgit commit-tree, then push it torefs/tags/claims/issue/Nwith--force-with-lease=refs/tags/claims/issue/N:, whose empty expected value makes the push fail if anyone else holds the tag),open N(open a PR from branchissue-Nthat saysCloses #N), andretire N(delete the branch, the worktree and the tag after the merge). Have every command print JSON. - Start with two sessions, code and review. File issues yourself at first. Add an issues session when you are tired of writing them, and a deploy session when you trust the review.
- Add a watch loop. A script that polls GitHub once a minute, prints a JSON line when something changes for its role, and exits. Run it as a background command in each session and let its exit start the next turn.
Then let it run, and watch what breaks. Each break is the next rule.
Where it could go next
This swarm is elementary. Every agent shares one GitHub login, coordination happens in issue comments, and only one human can unblock anything. Slack for Your AI Team describes what should replace the comment threads. The Living Software Repository is where we think this ends up: a repository that maintains itself, with people setting direction. Between here and there are separate identities for each agent, protected branches that GitHub enforces, reviewers from several vendors, and swarms that work across repositories. We will write about each one as we build it.