Skip to content

fix: install Claude plugin via marketplace + installer hardening#206

Merged
illeatmyhat merged 15 commits into
mainfrom
fix/claude-plugin-marketplace-install
Apr 21, 2026
Merged

fix: install Claude plugin via marketplace + installer hardening#206
illeatmyhat merged 15 commits into
mainfrom
fix/claude-plugin-marketplace-install

Conversation

@illeatmyhat

@illeatmyhat illeatmyhat commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Original fix:

  • claude plugin install <path> fails because the CLI requires the plugin to exist in a configured marketplace
  • Updated Claude installer to run claude plugin marketplace add (local .claude-plugin/marketplace.json if present, otherwise AgentToolkit/altk-evolve on GitHub), then plugin install evolve-lite@evolve-marketplace
  • Fail fast when marketplace add fails rather than running a doomed plugin install
  • When claude CLI is not found, tell the user to install it from claude.ai/download and re-run — instead of showing claude plugin ... commands they can't run yet

Installer refactor:

  • Split install logic into BobInstaller, ClaudeInstaller, CodexInstaller classes backed by FileOps/DryRunFileOps — dry-run behavior is inherited automatically, no if DRY_RUN sprinkled throughout
  • Source download is lazy: the bash wrapper no longer fetches the repo tarball at startup; _ensure_source_dir() downloads on demand and only when Bob/Codex actually need source files. Claude installs need no download at all.
  • Bob skill/command installation iterates the source skills/ directory dynamically; uninstall globs evolve-lite:* from the target — adding a new skill requires no script changes
  • Fixed marketplace_dir detection to probe SOURCE_DIR (the repo root) instead of target_dir (the user's project)
  • Replaced shell=True pipe for the tarball download with Popen to avoid fragility on paths with spaces

Tests:

  • Added remote_install_runner fixture: copies install.sh to an isolated dir with no platform-integrations/ sibling, simulating curl | bash
  • Added test_dry_run.py covering local and remote dry-run for all platforms — verifies no files are written and exit code is 0 even with no local source tree
  • Added test_claude.py covering CLI-absent fallback behaviour
  • _assert_all_bob_skills_installed / _assert_all_bob_commands_installed consolidated into FileAssertions in conftest.py

Test plan

  • install --platform all --dry-run from repo root (local source)
  • install --platform all --dry-run from isolated dir (remote simulation, no source tree)
  • install --platform claude from repo root (local marketplace path used)
  • All 42 platform integration tests pass
  • install --platform claude from a remote context (GitHub marketplace used) — covered by TestDryRunRemote
  • When claude CLI is absent, user is told to install it and re-run — covered by test_cli_absent_prompts_download

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Bash wrapper exports EVOLVE_REPO/EVOLVE_VERSION and runs embedded Python (now unbuffered). Python lazily downloads/extracts source when needed, introduces FileOps and per-platform installer classes, changes Claude to marketplace add+install, and updates dry-run and dispatch behavior.

Changes

Cohort / File(s) Summary
Install script & installer logic
platform-integrations/install.sh
Bash exports EVOLVE_REPO/EVOLVE_VERSION, invokes python3 -u. Embedded Python refactor: FileOps/DryRunFileOps, BobInstaller/ClaudeInstaller/CodexInstaller, DRY_RUN from args, lazy _ensure_source_dir() (downloads/extracts on-demand), and updated install/uninstall/status dispatch.
Claude marketplace flow
platform-integrations/install.sh
Replace previous Claude plugin install with claude plugin marketplace add <source> then claude plugin install evolve-lite@evolve-marketplace; <source> is target_dir/.claude-plugin/marketplace.json local manifest or EVOLVE_REPO; uninstall/status adjusted; dry-run output updated.
Bob/Codex behavior changes
platform-integrations/install.sh
Bob/Codex installers call _ensure_source_dir() for lazy source resolution; installer classes route filesystem writes through FileOps / dry-run variant.
Tests — fixtures & dry-run coverage
tests/platform_integrations/conftest.py, tests/platform_integrations/test_dry_run.py
Added remote_install_script and remote_install_runner fixtures; new test_dry_run.py verifies --dry-run across platforms (local + remote simulation), asserting exit code 0, DRY RUN output, and no filesystem changes.
Tests — idempotency & preservation helpers
tests/platform_integrations/test_idempotency.py, tests/platform_integrations/test_preservation.py
Use pathlib.Path repo-root constants and helpers to assert all evolve-lite skills/commands installed (replacing hard-coded skill/command assertions).

Sequence Diagram(s)

sequenceDiagram
  participant Bash as install.sh (wrapper)
  participant Py as embedded Python
  participant CurlTar as Downloader (`curl`/`tar`)
  participant ClaudeCLI as claude CLI

  Bash->>Py: invoke installer (env: EVOLVE_REPO/EVOLVE_VERSION)
  Py->>Py: parse args (DRY_RUN), choose platform installer
  Py->>Py: _ensure_source_dir()
  alt no local source
    Py->>CurlTar: download/extract evolve tarball to temp dir
    CurlTar-->>Py: extracted SOURCE_DIR (cleanup via atexit)
  end
  Py->>Py: instantiate ClaudeInstaller / BobInstaller / CodexInstaller
  alt Claude install path
    Py->>ClaudeCLI: claude plugin marketplace add <local-or-EVOLVE_REPO>
    ClaudeCLI-->>Py: ack
    Py->>ClaudeCLI: claude plugin install evolve-lite@evolve-marketplace
    ClaudeCLI-->>Py: install result
  else Bob/Codex install path
    Py->>Py: write files via FileOps (or DryRunFileOps)
  end
  Py-->>Bash: print outcomes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • vinodmut
  • gaodan-fang
  • visahak

Poem

🐇 I hopped a temp dir, nosed out the repo,
I fetch when asked and tidy up my toe,
I add the market, whisper “evolve-lite” loud,
Dry-run nibble shows no prints on the cloud,
Hooray — tidy installs make this rabbit proud.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 84.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately describes the main changes: fixing Claude plugin installation via marketplace and improving installer robustness through class-based refactoring and FileOps abstraction.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/claude-plugin-marketplace-install

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platform-integrations/install.sh`:
- Around line 745-747: The code uses EVOLVE_REPO when computing
marketplace_source but EVOLVE_REPO is not defined, causing a NameError when
has_local_marketplace is False; define EVOLVE_REPO in the Python scope before
that expression (e.g., a constant string URL or environment-derived value) so
marketplace_source = str(marketplace_dir) if has_local_marketplace else
EVOLVE_REPO can safely fall back to the remote repo; update or add the
EVOLVE_REPO declaration near where target_dir/marketplace_dir is resolved
(referencing marketplace_dir, has_local_marketplace, and marketplace_source) and
ensure any imports or env reads used to populate EVOLVE_REPO run earlier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ad1798c-08da-4540-850e-35760758ef11

📥 Commits

Reviewing files that changed from the base of the PR and between 034de13 and 6566d91.

📒 Files selected for processing (1)
  • platform-integrations/install.sh

Comment thread platform-integrations/install.sh Outdated
@visahak

visahak commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

@illeatmyhat can you address the comments?

@illeatmyhat

Copy link
Copy Markdown
Collaborator Author

yeah im trying a feature in claude to see if it can autofix it

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
platform-integrations/install.sh (1)

744-745: Info log is now slightly misleading.

plugin_source is no longer used to install the plugin — installation is driven by the marketplace entry — but the log still says Installing Claude plugin from {plugin_source}. Consider dropping the variable or clarifying the message (e.g., "Preparing Claude plugin install (marketplace flow)") so users aren't led to believe the install is sourced from that local path.

✏️ Proposed tweak
-def install_claude(source_dir, target_dir):
-    plugin_source = Path(source_dir) / "platform-integrations" / "claude" / "plugins" / "evolve-lite"
-    info(f"Installing Claude plugin from {plugin_source}")
+def install_claude(source_dir, target_dir):
+    info("Installing Claude plugin via marketplace")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platform-integrations/install.sh` around lines 744 - 745, The info log is
misleading because plugin_source (the variable assigned to Path(source_dir) /
"platform-integrations" / "claude" / "plugins" / "evolve-lite") is no longer
used for installation; update the code to either remove the unused plugin_source
variable or change the info(...) call to a clarifying message such as "Preparing
Claude plugin install (marketplace flow)" so it no longer implies installation
is coming from that local path; locate the assignment to plugin_source and the
info(...) invocation and apply one of these two fixes consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@platform-integrations/install.sh`:
- Around line 744-745: The info log is misleading because plugin_source (the
variable assigned to Path(source_dir) / "platform-integrations" / "claude" /
"plugins" / "evolve-lite") is no longer used for installation; update the code
to either remove the unused plugin_source variable or change the info(...) call
to a clarifying message such as "Preparing Claude plugin install (marketplace
flow)" so it no longer implies installation is coming from that local path;
locate the assignment to plugin_source and the info(...) invocation and apply
one of these two fixes consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5e6cec3-e28f-4669-84a8-b183da64cbc6

📥 Commits

Reviewing files that changed from the base of the PR and between 6566d91 and b3b83c8.

📒 Files selected for processing (1)
  • platform-integrations/install.sh

illeatmyhat and others added 3 commits April 20, 2026 15:40
The `claude plugin install <path>` command requires the plugin to exist
in a configured marketplace. This updates install_claude to first register
the marketplace (local if .claude-plugin/marketplace.json exists, otherwise
AgentToolkit/altk-evolve on GitHub), then install evolve-lite from it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e fallback

Export EVOLVE_REPO from bash and read it via os.environ in the Python heredoc
so marketplace_source can fall back to the remote repo without a NameError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@illeatmyhat illeatmyhat force-pushed the fix/claude-plugin-marketplace-install branch from 7894e95 to bfcff05 Compare April 20, 2026 22:40
Claude installs via the marketplace CLI and never use SOURCE_DIR, so
downloading the repo tarball upfront was unnecessary. Move the download
logic into a Python-side _ensure_source_dir() called lazily at the top
of install_bob and install_codex, so a Claude-only remote install
triggers no network fetch at all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
platform-integrations/install.sh (1)

165-168: Avoid shell=True with interpolated URL.

url is built from the EVOLVE_REPO / EVOLVE_VERSION env vars and then spliced into a shell string. These are normally user-controlled, so this isn't an exploitable issue, but it is fragile (spaces, quotes, or a stray ; in the env would break or misbehave). A small refactor using a pipe between two subprocess.Popen calls or curl | tar via shell=False with explicit argv would be more robust.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platform-integrations/install.sh` around lines 165 - 168, The subprocess.run
call that executes f"curl -fsSL {url} | tar -xz -C {_tmpdir_download}
--strip-components=1" should be rewritten to avoid shell=True and string
interpolation of url; instead spawn curl and tar as separate processes and pipe
curl's stdout into tar (use subprocess.Popen for curl with stdout=PIPE and
subprocess.run or Popen for tar reading from that pipe), or call both commands
with shell=False as argument lists (e.g., ["curl","-fsSL", url] piped into
["tar","-xz","-C", _tmpdir_download,"--strip-components=1"]); update the code
around subprocess.run, url, and _tmpdir_download to use the piped processes and
remove shell=True to make execution robust against spaces or malicious
characters in EVOLVE_REPO/EVOLVE_VERSION.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platform-integrations/install.sh`:
- Around line 160-163: The current logic treats EVOLVE_VERSION="latest" as an
alias for main, which fetches the branch instead of the newest release; update
the handling of EVOLVE_VERSION in the branch/tag selection (the conditional that
sets url using EVOLVE_VERSION and EVOLVE_REPO) so that when EVOLVE_VERSION ==
"latest" you resolve the latest published release via the GitHub Releases API
(GET https://api.github.com/repos/{EVOLVE_REPO}/releases/latest), extract the
release tag_name and use that in the tag URL, with a safe fallback to the main
branch or a clear error if the API call fails; ensure EVOLVE_VERSION and
EVOLVE_REPO are referenced exactly as in the diff and preserve the existing
branch/tag URL formats.
- Around line 770-779: The local-marketplace detection in install_claude
currently checks Path(target_dir) instead of the Evolve source checkout; update
the probe to use source_dir so has_local_marketplace =
(Path(source_dir).resolve() / ".claude-plugin" / "marketplace.json").is_file().
Adjust marketplace_dir/marketplace_source logic to use the resolved source_dir
when local marketplace exists (still falling back to EVOLVE_REPO otherwise) and
keep the existing info() messages and variable names (install_claude,
has_local_marketplace, marketplace_source) intact.

---

Nitpick comments:
In `@platform-integrations/install.sh`:
- Around line 165-168: The subprocess.run call that executes f"curl -fsSL {url}
| tar -xz -C {_tmpdir_download} --strip-components=1" should be rewritten to
avoid shell=True and string interpolation of url; instead spawn curl and tar as
separate processes and pipe curl's stdout into tar (use subprocess.Popen for
curl with stdout=PIPE and subprocess.run or Popen for tar reading from that
pipe), or call both commands with shell=False as argument lists (e.g.,
["curl","-fsSL", url] piped into ["tar","-xz","-C",
_tmpdir_download,"--strip-components=1"]); update the code around
subprocess.run, url, and _tmpdir_download to use the piped processes and remove
shell=True to make execution robust against spaces or malicious characters in
EVOLVE_REPO/EVOLVE_VERSION.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4493cec-7e67-4bbd-93ac-6d549c0f6b4a

📥 Commits

Reviewing files that changed from the base of the PR and between b3b83c8 and 5f57387.

📒 Files selected for processing (1)
  • platform-integrations/install.sh

Comment thread platform-integrations/install.sh
Comment thread platform-integrations/install.sh Outdated
illeatmyhat and others added 7 commits April 20, 2026 23:15
- Introduce FileOps class holding all write primitives (atomic_write_json,
  atomic_write_text, copy_tree, remove_dir, remove_file, run_subprocess)
  and higher-level JSON/YAML helpers as methods that compose them
- DryRunFileOps subclass overrides only the primitives with dryrun() logs,
  so every helper and installer automatically gets dry-run behaviour for
  free — no more if DRY_RUN: sprinkled through installer logic
- BobInstaller, ClaudeInstaller, CodexInstaller each receive an ops
  instance; install/uninstall/status are proper methods, Codex-specific
  hook/marketplace helpers are private static methods on CodexInstaller
- Dispatch instantiates DryRunFileOps or FileOps once based on DRY_RUN,
  set a single time in main() via getattr(args, "dry_run", False)
- PLATFORM_CLASSES dict replaces the per-platform if/elif chain
- sys.exit() inside installers replaced with RuntimeError so the
  dispatcher can catch, report all failures, and exit once at the end

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
capture_output=True captured the spinner/progress \r sequences,
causing garbled output when we printed them with an indentation prefix.
Streaming directly lets the terminal handle the cursor movement cleanly.
returncode is still checked; stdout/stderr scraping removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard source dir is_dir() checks with `not self.ops.is_dry_run` so a
  remote dry-run (where _ensure_source_dir skips the download) doesn't
  raise RuntimeError on missing local paths — DryRunFileOps.copy_tree
  already handles missing source gracefully with a (source not found)
  note
- Add -u to the python3 invocation so stdout is unbuffered; previously
  when piped, block-buffered stdout caused error() messages (stderr,
  unbuffered) to appear before info()/success() output in the terminal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
merge_yaml_custom_mode reads the source YAML file before writing, so
it must be overridden in DryRunFileOps rather than relying on the
atomic_write_text no-op. Similarly, Bob full mode reads mcp.json to
extract the server config before calling upsert_json_key — skip the
read and pass a placeholder in dry-run mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…emote tests

- install.sh: iterate source skills/ dir instead of hardcoding each skill name;
  uninstall globs evolve-lite:* from target instead of hardcoding removals;
  status also derived from installed globs
- Tests now derive expected skills from the source tree rather than naming them,
  so adding a skill requires no test or script edits
- Add remote_install_runner fixture to conftest: copies install.sh to an isolated
  dir with no platform-integrations/ sibling, simulating curl | bash
- Add test_dry_run.py covering local and remote dry-run for all platforms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
platform-integrations/install.sh (2)

162-163: ⚠️ Potential issue | 🟡 Minor

EVOLVE_VERSION=latest resolves to main, not the newest release tag.

Users typically interpret "latest" as the newest published release; silently tracking main will surprise anyone pinning EVOLVE_VERSION=latest for stability. Resolve via the GitHub Releases API, or drop the alias and document main explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platform-integrations/install.sh` around lines 162 - 163, The code treats
EVOLVE_VERSION=="latest" as an alias for "main" which returns the branch instead
of the newest release; update the logic around EVOLVE_VERSION, EVOLVE_REPO and
the url construction so that when EVOLVE_VERSION == "latest" you query the
GitHub Releases API for the repository's latest release tag (e.g., GET
/repos/{EVOLVE_REPO}/releases/latest), use that release tag to build the
download URL instead of using "main", and fall back to the current branch URL
only if the API call fails; alternatively, remove the "latest" alias and
document that EVOLVE_VERSION must be a branch or a tag, ensuring the url
variable is set from the resolved tag when constructing the download URL.

