Files
everyinc--compound-engineer…/docs/solutions/developer-experience/branch-based-plugin-install-and-testing.md
T
2026-07-13 12:20:01 +08:00

9.3 KiB

title, date, problem_type, category, component, root_cause, resolution_type, severity, tags, symptoms, root_cause_detail, solution_summary, key_insight, files_changed, verification_steps, related_docs
title date problem_type category component root_cause resolution_type severity tags symptoms root_cause_detail solution_summary key_insight files_changed verification_steps related_docs
Branch-based plugin install and testing for Claude Code plugins 2026-03-26 developer_experience developer-experience development_workflow missing_workflow_step workflow_improvement medium
cli
plugin-install
branch-testing
developer-experience
git-clone
plugin-path
No way to install or test a Claude Code plugin from a specific git branch
install command always cloned the default branch from GitHub
claude --plugin-dir only accepts a local filesystem path with no branch support
Developers had to manually checkout branches to test others' plugin changes
The CLI lacked any mechanism to target a specific git branch when installing or testing plugins. Claude Code's --plugin-dir flag only accepts local paths, and the install command had no --branch option. Added a new plugin-path subcommand that clones a specific branch to a deterministic cache path (~/.cache/compound-engineering/branches/) and outputs it for use with claude --plugin-dir. Also added a --branch flag to the install command for non-Claude targets. Worktree-based development means multiple branches are active simultaneously and the repo root checkout can't serve as a reliable plugin source. A deterministic cache path based on the sanitized branch name enables branch-specific plugin testing without disrupting any checkout, and re-runs update in place via git fetch + reset --hard.
src/commands/plugin-path.ts
src/commands/install.ts
src/index.ts
tests/plugin-path.test.ts
tests/cli.test.ts
Run bun test to confirm all tests pass including 5 new plugin-path tests and 1 new CLI test
Test plugin-path subcommand outputs correct deterministic cache path for a given branch
Test install --branch flag clones from the specified branch for non-Claude targets
Verify re-running plugin-path on same branch updates via fetch+reset rather than re-cloning
docs/solutions/adding-converter-target-providers.md
docs/solutions/plugin-versioning-requirements.md

Problem

The compound-engineering plugin CLI's install command always cloned the default branch from GitHub, and Claude Code's --plugin-dir flag only accepts local filesystem paths. Developers who wanted to test a plugin from a specific git branch had to manually check out that branch in their local repo, disrupting their working tree.

This is especially painful in worktree-based workflows where a single local checkout root can only represent one branch at a time. Two concrete scenarios:

  • Cross-repo: You're working in a different project and want to use a CE branch as your plugin. Without this, you'd have to switch the CE repo's checkout — which is likely WIP on something else.
  • Same-repo: You're working on CE itself — feat/feature-2 in your main checkout, feat/feature-1 in a worktree. You want to test feature-1's plugin while continuing to develop feature-2. The main checkout can't serve both purposes.

Note: the --branch flag works with pushed branches (those available on the remote). For unpushed local worktree branches, developers can point --plugin-dir directly at the worktree root (e.g., claude --plugin-dir /path/to/compound-engineering-plugin-worktree).


Symptoms

  • Running bunx compound-engineering install <plugin> always fetched the default branch regardless of what branch contained the changes under review.
  • claude --plugin-dir required a local path, so there was no way to point it at a remote branch without a manual git clone or git checkout.
  • Developers testing PR branches had to stash or commit their local work, switch branches, test, then switch back -- a disruptive and error-prone workflow.
  • In worktree-based workflows, the repo root checkout always points to one branch, not every worktree branch being developed. Developers working on multiple branches simultaneously had no ergonomic way to install from a specific worktree's branch.
  • No scripting path existed to spin up a branch-specific plugin directory for automated testing.

