chore(claude): install marketplace plugins via SessionStart hook - #387
Conversation
Declaring `extraKnownMarketplaces` in `.claude/settings.json` is gated on workspace trust for that exact folder, and cloud sessions arrive with `hasTrustDialogAccepted` false, so the declaration added in #386 loads nothing there. Hooks in settings files are not trust-gated, so a SessionStart hook is the reliable install path. Adds `.claude/hooks/install-plugins.sh` and registers it on `SessionStart` for `startup|resume`. The script adds the marketplace if absent, reads the enabled plugin IDs back out of `enabledPlugins`, and installs the missing ones at user scope. It is idempotent and best effort: a failed plugin costs its skills, not the session. `enabledPlugins` is unchanged — all 65 stay enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JibHJ2RtUbxdwpsLF4pG6
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Claude finished @kyle-sexton's task in 7m 4s —— View job Code review complete
Scope note: No FindingsOne portability finding posted inline on I also checked whether the hook's Everything else — the marketplace-add/install logic, idempotency via the Branch: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4e0c0ee750
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Skill discovery runs before SessionStart hooks finish, so plugins the hook installs would otherwise only become available in the next session. Emit `reloadSkills: true` so Claude Code re-scans once the hook completes. Stdout on SessionStart is consumed as hook output, so the human-readable summary moves to stderr, which this event shows to the user only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JibHJ2RtUbxdwpsLF4pG6
|
Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
The hook entry runs `bash <script>`, so the interpreter is whatever
`bash` resolves to on PATH rather than the shebang's. Stock macOS still
ships bash 3.2, which has no `mapfile` — under `set -euo pipefail` the
script aborted before installing anything.
Drop both arrays rather than only `mapfile`: bash before 4.4 also errors
on an empty `"${array[@]}"` under `set -u`, so a read loop into an array
would still fail when nothing is enabled. The wanted and installed sets
are newline-delimited strings matched with a newline-anchored glob, which
keeps suffix collisions such as `review` against `code-review` from
matching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JibHJ2RtUbxdwpsLF4pG6
Summary
#386 declared the
melodic-softwaremarketplace and enabled all 65 plugins in.claude/settings.json. That declaration alone loads nothing in a cloud session.extraKnownMarketplacessupplied by a repository is gated on workspace trust forthat exact folder, and the docs are explicit that no dialog is offered when the
gate is not met — What runs before you trust a folder
lists it as "Not used, and no dialog is offered" / "Not used". Hooks in settings
files sit in the row above it and are used in both untrusted situations.
This adds a
SessionStarthook that installs the enabled catalog, because a hookis the one path that runs untrusted.
enabledPluginsis unchanged. All 65 stay enabled, deliberately.No linked issue
Changes
.claude/hooks/install-plugins.sh(new,100755) — adds the marketplace ifit is not already registered, reads the enabled plugin IDs back out of
enabledPlugins, and installs the missing ones at--scope user. Idempotent andbest effort:
command -vguards onclaudeandjqexit 0, and a failed installis reported to stderr without failing the hook, so a broken plugin costs its
skills rather than the session.
.claude/settings.json— adds ahooks.SessionStartentry matchingstartup|resume. The 13 added lines are the whole diff;extraKnownMarketplacesand all 65
enabledPluginsentries are byte-for-byte unchanged.The script derives the repo root from
CLAUDE_PROJECT_DIRwith aBASH_SOURCEfallback, and finds
claudethroughcommand -v— no machine-specific paths.Installs are user-scoped, so nothing rewrites the repo's tracked settings file.
Same-session availability (
b00122d)Installing is not sufficient on its own.
hooks.md:
"Skill discovery normally runs before SessionStart hooks finish, so files the hook
writes ... would otherwise only appear in the next session." The hook therefore ends
by requesting the documented re-scan:
{"hookSpecificOutput":{"hookEventName":"SessionStart","reloadSkills":true}}Stdout on this event is consumed as hook output, so the human-readable summary goes
to stderr, which
SessionStartshows to the user only.The hook is synchronous — no async mode — which per Anthropic's own
session-start-hookskill "guarantees dependencies are installed before yoursession starts" and lets the container state be cached after the hook completes.
bash 3.2 portability (
9876e80)The hook entry runs
bash <script>, so the interpreter is whateverbashresolvesto on PATH, not the shebang's. Stock macOS still ships bash 3.2.57, which has no
mapfile. The script uses no arrays at all: bash before 4.4 also errors on an empty"${array[@]}"underset -u, and both sets are legitimately empty on a freshcontainer, so a read-loop-into-array would still have aborted.
wantedandhaveare newline-delimited strings compared with a newline-anchored glob — the anchors
matter, since a plain substring test would treat
review@melodic-softwareasinstalled because
code-review@melodic-softwarecontains it, and this catalog shipsboth.
Both post-review changes came from automated review on this PR.
Verification
Run in this cloud session, on this branch.
The problem is real, not theoretical
Before any change, in a session whose repo already carried #386's declaration:
The marketplace was declared in the repo and registered nowhere; zero plugins loaded.
End to end on a simulated cold container
The strongest run. Registration lives in
~/.claude/plugins/known_marketplaces.json,not user
settings.json; that file,installed_plugins.json, and the plugin cachewere all emptied, leaving
marketplace list[],plugin list0, and trustfalse— the exact state a fresh cloud container starts in. Then:Re-running with everything present reports
65 enabled, 0 newly installedin ~1.2s,so it is idempotent.
enabledPluginsmatches the 65 names in the marketplace catalogexactly — no drift in either direction.
The installed state is actually picked up
A fresh
claudeprocess in the same container, after the install, loads 169skills including
source-control:commit— a plugin skill from this marketplace.At this session's start, with the plugins uninstalled, there were roughly 20
built-ins and none from the marketplace.
Lint and schema
shellcheck --rcfile=.shellcheckrc .claude/hooks/install-plugins.sh— clean, onShellCheck 0.11.0, the version
ci.ymlpins and checksums.shfmt -d— no diff. shfmt reads.editorconfig, so this is against the repo's[*.{sh,bash}] indent_size = 2..claude/settings.jsonvalidates againsthttps://json.schemastore.org/claude-code-settings.jsonunder Ajv 8(
allErrors). Note the schemastore URL 301-redirects towww.; fetching withoutfollowing it yields a 176-byte HTML stub, not a schema.
components/comment-hygieneandcomponents/path-detectionpatterns both reportclean against the new files.
What I could not verify
Stated plainly, because the limits are real:
dispatch it on
SessionStart. A session that adds a hook cannot demonstrate thathook running, so the registration itself is only schema- and convention-checked.
reloadSkillscoverage of plugin caches. It is documented against~/.claude/skills/and.claude/skills/. Plugins live under~/.claude/plugins/and are a distinct mechanism whose interactive analogue is/reload-plugins. If the re-scan does not reach them, the fallback is themeasured behavior above — plugins live from the next session or resume onward.
on construction and documented version history, not on a 3.2 run.
folder the marketplace declaration is honored on its own, so the hook should be
a fast no-op there — but I did not run it in that state. Note that Claude Code
reports an external-source plugin as not installed until each user installs it
even when the marketplace is registered
(enabledPlugins), which
is the case for the hook being useful locally too, not only in the cloud.
cloud-environments
claims plugins declared in
.claude/settings.jsonare "Installed at session startfrom the marketplace you declared." The observed state above contradicts that for
this session. I could not determine whether that row is aspirational, applies to a
configuration this environment lacks, or is a bug. The hook makes the outcome hold
either way; if the row is accurate in some environments, the hook is redundant
there rather than harmful, since it no-ops in ~1s once plugins are present.
Cost
A cold container installs 65 plugins in ~90s before the session starts; once present
the hook no-ops in ~1s. All 65 plugins staying enabled is a deliberate choice — the
per-turn context cost is real and will be trimmed separately.
Related
melodic-software/claude-code-plugins🤖 Generated with Claude Code
https://claude.ai/code/session_014JibHJ2RtUbxdwpsLF4pG6