584-586: ⚠️ Potential issue | 🟠 Major

Local marketplace probe still uses target_dir, not the Evolve source tree.

marketplace.json lives in the Evolve source checkout, not the user's project. Probing Path(target_dir) means the local path is only picked up when cwd == SOURCE_DIR; running with --dir /some/other/project from a local checkout will silently fall back to GitHub even though a fresher local source is available. Also, ClaudeInstaller no longer receives source_dir — consider threading SOURCE_DIR (the resolved source) through and probing there.

🐛 Proposed fix
-    def install(self, target_dir):
+    def install(self, target_dir):
         info("Installing Claude plugin via marketplace")
-
-        marketplace_dir = Path(target_dir).resolve()
-        has_local_marketplace = (marketplace_dir / ".claude-plugin" / "marketplace.json").is_file()
-        marketplace_source = str(marketplace_dir) if has_local_marketplace else EVOLVE_REPO
+        source_dir = SOURCE_DIR
+        has_local_marketplace = bool(source_dir) and (
+            Path(source_dir) / ".claude-plugin" / "marketplace.json"
+        ).is_file()
+        marketplace_source = (
+            str(Path(source_dir).resolve()) if has_local_marketplace else EVOLVE_REPO
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platform-integrations/install.sh` around lines 584 - 586, The local
marketplace probe currently constructs marketplace_dir from target_dir; instead
use the resolved Evolve source tree (SOURCE_DIR) and thread that through
ClaudeInstaller (or add a source_dir parameter) so the probe checks
(Path(SOURCE_DIR).resolve() / ".claude-plugin" / "marketplace.json").is_file();
if found set marketplace_source to the resolved SOURCE_DIR string, otherwise
fall back to EVOLVE_REPO. Update the code that instantiates ClaudeInstaller to
accept and pass the resolved SOURCE_DIR and replace references to
marketplace_dir/has_local_marketplace/marketplace_source to use the new
source_dir-based path resolution to ensure local check works regardless of cwd.
🧹 Nitpick comments (2)
tests/platform_integrations/test_preservation.py (1)

14-28: Consider moving _assert_all_bob_* helpers to conftest.py.

These two helpers are duplicated verbatim in test_idempotency.py. Promoting them to conftest.py (or extending FileAssertions / BobFixtures) avoids drift if the source-layout contract changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/platform_integrations/test_preservation.py` around lines 14 - 28, Move
the duplicate helper functions _assert_all_bob_skills_installed and
_assert_all_bob_commands_installed out of
tests/platform_integrations/test_preservation.py into the shared test helper
(conftest.py) or attach them as methods on an existing fixture/object (e.g.,
FileAssertions or BobFixtures) so both test_preservation.py and
test_idempotency.py can import/use the same implementations; update both test
modules to import the helpers from conftest (or call the new
FileAssertions/BobFixtures methods) and remove the duplicated definitions from
each test file to avoid drift.
platform-integrations/install.sh (1)

601-603: Consider aborting when marketplace add fails.

If claude plugin marketplace add returns non-zero (e.g., unreachable GitHub source, invalid path), the subsequent plugin install evolve-lite@evolve-marketplace will almost certainly also fail — but only the second failure prints manual-fallback instructions. Fail fast here (print manual commands and return) so users see a single actionable message instead of two consecutive warnings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platform-integrations/install.sh` around lines 601 - 603, The marketplace add
step currently only warns on failure; change it to fail fast by detecting
non-zero result from self.ops.run_subprocess([claude, "plugin", "marketplace",
"add", marketplace_source]) and then print the same manual-fallback instructions
that are printed for the later install failure and return/exit immediately
(non-zero) so the script does not continue to attempt self.ops.run_subprocess to
install "plugin install evolve-lite@evolve-marketplace". Ensure you reference
the failing call (self.ops.run_subprocess for "plugin marketplace add"), use the
existing warn/log method to emit the manual commands, and stop further execution
(return or exit) when result.returncode != 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platform-integrations/install.sh`:
- Around line 162-172: The current download uses shell=True with an f-string
including EVOLVE_REPO and EVOLVE_VERSION which allows shell injection; change to
a safe two-step subprocess call that avoids shell=True: run curl with
subprocess.run([...], stdout=subprocess.PIPE) and pipe its stdout into a
separate subprocess.run([...]) invocation of tar (passing _tmpdir_download and
--strip-components=1 as list args), check return codes or use check=True and
raise on failure (replace the current result variable logic). Additionally
validate EVOLVE_REPO and EVOLVE_VERSION with a strict regex (e.g.,
alphanumerics, dot, underscore, hyphen, slash) before building the URL to
further harden inputs.
- Around line 619-632: The uninstall path uses an unqualified plugin name
causing mismatch with install: update the CLAUDE_PLUGIN constant so it is
marketplace-qualified (e.g., "evolve-lite@evolve-marketplace") and ensure the
uninstall flow in uninstall(...) continues to pass that qualified CLAUDE_PLUGIN
to self.ops.run_subprocess([claude, "plugin", "uninstall", CLAUDE_PLUGIN]) so
the same identifier used by install(...) is used for uninstalling; verify any
references to CLAUDE_PLUGIN (and the install function) now match the qualified
value.

---

Duplicate comments:
In `@platform-integrations/install.sh`:
- Around line 162-163: The code treats EVOLVE_VERSION=="latest" as an alias for
"main" which returns the branch instead of the newest release; update the logic
around EVOLVE_VERSION, EVOLVE_REPO and the url construction so that when
EVOLVE_VERSION == "latest" you query the GitHub Releases API for the
repository's latest release tag (e.g., GET
/repos/{EVOLVE_REPO}/releases/latest), use that release tag to build the
download URL instead of using "main", and fall back to the current branch URL
only if the API call fails; alternatively, remove the "latest" alias and
document that EVOLVE_VERSION must be a branch or a tag, ensuring the url
variable is set from the resolved tag when constructing the download URL.
- Around line 584-586: The local marketplace probe currently constructs
marketplace_dir from target_dir; instead use the resolved Evolve source tree
(SOURCE_DIR) and thread that through ClaudeInstaller (or add a source_dir
parameter) so the probe checks (Path(SOURCE_DIR).resolve() / ".claude-plugin" /
"marketplace.json").is_file(); if found set marketplace_source to the resolved
SOURCE_DIR string, otherwise fall back to EVOLVE_REPO. Update the code that
instantiates ClaudeInstaller to accept and pass the resolved SOURCE_DIR and
replace references to marketplace_dir/has_local_marketplace/marketplace_source
to use the new source_dir-based path resolution to ensure local check works
regardless of cwd.

