How we version-control our subagents (and why we started) | Claude Code Toolkit
Deep dives

How we version-control our subagents (and why we started)

For six months we treated subagents like config files: commit them to git, done. Then a "small tweak" broke twelve engineers' workflows silently for a week. This is the version control discipline we adopted after that, and the specific incident that convinced us it was worth the process.

MK
Mateo K. September 23, 2026 · 10 min read

For the first six months of our subagent library, our version control was: commit the file, ship. That worked well enough — subagents lived in the repo, changes went through PR review, if something broke we could revert. Standard stuff. Then someone made a "small tweak" to our @pr-reviewer subagent and silently broke twelve engineers' workflows for a week before anyone realized what happened.

The tweak was innocuous-looking. The output format changed slightly — a "Blocking issues" section that had been a bulleted list became a numbered list. Downstream automation that parsed the output relied on the bulleted format. That automation quietly started failing. Nobody noticed because failures weren't loud — they just meant certain fixes never got applied. A week later, a bug that would have been caught by pr-reviewer's finding shipped to production. Post-mortem tracing it back to the subagent format change was painful.

This post is the version control discipline that emerged from that incident. Not heavy process for its own sake — the specific things we do that would have prevented that specific incident, and the incidents we've had since.

Why "just commit them" isn't enough

The gap between "subagents are in git" and "we can safely change subagents" is bigger than it looks. Three specific gaps we hit:

Gap 1: No stable contract

Traditional versioned software has a contract — a public API that consumers depend on. Changes to the contract are visible; internal changes aren't. Subagents blur this: any part of the subagent's behavior might be depended on by some consumer. Output format, exact wording of certain phrases, whether certain issues get flagged — all potentially part of the "contract."

Without an explicit contract, every change is potentially breaking. Consumers can't know what to depend on; authors can't know what they're allowed to change.

Gap 2: No compatibility signals

In traditional software, semantic versioning tells consumers whether a version change is safe. Patch: safe. Minor: safe if you weren't depending on internals. Major: might break. Consumers can consume with appropriate caution.

With subagents in git without version numbers, consumers have no signal. Every change is opaque. Consumers don't know whether the update is a bug fix or a rewrite. They just get whatever's at HEAD.

Gap 3: No deprecation window

In traditional software, deprecated APIs stay working while consumers migrate. Subagents in "just commit them" mode don't: when a subagent changes, everyone gets the new version immediately. Consumers can't migrate at their own pace; they get surprised by changes.

For subagents with automated consumers (scripts, CI hooks, other subagents), this "immediate cutover" pattern breaks things regularly. Which is exactly what happened to us.

The mechanics we adopted

Five practices, in rough order of importance:

Practice 1: Explicit contract per subagent

Every subagent has a "contract" section in its definition that specifies what consumers can rely on. Output format, invocation interface, expected behavior categories.

markdown
---
name: pr-reviewer
version: 2.1.0
---

## Contract

Consumers of this subagent can rely on:
- Output is Markdown
- Sections in this order: Summary, Blocking Issues, Suggestions, Questions
- "Blocking Issues" section always present (may be empty)
- Blocking issues formatted as: `- **[Type]**: description`
- Every finding has a severity: BLOCKING / SUGGESTION / QUESTION

Consumers should NOT rely on:
- Specific wording of the Summary
- Exact number of findings
- Ordering within a section (sorted by internal heuristics)
- Length of any specific finding

Breaking changes to the contract require major version bump.

The contract makes explicit what was previously implicit. Consumers know what's safe to depend on; authors know what they can't change without a major version bump.

Practice 2: Semantic versioning

Each subagent has a version number in its frontmatter. Standard SemVer: MAJOR.MINOR.PATCH.

  • PATCH: fixes to behavior that shouldn't affect the contract. Prompt clarification that produces same outputs; typo fix; example update.
  • MINOR: new capabilities that don't break existing consumers. Adding a new field to output; supporting a new input variant.
  • MAJOR: contract breaks. Output format change; removal of a field; behavior change affecting existing consumers.

Version numbers get bumped in the same commit as the change. PR reviewers check that version increments match the change type — a contract-breaking change with only a patch bump gets rejected.

Practice 3: Test gates

Each subagent has a test suite (see the subagent library post). Changes to the subagent must pass the tests before merge. This catches obvious regressions.

Beyond correctness tests, we added contract tests: specific tests that verify the contract holds. "Output has a Blocking Issues section" is a contract test. If a change removes that section, the contract test fails, and the change either gets a major version bump or gets modified to preserve the contract.

Practice 4: Deprecation windows

When a major version ships, the previous major version stays available for 30 days. Consumers pin to a specific version if they need stability; the pinned version keeps working while consumers migrate.

The workflow: @pr-reviewer defaults to the latest version. Consumers who need stability pin: @pr-reviewer@2 gets the latest 2.x. When we ship 3.0, both are available; consumers can migrate at their own pace over 30 days. After the deprecation window, we remove 2.x.

This is the single practice that would have prevented our original incident. The pr-reviewer format change would have been a major bump; downstream automation would have pinned to @pr-reviewer@1 until they migrated; no silent breakage.

