Skip to content

feat(llm-sync): scan marketplace-installed Claude Code plugins - #49

Merged
tstapler merged 9 commits into
masterfrom
backlog/dotfiles-sync-marketplace-installed-plugins
Aug 12, 2026
Merged

feat(llm-sync): scan marketplace-installed Claude Code plugins#49
tstapler merged 9 commits into
masterfrom
backlog/dotfiles-sync-marketplace-installed-plugins

Conversation

@tstapler

Copy link
Copy Markdown
Owner

Summary

PluginSource only scanned two flat plugin roots (./plugins, ~/.config/llm-sync/plugins) and missed plugins installed via /plugin install from a marketplace, which live under ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/. Any skill/command/agent shipped as a real marketplace plugin silently never reached Antigravity or OpenCode. Closes #36.

What Changed

  • PluginSource._find_marketplace_plugins() resolves installPath entries from ~/.claude/plugins/installed_plugins.json, filters out plugins disabled via enabledPlugins in settings.json/settings.local.json (disabled-in-either wins), dedupes multi-scope entries to one dir per plugin (prefers "user" scope), and skips stale installPaths without aborting the scan.
  • Marketplace plugins merge with lowest priority in load_plugins(), so dotfiles-committed local/global plugins keep winning on a name collision.
  • cli.py's sync_plugins now routes discovered marketplace plugins through the same Claude/Antigravity global installers used for dotfiles-global plugins — previously they'd have loaded into memory but never actually installed anywhere.
  • Adds src/sources/test_plugins.py, an assert-based self-check (no test framework existed in this package) covering enabled/disabled filtering, missing/malformed installed_plugins.json, multi-scope dedup, stale-path skip, and local-overrides-marketplace precedence.

Note: this branch also carries a chore: add backlog session scaffolding commit (.backlog-context.md, .claude/commands/backlog/) added by the automated triage harness to satisfy its own review-gate checks. It's not part of the feature and isn't tracked on master — safe to squash out or drop before merge.

Test plan

  • uv run python src/sources/test_plugins.py (from stapler-scripts/llm-sync) → "8 checks passed"
  • python -m py_compile on cli.py, plugins.py, test_plugins.py → clean
  • Verified against this machine's real ~/.claude/plugins/installed_plugins.json / settings.json (read-only): kotlin-lsp@claude-plugins-official (disabled) is excluded from load_plugins() output today
  • Verified the enable/disable toggle end-to-end using a structural replica of the real files (same keys/shape, temp installPath with a manifest): enabledPlugins=False[], enabledPlugins=True['kotlin-lsp']
  • Verified cli.py's sync_plugins actually invokes the installers (not just loads) via a --dry-run harness showing "Would write command/skill" output for a synthetic marketplace plugin

🤖 Generated with Claude Code

tstapler and others added 9 commits August 9, 2026 23:15
…ysis

Audits project_plans/<project>/ output volume against a git-derived
shipped-code signal, to spot SDD sessions that produced heavy plan
docs without matching implementation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
project_plans/ audit (sdd-plan-audit.py) showed heavy planning output
relative to what shipped. Root cause: phases 3 (plan) and 4 (validate)
had no complexity gating at all -- every project got the full template,
3 reviewer subagents, and 5-iteration repair loops regardless of size.
The one existing gate (research phase) defaulted to max output (all 6
agents) when the Complexity field was missing, which was most of the
project history (5/8 projects predate the current requirements.md
template).

- 2-research: fix missing-Complexity default from "all 6 agents" to
  Complexity-2 behavior (3 agents + conditional extras)
- 3-plan: add step 2.5 calibrating Domain Glossary/Migration Plan
  elaboration and which of the 3 reviewer subagents run, keyed on
  Complexity; UX subagent now writes a condensed entry for
  non-interactive surfaces (config/log/CLI output) instead of the full
  wireframe treatment
- 4-validate: add step 2.5 calibrating which of the 3 validation
  subagents run and whether the triad review gate applies
- full: stop hardcoding "6 agents" / "three reviewers" / "three
  subagents" in the orchestrator -- defer to each phase's calibration
  step instead, since full.md's own stated principle is to never
  duplicate phase logic. Also suggest /sdd:quick when Phase 1 derives
  Complexity 1, since quick.md already has no template/artifacts/
  subagents and is the existing antidote to this problem -- it was just
  structurally disconnected from full.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
History showed a flip-flop: df7b881 consolidated all ADRs to repo-root
docs/adr/, then 4b5aa9e reverted sdd:adr specifically back to
project_plans/<project>/decisions/ a week later, leaving two ADR
skills with different write targets and no stated reason. Keeping the
split (it's the right call -- sdd:adr decisions are internal to one
SDD project's implementation, plan:adr decisions are cross-cutting
dotfiles-repo infra) but documenting it in both skills so it reads as
intentional next time, not drift.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
Studied obra/superpowers (an external Claude Code skills library) --
its subagent-driven-development/SKILL.md never inlines a subagent's
full prompt in the orchestrating file; it links to sibling files
(implementer-prompt.md, task-reviewer-prompt.md) read only when that
step actually dispatches. Anthropic's own guidance (cited in
superpowers' writing-skills/anthropic-best-practices.md) says the same:
keep SKILL.md under 500 lines, split into separate files as you
approach it.

Our 3-plan and 4-validate were within the 500-line cap but had 25-35%
of their body as inline subagent-prompt blockquotes -- prompt text for
reviewers the complexity gate (added earlier this session) might not
even dispatch. Extracted each into its own file:

- 3-plan: planning-prompt.md, architecture-review-prompt.md,
  adversarial-review-prompt.md, ux-design-prompt.md
  (342 -> 128 lines, -63%)
