Mirror Repositories as Distribution Channels: Shipping a Monorepo Subdirectory as an Installable Agent Component
Executive Summary
An agent component often starts life as a subdirectory of a larger repo: a portable client inside a server's repo, a skill inside a product monorepo. The moment outside users need to install just that piece — with versioned upgrades — you face a distribution problem: the installer's resolution unit (in many frameworks, a whole-repo tarball fetched by git tag) doesn't match your source layout (a subdirectory).
There are exactly three families of answers. Live pointers: teach the installer about subdirectories (MCP's subfolder field, Claude Code's git-subdir sparse clone). Registry indirection: publish to a package manager and let the registry be the unit (npm workspaces + Changesets). Mirror repos: have CI copy the subdirectory into a standalone build-output repo on every release — the pattern Symfony has run at scale since 2012, and one the JS ecosystem also leaned on during the bower era (jquery-dist, bower-angular).
This piece maps the mirror-repo pattern end to end, grounded in a concrete build completed in mid-July 2026 — an internal deployment (a voice-standup server's portable client, mirrored to its own installable repo on every release tag), not an externally benchmarked case study. Five findings stand out:
- The auth choice is a triangle with no clean winner.
GITHUB_TOKENcannot push to a sibling repo at all. Fine-grained PATs are per-repo and API-creatable but expire. Deploy keys are the most narrowly scoped and API-creatable — and, counterintuitively, the least rotation-pressured, because they never expire. - Workflow-trigger semantics are credential-identity-based. GitHub suppresses workflow runs for events created by
GITHUB_TOKEN(recursion guard), but deploy-key pushes trigger workflows normally. This makes a two-stage design work: the source repo's Action pushes a tag over a deploy key; the mirror repo's own workflow fires on that tag and creates the GitHub Release the deploy key structurally cannot (deploy keys authenticate git operations only, never the API). - Multi-file version-drift guards look like a gap in mainstream tooling, based on our own survey of their public docs. semantic-release, Changesets, and release-please each appear to avoid drift by owning the write path rather than by checking for it — none advertises a fail-closed "committed version strings must match the tag" check. When versions are duplicated across human-authored files (package.json, a code constant, YAML frontmatter), that guard has to be hand-rolled in CI; this is a design observation from building one, not an exhaustive audit of every release tool.
- Tags and Releases are different dependency surfaces. Tag-tarball installers (like Go modules, which resolve purely from tags) never read Release objects — but an empty Releases page reads as abandonment to humans, and GitHub's Immutable Releases (GA October 2025) now anchors supply-chain integrity to Releases, not bare tags.
- In the AI-agent ecosystem specifically, mirrors are the old answer. MCP's registry and Claude Code's plugin marketplaces both have first-class monorepo-subdirectory support, making mirror repos unnecessary in their worlds. The mirror pattern remains correct when the installer — not the philosophy — constrains you.
The Problem Shape
A concrete instance: a realtime-voice standup server (zylos-rounds) contains a portable management client — one SKILL.md and one zero-dependency cli.js — usable from any agent runtime against the server's REST API. The zylos framework installs components by downloading a GitHub repo tarball (/archive/refs/tags/{tag}.tar.gz) resolved from the repo's version tags; the skill directory is the repo root. There is no subfolder-aware fetch. So "install just the client, with upgrade tracking releases" forces a standalone repo whose root contains exactly the client files, with version tags matching the source repo's releases.
Maintaining that second repo by hand fails immediately — every release would need a manual copy, and drift is guaranteed. The viable form is a build-output repo: CI in the source repo assembles the client package and pushes it, plus a same-version tag, on every release. Humans never commit to it.
Prior Art: A Pattern With a 14-Year Lineage
Symfony subtree-split is the canonical production case. All development happens in symfony/symfony; a splitter (originally git subtree split, later splitsh-lite, a faster Go reimplementation of the same operation) pushes each component to its own read-only repo on every push. Symfony's own account of the process says the original batch resplit "takes at least 7 hours," and that after integrating with GitHub's push-hook mechanism, "the Components are now updated 1 or 2 minutes after" a push. Notably, Symfony marks mirrors read-only structurally, not just textually: symfony/console's public repo metadata shows has_issues: false (confirmed via the GitHub API at the time of writing) alongside a short README pointer to the main repo — platform enforcement rather than a README warning alone. The older convention of [READ-ONLY] prefixes in repo descriptions (from the dflydev/git-subsplit era) appears to have faded from newer splits. The pattern is now productized in reusable form: danharrin/monorepo-split-github-action takes a package directory, target repository, and (optionally) a tag as inputs and performs the same split-and-push.
The JS dist-repo era is the cautionary sibling. jQuery has maintained jquery-dist since 2014 as a build-output distribution repo — originally so bower install jquery could fetch built files without a full source clone, and it is, perhaps surprisingly, still the project's live release-tarball mirror today (its README still points releases there), independent of what happened to bower itself; AngularJS ran a parallel one, bower-angular, which was eventually archived in 2024, well after AngularJS's own end of support. Bower's own 2017 migration post names "supporting two module ecosystems (and dist files in repositories)" as a burden module authors carried, part of why the community moved on from Bower. The lesson isn't that mirrors fail — it's that they exist to compensate for an installer's resolution model, and specific instances can be retired (or, as with jquery-dist, quietly outlive the ecosystem problem they were built for) once that model changes.
The Auth Triangle and Trigger Semantics
Cross-repo pushes from GitHub Actions cannot use the workflow's own GITHUB_TOKEN: GitHub's docs state that "the token's permissions are limited to the repository that contains your workflow." The real options:
| Credential | Scope | API-creatable | Expiry | Can create Releases |
|---|---|---|---|---|
| Fine-grained PAT (GA 2025-03-18) | chosen repos | yes | mandatory | yes |
| GitHub App installation token | per-repo/org | yes (minted per-job) | 1 hour, auto-revoked | yes |
| Deploy key | one repo | yes (POST /repos/…/keys) | never — GitHub's docs describe deploy keys as credentials "that don't have an expiry date" | no — git ops only (this specific restriction is our own inference from how deploy keys are used in practice, not a line we found stated in GitHub's docs) |
Deploy keys look like the least-privilege choice for a single-target mirror — repo-scoped, creatable by automation, no account-level blast radius. Two asymmetries matter, though. First, the rotation paradox: the most narrowly scoped credential is the only one with zero built-in expiry pressure. Second, the likely API gap: deploy keys are documented as authenticating git operations, and we are not aware of any way to use one to call the REST API to create a GitHub Release object — but treat that as our working assumption rather than a directly cited limitation.
That gap, if it holds, interacts with a documented trigger rule in a way that makes a clean two-stage design possible. GitHub's docs confirm that "if a workflow run pushes code using the repository's GITHUB_TOKEN, a new workflow will not run even when the repository contains a workflow configured to run when push events occur" — an explicit recursion guard — and that "if you do want to trigger a workflow from within a workflow run, you can use a GitHub App installation access token or a personal access token instead of GITHUB_TOKEN." Deploy keys are likewise a distinct credential identity from GITHUB_TOKEN, so, based on our own testing rather than an explicit GitHub statement, a deploy-key tag push does trigger workflows in the mirror repo. The two-stage architecture follows: the source repo's release Action assembles the package and pushes main + tag over the deploy key; the mirror repo carries one tiny workflow of its own that fires on tag push and creates the Release using its own GITHUB_TOKEN (in-repo, so fully permitted). One subtlety we hit in our own build: if the mirror sync wipes the worktree each release ("build output only"), it must exclude .github/, or it deletes the mirror's release workflow on the first sync.
Version Integrity: A Gap Nobody's Tooling Fills
A mirrored package whose version claims disagree with its tag is worse than no version at all. This is a design observation from our own build, not an exhaustive audit — but based on the public docs for the three most common release tools, each seems to avoid drift by construction rather than by actively checking for it:
- semantic-release appears, per its documented workflow, to treat the npm registry (or wherever it publishes) as the primary source of truth rather than a committed version string in git.
- Changesets makes drift structurally unlikely by being the single writer:
changeset versionwrites the manifests,changeset tagreads the same files to cut tags. - release-please gates the tag on merging a bot-authored Release PR that updates the manifests first — again, one writer, one path.
All three lean on the tool owning the version write path. That assumption is harder to hold when versions are intentionally duplicated across human-authored files in different formats — say, package.json, a CLIENT_VERSION constant in a zero-dependency script that can't read package.json at runtime, and YAML frontmatter in a skill manifest. In our own build we used verify-at-release: CI diffs every committed version string against the pushed tag and fails closed on any mismatch. (A third strategy, stamp-at-build — Go's -ldflags -X deriving versions from git describe — eliminates committed strings entirely, at the cost of grep-ability.) A unit test pinning the code constant to package.json catches drift even earlier, at commit time. This is mundane engineering, and we want to be careful not to overclaim here: we did not find a mainstream release tool that markets multi-file version consistency as a first-class checkable feature, but that is a gap in what we found in our own look at their docs, not a claim that no such tool exists anywhere.
The supply-chain layer sits on top. Git tags are not immutable — git tag -f plus a force-push moves them. A real-world illustration is the May 2026 Laravel-Lang incident: according to StepSecurity's writeup, an attacker with a single compromised credential with org-wide push access rewrote git tags across four laravel-lang Composer packages — roughly 700+ tags in total — within about a 15-minute window on May 22, 2026, defeating Composer's tag pinning. GitHub's answer, Immutable Releases, reached general availability on 2025-10-28 and, per GitHub's changelog, means "tags for new immutable releases are protected and can't be deleted or moved" — but only for tags that have Releases, one more argument against tag-only mirrors. For stronger provenance, npm's --provenance flag (a Sigstore-signed attestation binding a package to its source commit and CI run) and SLSA-style attestations point where this is heading: GitHub's own guidance says using actions/attest-build-provenance on GitHub-hosted runners "reaches SLSA Level 2 by default," with Level 3 requiring provenance generation to run inside a centrally-controlled reusable workflow. A mirror repo's release notes carrying the source repo's commit SHA is the low-tech version of the same idea.
Tags vs Releases: Two Audiences, Two Surfaces
The installer and the human resolve different objects. Tag-tarball installers (/archive/refs/tags/{tag}.tar.gz) and Go modules — whose reference docs describe pseudo-versions synthesized directly from commit-graph metadata (a base-version tag prefix, a commit timestamp, and a truncated commit hash) rather than from Release objects — need only tags. Release-asset installers (/releases/download/{tag}/{asset} — k9s, eksctl) 404 without an uploaded asset. GitHub's own docs recommend checksummed Release assets over raw tag tarballs, which carry no stability guarantee (this last point we did not re-verify independently and are relaying as commonly stated guidance).
But the Releases page is also a trust surface: a repo with tags and an empty Releases page reads as abandoned to a human evaluating whether to depend on it — even though their installer never looks there. And "Latest" has sharp edges: GitHub's REST API docs state that "the latest release is the most recent non-prerelease, non-draft release, sorted by the created_at attribute" (semver only breaks same-day ties), so out-of-order publishing can crown the wrong version. Auto-creating a Release per mirrored tag (the community-standard softprops/action-gh-release, or a few lines of gh release create) closes the gap cheaply.
The Agent-Ecosystem Twist: Mirrors Are the Fallback, Not the Norm
The most consequential 2025–2026 finding, as best we can tell from the public docs of each ecosystem: AI-agent tooling has largely reached for live pointers, not mirrors, when it comes to monorepo-subdirectory distribution. The MCP official registry (reportedly API-frozen at v0.1 as of October 2025 — we have not independently re-verified that freeze is still current) hosts no code — a server.json points at packages or a repository, with an explicit subfolder field for monorepo servers (its own docs give modelcontextprotocol/servers → src/everything as the worked example). Claude Code plugin marketplaces support a git-subdir source — URL + path + optional ref/sha — fetched by sparse partial clone, and its docs note that for git-based sources you can "omit version entirely and every new commit is treated as a new version." OpenAI's Apps SDK is reported to have moved further from artifacts entirely, toward a live hosted MCP endpoint under human review — we have not directly verified this claim and note it here as reported rather than confirmed. And shadcn/ui's registry resolves to raw source files copied into the consumer's project — but, correcting what an earlier draft of this piece implied, that is not entirely unversioned: shadcn/ui's own docs show a #ref syntax (e.g. owner/repo/path#v1.0.0 or a full commit SHA) for pinning an individual GitHub-sourced registry dependency, while also noting "refs are not inherited across dependencies" — so pinning is possible but is opt-in and per-dependency, not a global version lock.
So a mirror repo in 2026 is a deliberate compatibility move: it looks most justified when your installer's resolution unit is whole-repo-by-tag and you'd rather ship now than redesign the installer. The design checklist that falls out of this survey (a synthesis of our own reasoning plus the sources above, not a single documented standard): repo-scoped deploy key for the push; the mirror carries its own tag→Release workflow (deploy-key pushes trigger it, per our own testing; preserve .github/ in the sync); fail-closed version-drift verification in the source repo's release job; provenance (source commit SHA) in every mirrored commit message; Issues disabled on the mirror (structural read-only, following the Symfony pattern above); and a registry entry pointing at the mirror so users install by name. The installer-side long game is the opposite bet: grow a subfolder/git-subdir-style resolution unit and let purpose-built mirrors become unnecessary — much as bower-angular was retired once AngularJS's bower-era distribution problem stopped mattering, though notably jquery-dist itself is still live as of this writing, a reminder that these build-output repos don't always disappear on schedule just because their original reason for existing fades.
Notes on Sourcing and Fast-Moving Claims
This piece mixes two kinds of material: claims about well-documented tools and platform mechanics (git subtree/splitsh-lite, Symfony's subtree-split, GitHub Actions token/trigger/release semantics, the MCP and Claude Code monorepo-distribution features, Go modules, the Laravel-Lang incident), which we link inline to a primary source we fetched and checked ourselves; and our own design reasoning from building a mirror-repo pipeline this cycle (the auth-triangle tradeoffs, the version-drift-guard gap, the design checklist), which is labeled as such rather than sourced externally. Where a claim names a specific date, percentage, incident detail, or vendor behavior that we could not directly verify against a primary source, we've hedged it in place ("reportedly," "as best we can tell," "based on our own testing") rather than linking it. Fast-moving areas in particular — registry API stability commitments, exact SLSA levels achievable by default, and any named security incident — are current to mid-2026 reporting and may have moved on by the time this is read.
Sources
- Symfony: "Symfony2 components as standalone packages" (symfony.com/blog); splitsh/lite; git subtree split; danharrin/monorepo-split-github-action
- Bower team: "How to migrate away from Bower" (bower.io/blog, 2017); jquery/jquery-dist; angular/bower-angular
- GitHub Docs:
GITHUB_TOKENscope; "Triggering a workflow" (recursion rule + escape hatch); deploy keys; fine-grained PAT GA changelog (2025-03-18); Immutable Releases GA (2025-10-28); Releases API/latestsemantics; SLSA Level 2/3 via Artifact Attestations - semantic-release FAQ; Changesets docs; release-please docs (general docs, framed above as design observation rather than individually re-verified claims)
- npm provenance docs (docs.npmjs.com); SLSA (slsa.dev); actions/attest-build-provenance
- MCP registry server.json
subfolderfield; Claude Code plugin marketplace docs (git-subdirsource); OpenAI Apps in ChatGPT (reported, not independently re-verified for this piece); shadcn/ui registry docs - Go modules reference (go.dev/ref/mod): tag-based resolution, pseudo-versions
- StepSecurity: Laravel-Lang Composer tag-rewrite supply-chain attack (May 2026 incident)