Practice 5: Changelog + release notes

Every version change gets a changelog entry. For major changes, a short release note goes to the team channel: "pr-reviewer 3.0 shipping this week. Change: output format is now JSON instead of Markdown. Deprecation: 2.x stays available until Oct 15. Migration guide: [link]."

This makes changes visible. Consumers who might be affected have a chance to react before things break. The "surprise" element of silent updates goes away.

How this looks in practice

A concrete example: recently we changed @commit-message-writer from producing single-line commit messages to producing full commit messages with body paragraphs.

The workflow that followed:

  1. Contract review: the change would break consumers depending on single-line output. Major version bump required. Current version 1.4.0 → new version 2.0.0.
  2. Implementation PR: updated the subagent, updated the contract section to specify the new output format, bumped version to 2.0.0. Contract tests updated to reflect new format.
  3. Announcement: team channel: "commit-message-writer 2.0 shipping Monday. New: outputs full commit messages with body. Migration: consumers can pin to @commit-message-writer@1 for 30 days if they need single-line."
  4. Ship: merged Monday morning. Both 1.x and 2.x available.
  5. Monitoring: tracked usage of pinned vs unpinned invocations over the deprecation window. Most consumers moved to 2.x within a week. Two automations pinned to 1.x needed to be updated.
  6. Sunset: after 30 days, removed 1.x. Consumers still on 1.x had been warned via changelog and release notes; they had time to migrate.

That's more process than "edit the file and commit." It's also the process that prevented any consumers from being broken by a substantive change. The extra work is bounded (an hour or two total across the whole change) and prevents an unbounded downside (mysterious breakage).

The version pinning mechanism

The version-pinning syntax we adopted: @name@version. Examples:

  • @pr-reviewer — latest version
  • @pr-reviewer@2 — latest 2.x version
  • @pr-reviewer@2.1 — latest 2.1.x version
  • @pr-reviewer@2.1.4 — exact version

Consumers pick their level of stability. Interactive human users mostly use unpinned (they can adapt); automated consumers pin to major or minor for stability. The mechanism gives everyone what they need.

The pin-in-CI pattern

For subagents invoked from CI/CD pipelines, always pin to at least the major version. CI failures caused by unexpected subagent behavior changes are the worst kind of debugging session — nothing in your PR should have caused this. Explicit pinning eliminates that class of incident.

What we don't do (and why)

Not everything from software versioning applies. Things we tried and dropped:

Continuous versioning per commit. Bumping the patch version on every commit sounded thorough but produced version-number churn without benefit. Now we bump only when the subagent's behavior actually changes.

Long deprecation windows. Tried 90 days initially; nobody used the extra time. 30 days is enough — consumers who need to migrate do so within a couple weeks; the last two weeks are buffer.

Backport patches to old versions. Tried maintaining bug fixes on both old and new majors during deprecation windows. The maintenance burden wasn't worth it. Now: bug fixes go to latest only; consumers pinned to old versions accept that they won't get fixes and should migrate.

The measurable impact

Six months after adopting these practices:

  • Subagent-related incidents: zero. Down from ~1 per month before the discipline.
  • Time to deploy subagent changes: about the same. The extra process is bounded; most changes still ship within a day.
  • Consumer confidence: much higher. Engineers automate against subagents more freely because they know the contract is stable.
  • Library velocity: slightly slower for major changes, faster for minor ones. Major changes take more care (correctly). Minor changes ship confidently because the tests catch regressions.

The trade-off is exactly what you'd hope for: incidents drop dramatically; velocity for legitimate changes stays similar or improves; the cost is process overhead on the small fraction of changes that are actually breaking.

When you need this

The full discipline pays off when: multiple engineers depend on shared subagents, automated consumers exist (scripts, CI, other subagents), and subagent breakage would cause real problems.

The discipline is overkill for: personal subagent libraries (one user, informal), throwaway experimental subagents, or teams so small everyone can be notified about every change in real time.

The transition point for us was around 15 engineers using shared subagents with any automation involved. Below that, "just commit them" worked fine. Above that, incidents started happening; the discipline paid off. Your threshold might differ, but the shape is similar: as consumers scale up, ad-hoc versioning breaks down; formal versioning becomes worth the cost.

Subagents look like config files. They're actually more like APIs — they have consumers who depend on their behavior. Treat them accordingly and incidents go away.

The starting point

If you have shared subagents without version control, the highest-leverage first step is adding an explicit contract section to each subagent. Just writing down "here's what consumers can rely on" surfaces where consumers might be depending on things you didn't know were part of the contract.

From there, add version numbers, then test gates, then deprecation windows. Each layer adds a little more discipline; each layer catches a class of incidents that the previous layers missed. You don't need to do all of it at once. But the contract section is the entry point — everything else flows from having explicit contracts.

MK

Written by

Mateo K.

Mateo focuses on platform engineering, agentic workflows, and turning experimental patterns into infrastructure that scales.

Get cctk running in one command

85 slash commands, 12 subagents, 12 MCP integrations, 12 hooks. All the patterns from this post are shipped in cctk.

npx cctk@latest init Get cctk v1.5.2 →

Share with