What Didn't Work

  • Using /tmp/ for cloned branches was rejected because temporary directories are cleared on reboot, forcing a full re-clone every session and losing the fast-update path.
  • Random temp directory names (e.g., mktemp -d) were rejected because they cause directory proliferation and make it impossible to re-run the same command and update in place.
  • Extending claude --plugin-dir itself was not an option -- that flag is owned by Claude Code and only accepts local filesystem paths; the solution had to live in the plugin CLI layer.
  • Symlinking the bundled plugin would not help because the bundled copy is always pinned to the installed CLI version, not an arbitrary remote branch.
  • Naive branch sanitization (replace(/[^a-zA-Z0-9._-]/g, "-")) collapsed distinct branches to the same cache path (e.g., feat/foo-bar and feat-foo/bar both became feat-foo-bar). An escape-then-replace scheme (~~~, /~) was attempted next but was still not injective — feat~~foo and feat~//foo both produced feat~~~~foo. The correct insight was that ~ is illegal in git branch names (git-check-ref-format reserves it for reflog notation), so a simple /~ replacement is injective without any escape step.

Solution

Two complementary features were added:

1. New plugin-path command (for Claude Code)

Clones a branch to a deterministic cache directory and prints the path for use with claude --plugin-dir.

bun run src/index.ts plugin-path compound-engineering --branch feat/new-agents
# Output: claude --plugin-dir ~/.cache/compound-engineering/branches/compound-engineering-feat~new-agents

Key implementation details in src/commands/plugin-path.ts:

  • Cache path: ~/.cache/compound-engineering/branches/<plugin>-<sanitized-branch>/
  • Branch sanitization: /~, then strip remaining non-[a-zA-Z0-9._~-] chars. This is injective because ~ is illegal in git branch names (git-check-ref-format reserves it for reflog notation), so no valid branch input contains ~ and the mapping is 1:1.
  • First run: git clone --depth 1 --branch <name> <source> <dest>
  • Re-run: git fetch origin <branch> + git reset --hard origin/<branch>

2. --branch flag on install command (for converter-backed targets)

Threads a branch name through the full resolution chain so install clones from the specified branch instead of the default.

bun run src/index.ts install compound-engineering --to opencode --branch feat/new-agents

Changes in src/commands/install.ts:

  • When --branch is provided, skips bundled plugin lookup (user explicitly wants a remote version)
  • Threaded through resolvePluginPath -> resolveGitHubPluginPath -> cloneGitHubRepo
  • cloneGitHubRepo conditionally adds --branch <name> to git clone --depth 1

Key difference between the two

plugin-path caches the checkout in ~/.cache/ for reuse across sessions. install --branch uses an ephemeral temp directory that's cleaned up after the install completes -- it only needs the clone long enough to read and convert the plugin.


Why This Works

The root issue was a missing indirection layer: the CLI assumed "install" always means "use the default branch," and Claude Code assumes "plugin directory" always means "a path that already exists locally." The solution bridges that gap by:

  • Deterministic cache paths mean the same branch always maps to the same directory. No proliferation, no ambiguity.
  • Fetch + hard reset on re-run keeps the cached checkout current without requiring a full re-clone, making iteration fast.
  • ~/.cache/ follows XDG conventions, persists across reboots, and is understood by users and tooling as a safe-to-delete cache layer.
  • The COMPOUND_PLUGIN_GITHUB_SOURCE env var works with both features, allowing tests to use local git repos and avoiding network dependency.

Prevention

  • Test coverage: tests/plugin-path.test.ts (6 tests: clone-to-cache, slash sanitization, update-on-rerun, slash-placement collision resistance, nonexistent branch error, nonexistent plugin error) and tests/cli.test.ts (1 test: install --branch clones specific branch). All tests use local git repos via COMPOUND_PLUGIN_GITHUB_SOURCE.
  • Cache directory convention: Any future features that need ephemeral or semi-persistent clones should use ~/.cache/compound-engineering/<purpose>/ with deterministic, sanitized subdirectory names. Avoid /tmp/ for anything that benefits from surviving a reboot.
  • Branch sanitization: Always sanitize branch names before using them in filesystem paths. Using ~ as the slash replacement is injective because ~ is illegal in git branch names (git-check-ref-format). A naive replace(/[^a-zA-Z0-9._-]/g, "-") is insufficient because it collapses branches like feat/foo-bar and feat-foo/bar into the same path.
  • Resolution chain threading: When adding new resolution strategies to the CLI, thread optional parameters through the full resolvePluginPath -> resolveGitHubPluginPath -> cloneGitHubRepo chain rather than branching at the top level. This keeps the resolution logic composable.