---

Nitpick comments:
In `@platform-integrations/install.sh`:
- Around line 601-603: The marketplace add step currently only warns on failure;
change it to fail fast by detecting non-zero result from
self.ops.run_subprocess([claude, "plugin", "marketplace", "add",
marketplace_source]) and then print the same manual-fallback instructions that
are printed for the later install failure and return/exit immediately (non-zero)
so the script does not continue to attempt self.ops.run_subprocess to install
"plugin install evolve-lite@evolve-marketplace". Ensure you reference the
failing call (self.ops.run_subprocess for "plugin marketplace add"), use the
existing warn/log method to emit the manual commands, and stop further execution
(return or exit) when result.returncode != 0.

In `@tests/platform_integrations/test_preservation.py`:
- Around line 14-28: Move the duplicate helper functions
_assert_all_bob_skills_installed and _assert_all_bob_commands_installed out of
tests/platform_integrations/test_preservation.py into the shared test helper
(conftest.py) or attach them as methods on an existing fixture/object (e.g.,
FileAssertions or BobFixtures) so both test_preservation.py and
test_idempotency.py can import/use the same implementations; update both test
modules to import the helpers from conftest (or call the new
FileAssertions/BobFixtures methods) and remove the duplicated definitions from
each test file to avoid drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 27d015e0-7c81-4f8b-bc44-cf3c9a0a18fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5f57387 and e6f62b6.

