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 |
|
|
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. |
|
|
|
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-2in your main checkout,feat/feature-1in 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-dirrequired a local path, so there was no way to point it at a remote branch without a manualgit cloneorgit 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-engineeringin 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-diritself 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-barandfeat-foo/barboth becamefeat-foo-bar). An escape-then-replace scheme (~→~~,/→~) was attempted next but was still not injective —feat~~fooandfeat~//fooboth producedfeat~~~~foo. The correct insight was that~is illegal in git branch names (git-check-ref-formatreserves 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-formatreserves 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
--branchis provided, skips bundled plugin lookup (user explicitly wants a remote version) - Threaded through
resolvePluginPath->resolveGitHubPluginPath->cloneGitHubRepo cloneGitHubRepoconditionally adds--branch <name>togit 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_SOURCEenv 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) andtests/cli.test.ts(1 test: install --branch clones specific branch). All tests use local git repos viaCOMPOUND_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 naivereplace(/[^a-zA-Z0-9._-]/g, "-")is insufficient because it collapses branches likefeat/foo-barandfeat-foo/barinto the same path. - Resolution chain threading: When adding new resolution strategies to the CLI, thread optional parameters through the full
resolvePluginPath -> resolveGitHubPluginPath -> cloneGitHubRepochain rather than branching at the top level. This keeps the resolution logic composable.