Skip to content

chore(claude): install marketplace plugins via SessionStart hook - #387

Merged
kyle-sexton merged 3 commits into
mainfrom
claude/plugin-marketplace-setup-wxabdn
Aug 15, 2026
Merged

chore(claude): install marketplace plugins via SessionStart hook#387
kyle-sexton merged 3 commits into
mainfrom
claude/plugin-marketplace-setup-wxabdn

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

#386 declared the melodic-software marketplace and enabled all 65 plugins in
.claude/settings.json. That declaration alone loads nothing in a cloud session.

extraKnownMarketplaces supplied by a repository is gated on workspace trust for
that 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 SessionStart hook that installs the enabled catalog, because a hook
is the one path that runs untrusted.

enabledPlugins is unchanged. All 65 stay enabled, deliberately.

No linked issue

Changes

  • .claude/hooks/install-plugins.sh (new, 100755) — adds the marketplace if
    it is not already registered, reads the enabled plugin IDs back out of
    enabledPlugins, and installs the missing ones at --scope user. Idempotent and
    best effort: command -v guards on claude and jq exit 0, and a failed install
    is reported to stderr without failing the hook, so a broken plugin costs its
    skills rather than the session.
  • .claude/settings.json — adds a hooks.SessionStart entry matching
    startup|resume. The 13 added lines are the whole diff; extraKnownMarketplaces
    and all 65 enabledPlugins entries are byte-for-byte unchanged.

The script derives the repo root from CLAUDE_PROJECT_DIR with a BASH_SOURCE
fallback, and finds claude through command -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 SessionStart shows to the user only.

The hook is synchronous — no async mode — which per Anthropic's own
session-start-hook skill "guarantees dependencies are installed before your
session 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 whatever bash resolves
to 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[@]}" under set -u, and both sets are legitimately empty on a fresh
container, so a read-loop-into-array would still have aborted. wanted and have
are newline-delimited strings compared with a newline-anchored glob — the anchors
matter, since a plain substring test would treat review@melodic-software as
installed because code-review@melodic-software contains it, and this catalog ships
both.

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:

$ jq '.projects["/home/user/standards"].hasTrustDialogAccepted' ~/.claude.json
false
$ claude plugin marketplace list --json
[]
$ claude plugin list --json
[]
$ cat ~/.claude/plugins/installed_plugins.json
{ "version": 2, "plugins": {} }

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 cache
were all emptied, leaving marketplace list [], plugin list 0, and trust
false — the exact state a fresh cloud container starts in. Then:

$ CLAUDE_PROJECT_DIR=/home/user/standards bash .claude/hooks/install-plugins.sh
install-plugins: 65 enabled, 65 newly installed        # exit 0, ~90s
{"hookSpecificOutput":{"hookEventName":"SessionStart","reloadSkills":true}}

$ claude plugin marketplace list --json | jq -c 'map(.name)'
["melodic-software"]
$ claude plugin list --json | jq 'group_by(.scope)|map({scope:.[0].scope,count:length})'
[{"scope":"user","count":65}]

Re-running with everything present reports 65 enabled, 0 newly installed in ~1.2s,
so it is idempotent. enabledPlugins matches the 65 names in the marketplace catalog
exactly — no drift in either direction.

The installed state is actually picked up

A fresh claude process in the same container, after the install, loads 169
skills 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, on
    ShellCheck 0.11.0, the version ci.yml pins and checksums.
  • shfmt -d — no diff. shfmt reads .editorconfig, so this is against the repo's
    [*.{sh,bash}] indent_size = 2.
  • .claude/settings.json validates against
    https://json.schemastore.org/claude-code-settings.json under Ajv 8
    (allErrors). Note the schemastore URL 301-redirects to www.; fetching without
    following it yields a 176-byte HTML stub, not a schema.
  • LF endings, final newline, no tabs, no trailing whitespace. The repo's own
    components/comment-hygiene and components/path-detection patterns both report
    clean against the new files.

What I could not verify

Stated plainly, because the limits are real:

  • The hook firing. I executed the script directly; I did not observe Claude Code
    dispatch it on SessionStart. A session that adds a hook cannot demonstrate that
    hook running, so the registration itself is only schema- and convention-checked.
  • reloadSkills coverage 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 the
    measured behavior above — plugins live from the next session or resume onward.
  • bash 3.2 itself. This container has only 5.2.21, so the portability fix rests
    on construction and documented version history, not on a 3.2 run.
  • Local (trusted) sessions. I only had this cloud session. In a trusted local
    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.
  • A documentation conflict I did not resolve.
    cloud-environments
    claims plugins declared in .claude/settings.json are "Installed at session start
    from 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

🤖 Generated with Claude Code

https://claude.ai/code/session_014JibHJ2RtUbxdwpsLF4pG6

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
@cursor

cursor Bot commented Aug 15, 2026

Copy link
Copy Markdown

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

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 7m 4s —— View job


Code review complete

  • Gather context (diff, CLAUDE.md, REVIEW.md)
  • Review .claude/hooks/install-plugins.sh
  • Review .claude/settings.json
  • Post findings

Scope note: No claude-security-review workflow exists in this repo yet, so per REVIEW.md this lane also applied the security-scope checks. Nothing to report there — the plugin IDs installed come from the repo's own tracked .claude/settings.json, are properly quoted/array-passed (never string-concatenated) into claude plugin install, and no secret or credential is introduced.

Findings

One portability finding posted inline on .claude/hooks/install-plugins.sh (lines 25–31): the script uses mapfile (bash 4+), but the SessionStart hook in .claude/settings.json invokes it via a bare bash "..." command, which PATH-resolves to bash 3.2.57 on stock macOS (no mapfile builtin) — under set -euo pipefail this aborts the hook before any plugin installs, on any platform/trust-state the startup|resume matcher fires for, not only cloud sessions.

I also checked whether the hook's command needed an explicit "shell": "bash" key (this org's own melodic-software/claude-code-plugins plugin repo documents a related, previously-shipped-three-times Windows bug — #1416/#2570 — where a bare bash/python3 exec-form command resolves to a broken Windows shim). I verified against the current Claude Code docs that this doesn't apply here: this hook is shell form (no args), and shell form already defaults to Git Bash on Windows (falling back to PowerShell only when Git Bash isn't installed) with no explicit shell key needed — so that angle is a non-issue for this diff and isn't reported as a finding.

Everything else — the marketplace-add/install logic, idempotency via the have/wanted diff, --scope user targeting, and the settings.json hook wiring itself — looks correct for its stated purpose (cloud sessions with a modern Linux bash), consistent with the author's own cloud-session verification in the PR description (which I'm treating as author-claimed, not independently re-run here).

Branch: claude/plugin-marketplace-setup-wxabdn

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread .claude/settings.json
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
Comment thread .claude/hooks/install-plugins.sh Outdated
@github-actions

Copy link
Copy Markdown

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
@kyle-sexton
kyle-sexton merged commit f40f304 into main Aug 15, 2026
46 checks passed
@kyle-sexton
kyle-sexton deleted the claude/plugin-marketplace-setup-wxabdn branch August 15, 2026 03:23
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