Files
claude-engineering-plugin/docs/solutions/developer-experience/branch-based-plugin-install-and-testing-2026-03-26.md

9.4 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 ./plugins/compound-engineering always points to whatever branch the main checkout is on. 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 path (e.g., claude --plugin-dir /path/to/worktree/plugins/compound-engineering).


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, ./plugins/compound-engineering in the repo root always points to the main checkout's branch, not the 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/plugins/compound-engineering

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 Codex, OpenCode, etc.)

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 codex --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.