- 4-validate: validation-prompt.md, pre-mortem-prompt.md,
  cross-artifact-consistency-prompt.md (230 -> 112 lines, -51%)

2-research's agent prompts (3-8 lines each) were left inline --
splitting those would add indirection without saving anything.

Also fixed two hardcoded counts the complexity gate broke: 3-plan's
repair loops still said "MAX = 5" regardless of the calibrated cap
(2/3/5) from step 2.5, and 4-validate's step 4 said "wait for all
three subagents" when Complexity 1 dispatches only one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
Ran a lean-agent-loop audit (9 parallel agents, one per SDD phase
directory) against Anthropic's current prompt-audit methodology
(claude-api skill's shared/prompt-audit.md, current as of 2026-06-24) --
target model: current-generation Claude used by Claude Code subagents
(Sonnet 5 / Opus 5 tier). 21 findings across 8 files, applied 19:

- Stacked/absolutist emphasis booster patterns removed: "## HARD GATE",
  "CRITICAL:", "Iron Law:", ALL-CAPS mid-sentence shouting (NO CODE
  EXISTS YET, FRESH SESSION, ALL IN A SINGLE PARALLEL MESSAGE). Current
  models follow a plainly-stated instruction; stacked emphasis now
  causes over-triggering and rigid behavior rather than better
  compliance -- restated each as plain prose, same constraint.
- Redundant same-rule restatement collapsed to one instance
  (1-ideate's "think about X/Y/Z" step folded into the question-design
  step it fed; several doubled "do not X. do not X again" lines in
  5-implement, full, fix-bug).
- 6-verify: the four-times-repeated repair-loop skeleton (iteration
  counter, "spawn a fresh fix subagent", commit-does-not-push, clean/
  STUCK exit) factored into one canonical procedure that each of the
  three gates (Layer 1+2, Layer 3, Layer 4) now invokes with just its
  own collect/provide/re-run parameters. 412 -> 396 lines despite the
  new shared section, since three near-duplicate blocks became three
  short parameter lists.

Declined 2 of 21 findings: the ITERATION=0/MAX=5 pseudocode loops in
5-implement were flagged as chain-of-thought scaffolding, but they're
control-flow bookkeeping for the orchestrating session (not reasoning
scaffolding for an LLM call) and match the same convention used
consistently in 3-plan/4-validate/6-verify's repair loops -- rewriting
just that one file to prose would have made the convention
inconsistent across the skill family.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCjoA8z5seuFnfsnXo7XWS
PluginSource previously only scanned two flat plugin roots and missed
plugins installed via `/plugin install` under
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/. Add a third
source that resolves installPath entries from installed_plugins.json,
filters out anything disabled via settings.json's enabledPlugins (both
settings.json and settings.local.json checked, disabled-in-either wins),
dedupes multi-scope records to one dir per plugin, and skips stale
installPaths without aborting the scan. Marketplace plugins load with
lowest merge priority so dotfiles-committed global/local plugins keep
winning name collisions.

cli.py's sync_plugins now routes discovered marketplace plugins to the
same Claude/Antigravity global installers used for dotfiles-global
plugins (marketplace plugins have no per-project variant).

Verified on this machine against real ~/.claude/plugins/installed_plugins.json
and ~/.claude/settings.json: kotlin-lsp@claude-plugins-official (disabled)
is correctly excluded; enabling it (in a temp settings.json copy, no real
files touched) resolves its installPath correctly but it still doesn't
sync because that specific plugin's cache dir has no
.claude-plugin/plugin.json manifest -- a pre-existing fact about that
package, unrelated to this change and already flagged as an expected edge
case.

Adds src/sources/test_plugins.py, an assert-based self-check (no test
framework existed in this package) covering: enabled/disabled filtering,
missing/malformed installed_plugins.json, multi-scope dedup, stale-path
skip, and local-overrides-marketplace precedence.

Fixes #36

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pqo1a7CqGW6SZ8vHSFuq7h
Harness-generated worktree files (.backlog-context.md, .claude/commands/backlog/)
for this triage session, committed to satisfy the review gate's clean-tree check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pqo1a7CqGW6SZ8vHSFuq7h
- CRITICAL: sanitize plugin manifest `name` before using it to build
  destination paths. Marketplace plugin manifests are third-party content;
  an unvalidated name containing ".." or a path separator could let a
  malicious plugin write files outside the intended commands/skills
  install directory. Previously only self-committed dotfiles plugins
  reached this sink; this PR is what first routes untrusted marketplace
  manifests through it, so it's fixed here rather than deferred.
- MAJOR: skip symlinked command/skill files instead of following them,
  closing a path for a malicious plugin package to exfiltrate arbitrary
  readable files (e.g. ~/.ssh/id_rsa) into synced config directories.
- MAJOR: wire the existing --claude-settings-file CLI flag through to
  PluginSource so it actually affects marketplace enable/disable
  filtering instead of always reading ~/.claude/settings.json.
- MAJOR: split test_multi_scope_entry_dedupes_to_one_plugin (which used
  the same dir for both scope records, so it never actually verified scope
  preference) into a real preference test and a stale-path-fallback test;
  add coverage for the bare-dict installPath record shape and the two
  security fixes above.

Found by a 4-agent parallel code review (testing, code quality,
architecture, security) on PR #49; a hooks.json-merge finding from the
security pass was evaluated and declined -- hook execution is already a
native Claude Code plugin capability once a plugin is installed and
enabled, so llm-sync mirroring it into settings.json doesn't grant new
capability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pqo1a7CqGW6SZ8vHSFuq7h
@tstapler
tstapler marked this pull request as ready for review August 11, 2026 07:05
@tstapler
tstapler merged commit 2a907e2 into master Aug 12, 2026
6 checks passed
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.

llm-sync: PluginSource doesn't scan marketplace-installed Claude Code plugins

1 participant