📒 Files selected for processing (5)
  • platform-integrations/install.sh
  • tests/platform_integrations/conftest.py
  • tests/platform_integrations/test_dry_run.py
  • tests/platform_integrations/test_idempotency.py
  • tests/platform_integrations/test_preservation.py

Comment thread platform-integrations/install.sh
Comment thread platform-integrations/install.sh
illeatmyhat and others added 2 commits April 21, 2026 00:23
- Replace shell=True pipe in _ensure_source_dir with Popen so URL values
  can't break execution if they contain spaces or special characters
- Fix marketplace local-source detection to check SOURCE_DIR (the repo root)
  rather than target_dir (the user's project dir), so local dev runs correctly
  find .claude-plugin/marketplace.json and fall back to EVOLVE_REPO remotely

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fail fast in ClaudeInstaller when 'marketplace add' fails instead of
  continuing to run 'plugin install' and getting a second confusing failure
- Move _assert_all_bob_skills/commands_installed into FileAssertions in
  conftest.py so they're shared rather than duplicated across test files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@illeatmyhat illeatmyhat changed the title fix: install Claude plugin via marketplace instead of direct path fix: install Claude plugin via marketplace + installer hardening Apr 21, 2026
illeatmyhat and others added 2 commits April 21, 2026 00:37
…ote context

- Verify fallback manual instructions include both required commands
- Verify local context uses local marketplace source
- Verify remote context (no source tree) falls back to GitHub repo as source
- Verify missing claude CLI exits 0 rather than failing the install

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mands

When the claude CLI is not on PATH, the previous fallback showed
'claude plugin marketplace add ...' instructions — which the user can't
run without claude. Now we tell them to install it from claude.ai/download
and re-run the script.

Remove tests that asserted which marketplace source appeared in that message,
since the source detail is irrelevant when the CLI isn't installed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@illeatmyhat illeatmyhat merged commit 436cbdd into main Apr 21, 2026
16 checks passed
@illeatmyhat illeatmyhat deleted the fix/claude-plugin-marketplace-install branch April 21, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants