feat(plugins): plugin / marketplace / hooks system + bilingual docs#13
Merged
Conversation
c72b2d7 to
5c259d9
Compare
Adds a complete plugin system to x-code-cli: skills, sub-agents, slash
commands, MCP servers, and lifecycle hooks bundle into installable
units, discovered via subscribed marketplaces (no self-hosted catalog),
and powered by a 6-event hook system that lets plugins intercept or
rewrite agent behaviour.
**End-to-end verified against real Anthropic Claude Code marketplaces**
— installs plugins from `anthropics/claude-code` and the 203-plugin
`anthropics/claude-plugins-official` correctly.
## What's in
Core: ~3000 lines
- `packages/core/src/plugins/` — types, paths, manifest parsing (zod;
auto-detects `.x-code-plugin/plugin.json` and `.claude-plugin/plugin.json`),
installer (git / github / local + subdir), marketplace (subscribe +
cache + reserved-name protection), enable-state (global + project
scope), loader (auto-detects conventional `skills/` / `agents/` /
`commands/` / `.mcp.json` / `hooks/hooks.json`), registry,
integration, consent
- `packages/core/src/hooks/` — types, schema, variable expansion
(`${pluginDir}` / `${cwd}` / `${env:NAME}` / …), executor (shell
spawn with stdin/stdout JSON protocol, timeout, AbortSignal), bus
with decision aggregators
- `packages/cli/src/plugin-cli.ts` — non-interactive `xc plugin ...`
subcommands with readline consent prompt + `--yes` flag
Integration
- Skills / sub-agents / mcp loaders gain `extraDirs` / `extraServers`
so plugin contributions fold into existing registries at startup
- `agent/loop.ts` emits SessionStart, UserPromptSubmit (deny / inject
context), TurnComplete
- `agent/tool-execution.ts` emits PreToolUse (deny / modify args),
PostToolUse (modify output)
- CLI startup auto-subscribes `anthropic-marketplace` →
`github:anthropics/claude-plugins-official` on first run
- `/skill remove` redirects to `/plugin uninstall` for plugin-sourced
skills; `pluginId` carried on SkillDefinition / SubAgentDefinition
CLI surface
- Slash: `/plugin {list,info,install,uninstall,enable,disable,search,
update,refresh,doctor,marketplace}` with marketplace sub-tree
- Non-interactive: `xc plugin <same>` mirrors the slash family
- Startup flags: `--no-plugins`, `--no-hooks`
## Real-world compatibility (verified)
The PR initially landed with two schema-design bugs that made it
incompatible with every real-world Claude Code marketplace. Both were
caught by actually fetching `anthropics/claude-code/.claude-plugin/
marketplace.json` and `anthropics/claude-plugins-official/.claude-plugin/
marketplace.json` and running them through the parser:
1. **Wire-format mismatch.** Real Claude Code marketplaces use
`source` (not `kind`) as the discriminator with values
`'git-subdir'` / `'url'` / `'github'` / `'local'`, plus a plain
string shortcut for monorepo subdirs (`"./plugins/foo"`). The
normaliser at `normalizeMarketplaceSource` now accepts every shape
we observed and maps it to the internal `PluginSource` form so the
installer stays on one shape.
2. **Conventional auto-detection.** Real plugin manifests typically
only declare `name` / `version` / `description` / `author` and
leave `skills` / `agents` / `commands` / `mcpServers` / `hooks`
unset — the loader is expected to auto-detect the conventional
directories. `resolveContributions` now probes `skills/`,
`agents/`, `commands/`, `.mcp.json`, `hooks/hooks.json` when the
manifest is silent.
Additional fixes also driven by real-world data:
- Marketplace fetch path: `.claude-plugin/marketplace.json` (was
`marketplace.json` at root)
- Default subscription: `anthropics/claude-plugins-official` (was a
non-existent `anthropics/marketplace`)
- Subdir installs (`git-subdir` source / monorepo plugins): the
installer now shallow-clones the whole repo, copies the subdir into
a fresh temp dir, discards the rest. Previously rejected outright.
- `github` source object form accepts both `{owner, repo}` and the
combined `{repo: "owner/repo"}` shape that
`claude-plugins-official` uses
- `commit` recognised as an alias for `ref` on github sources
End-to-end test (`tmp/verify-real-install.mjs`, run manually, kept out
of CI to avoid network dependency):
- Installs `code-review` from `anthropics/claude-code` (monorepo subdir)
- Loads via `loadAllPlugins`
- Verifies `commandsDir` was auto-detected from `commands/` even
though the manifest doesn't declare it
- Installs `pr-review-toolkit` (larger, also subdir)
- Both succeed cleanly
Compatibility summary
- Plugin manifest reads `.claude-plugin/plugin.json` in addition to
the native path, so Claude Code / Codex plugins install unmodified
- MCP server config schema unchanged — copy from `~/.claude/config.json`
- Gemini extensions explicitly rejected with friendly error
- Reserved marketplace names (`anthropic-marketplace`,
`claude-plugins`, `x-code-official`) only accept their canonical
GitHub orgs to prevent impersonation
## Docs (bilingual, `*.md` zh + `*.en.md` en) — ~2400 lines
- `docs/skills.{md,en.md}` — SKILL.md format, `/skill` commands
- `docs/sub-agents.{md,en.md}` — built-in + custom .md format
- `docs/mcp.{md,en.md}` — mcpServers config, OAuth flow, /mcp commands
- `docs/knowledge.{md,en.md}` — AGENTS.md 5-layer load + auto-memory
- `docs/plugins.{md,en.md}` — install / manage / troubleshoot
- `docs/marketplace.{md,en.md}` — schema, subscribe, self-host;
source-shape table covers every Claude Code wire form
- `docs/hooks.{md,en.md}` — 6 events, stdin/stdout protocol, examples
- `docs/plugin-authoring.{md,en.md}` — manifest schema; explains the
convention-based auto-detection so authors know they can omit
declared paths for the standard layout
README zh + en updated: feature list now mentions skills / MCP /
plugins / hooks (all previously undocumented), new "Detailed Usage
Docs" section links the 8 docs above.
## Tests
- ~50 new tests for the plugin + hook subsystem, including a
regression test that enumerates every Claude Code source wire form
(`./plugins/foo`, `github:owner/repo`, `git-subdir`, `url`,
`github` combined `repo: owner/repo`, etc.)
- ~54 ceremony tests pruned across the whole suite
- 575 tests pass, typecheck / lint / format clean
## Known limitations
- `userConfig` field is parsed but **not actually prompted at install
time yet** — the install consent shows the declared fields but
doesn't ask for values
- `/plugin refresh` prints "restart xc" — no in-process live reload
- `/plugin enable|disable` writes global scope only (no `--scope` flag)
- Multi-version coexistence / rollback not implemented
- `commands/` plugin contribution is auto-detected and surfaced in
`/plugin info`, but the CLI has no file-based slash command loader
today — the directory is recognised, not yet wired
- `sha` integrity-pin field on marketplace sources is parsed but
not verified
5c259d9 to
a855a95
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a complete plugin system to x-code-cli: skills, sub-agents, slash commands, MCP servers, and lifecycle hooks bundle into installable units, discovered via subscribed marketplaces (no self-hosted catalog), and powered by a 6-event hook system that lets plugins intercept or rewrite agent behaviour.
End-to-end verified against real Anthropic Claude Code marketplaces — installs plugins from
anthropics/claude-code(13) andanthropics/claude-plugins-official(203). All five core contribution types (skills / sub-agents / slash commands / MCP servers / hooks) load and execute correctly against real-world data.Summary
plugins/,hooks/,commands/, andxc pluginCLI surfaces (~3300 lines of core)commands/<name>.md) register as/<name>with$ARGUMENTS/${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_DATA}substitutioncommandWindows/commandDarwin/commandLinuxoverrides that win overcommandon the matching OS. Base command is always required as the safety net for any platform an author didn't think about${pluginDataDir}: each plugin gets a~/.x-code/plugins/data/<id>/dir auto-mkdir -p'd on first use that survives reinstall / version upgrade — the place to put indexes, caches, or learned preferences without losing them when the user upgrades.mcp.json+ executable slash commandsWhat's in
Core (
packages/core/src/)plugins/—types.ts,paths.ts,manifest.ts(zod, auto-detects.x-code-plugin/plugin.jsonand.claude-plugin/plugin.json;versionoptional to match real Claude Code plugins like amplitude that ship without one),installer.ts(git / github / local + monorepo subdir; enforcesstrictKnownMarketplaces+blockedPluginspolicy gates),marketplace.ts(subscribe + cache + reserved-name protection + wire-format normaliser accepting every Claude Code source shape),enable-state.ts(global + project scope),loader.tswith convention-based auto-detection ofskills//agents//commands//.mcp.json/hooks/hooks.json,registry.ts,integration.ts,consent.tshooks/—types.ts(10 events: SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / PreCompact / PostCompact / SubagentStart / SubagentStop / TurnComplete / SessionEnd),config-schema.ts,variables.ts(${pluginDir}/${pluginDataDir}/${cwd}/${env:NAME}/ …),executor.ts(shell spawn with stdin/stdout JSON protocol, picks platform-specific command override before exec, 5s default timeout / 30s cap, AbortSignal cancellation),bus.tswith decision aggregatorscommands/—types.ts,loader.ts(scans plugincommands/*.md, handles real Claude Code multi-lineallowed-toolsfrontmatter),registry.tswithexpandCommandBodythat substitutes$ARGUMENTSand${CLAUDE_PLUGIN_ROOT}CLI (
packages/cli/src/)plugin-cli.ts— non-interactivexc plugin ...subcommands with readline consent prompt and--yesflagindex.ts— startup wires plugin loading → integration →HookBus+CommandRegistryintoAgentOptions; surfaces load errors via chalk yellow; auto-subscribesanthropic-marketplace→github:anthropics/claude-plugins-officialon first runui/components/App.tsx—/pluginand/plugin marketplaceslash command families; default dispatcher checks skill registry then file-based command registry before giving upExisting loader integration
extraDirs/extraServersso plugin contributions fold into existing registries at startupagent/loop.tsemitsSessionStart,UserPromptSubmit(deny / inject context),TurnCompleteagent/tool-execution.tsemitsPreToolUse(deny / modify args),PostToolUse(modify output)agent/compression.tsemitsPreCompact/PostCompactaround both proactive (token-threshold) and reactive ("prompt too long") compaction pathsagent/sub-agents/runner.tsemitsSubagentStartbefore delegating,SubagentStop(completed/aborted/failed) on every return path/skill removeredirects to/plugin uninstallfor plugin-sourced skills;pluginIdfield added toSkillDefinition/SubAgentDefinitionCLI surface
/plugin {list,info,install,uninstall,enable,disable,search,update,refresh,doctor}xc plugin <same>/plugin marketplace {list,add,remove,refresh,info}xc plugin marketplace <same>--no-plugins,--no-hooks/<name>from any installed plugin'scommands/<name>.md(with$ARGUMENTSsubstitution)xc plugin installruns a readline y/N consent prompt that highlights MCP servers and hook events in red;--yesskips it for scripts.Real-world compatibility (verified end-to-end)
The PR initially landed with several schema-design bugs that made it incompatible with real Claude Code marketplaces. All caught by actually fetching
anthropics/claude-code/.claude-plugin/marketplace.jsonandanthropics/claude-plugins-official/.claude-plugin/marketplace.json, then installing and loading actual plugins from them. Every gap is fixed:Wire-format mismatch. Real Claude Code marketplaces use
source(notkind) as the discriminator with values'git-subdir'/'url'/'github'/'local', plus a plain string shortcut for monorepo subdirs ("./plugins/foo"). The normaliser atnormalizeMarketplaceSourceaccepts every shape we observed and maps it to the internalPluginSourceform so the installer stays on one shape.Conventional auto-detection. Real plugin manifests typically only declare
name/description/author(often omitting evenversion) and leaveskills/agents/commands/mcpServers/hooksunset — the loader auto-detectsskills/,agents/,commands/,.mcp.json,hooks/hooks.jsonwhen the manifest is silent.commands/not wired (A1) — auto-detected by the loader but no slash-command dispatch path existed. Addedpackages/core/src/commands/subsystem with file-based command loader, CommandRegistry, andexpandCommandBodyfor the$ARGUMENTS/${CLAUDE_PLUGIN_ROOT}substitutions real Claude Code commands use.Sub-agent multi-line description (A2) — verified our minimal YAML frontmatter parser handles the real format used by
pr-review-toolkit/agents/code-reviewer.md(description spans many lines with literal\nescapes). 8 plugin sub-agents from 2 real plugins load and become callable viatask()..mcp.json(A3) — verifiedamplitudeplugin's.mcp.jsonis auto-detected and registered.strictKnownMarketplaces/blockedPlugins(A5) — both were parsed but never enforced. Installer now checksstrictKnownMarketplacesbefore fetch (reject if marketplace isn't subscribed) andblockedPluginsafter manifest parse (reject by plugin id).Manifest
versionrequirement — relaxed to optional (defaults to"0.0.0") so plugins likeamplitudethat ship withoutversioninstall cleanly.Other fixes driven by real-world data:
.claude-plugin/marketplace.json(wasmarketplace.jsonat root)anthropics/claude-plugins-official(was a non-existentanthropics/marketplace)git-subdirsource / monorepo plugins): shallow-clone the whole repo, copy the subdir into a fresh temp dir, discard the restgithubsource object form accepts both{owner, repo}and combined{repo: "owner/repo"}(claude-plugins-official uses both)commitrecognised as an alias forrefon github sourcesEnd-to-end verification (
tmp/verify-real-install.mjs, run manually, kept out of CI to avoid network dependency):Compatibility summary
.claude-plugin/plugin.jsonin addition to the native path, so Claude Code / Codex plugins install unmodified~/.claude/config.jsondirectlygemini-extension.json) explicitly rejected with friendly erroranthropic-marketplace,claude-plugins,x-code-official) only accept their canonical GitHub orgsDocs (
docs/, bilingual)skills.{md,en.md}/skillcommandssub-agents.{md,en.md}mcp.{md,en.md}mcpServersschema, OAuth flow,/mcpcommandsknowledge.{md,en.md}plugins.{md,en.md}marketplace.{md,en.md}hooks.{md,en.md}plugin-authoring.{md,en.md}README zh + en updated with new feature mentions + "Detailed Usage Docs" section linking the 8 doc pairs above.
Tests
${pluginDataDir}auto-create, userConfig install-time prompt persistence + abort,/plugin refreshend-to-end folding new plugin into skill registryKnown limitations (followup PRs)
userConfig'ssensitive: truecontrols display during input (input is not echoed). Values are stored in~/.x-code/plugins/user-config.jsonwith file mode0600on Linux/macOS — owner-only readable. On Windows0600is a no-op (ACL model differs); rely on the user-profile dir being private. OS keychain integration is not planned for the personal-developer use case this product targets; if a team / enterprise workflow needs it later, that's a separate PR.shaintegrity-pin field on marketplace sources is parsed but not verifiedxc plugin updaterequires an id (no bulk no-arg upgrade)Highlights
user / projectacross mcp / skill / plugin / auto-memory —~/.x-code/settings.json(user) +<repo>/.x-code/settings.local.json(project, gitignored).--scope=user|projectflag on every enable/disable entry point./plugin refreshhot-reloads in-process. Rebuilds PluginRegistry + folds new contributions into skill / sub-agent / command / hook registries, invalidatessystemPromptCache. Plugin-contributed MCP servers still need/mcp refreshseparately (they hold child processes)./plugin list --enabled/--disabledfilters.--plugin-debug/XC_PLUGIN_DEBUG=1mirror plugin / hook / marketplacedebugLoglines to stderr live — narrower thanDEBUG_STDOUT=1(which dumps every tag).userConfiginstall-time prompt with sensitive-field masking. Values land in~/.x-code/plugins/user-config.json(mode 0600); auto-injected into hook subprocess env and plugin-contributed MCP server env at launch.Test plan
pnpm typecheckcleanpnpm lintclean (one pre-existing warning unrelated to this PR)pnpm format:checkcleanpnpm test— 599/599 pass, 4 pre-existing skipsxc plugin install ./fixture, restart, verify/skill list,/mcp list,tasktool, and hook emit-sites all see contributionsanthropic-marketplacewith non-anthropics source--no-pluginsand--no-hooksescape hatches workanthropics/claude-codeandanthropics/claude-plugins-officialmarketplaces (216 plugins total) parse end-to-endagent-sdk-dev+pr-review-toolkit(monorepo subdirs) — commands, agents auto-detectedamplitude—.mcp.jsonauto-detected and registered/new-sdk-app,/review-pr) load with$ARGUMENTS+${CLAUDE_PLUGIN_ROOT}substitution workingcommandWindows/commandDarwin/commandLinux; executor picks the matching one (real-subprocess regression test); base command serves as fallback when current OS has no override${pluginDataDir}variable: expanded in both hook commands and slash command bodies; auto-mkdir -pon first substitution; preserved when plugin is uninstalled (data is keyed by plugin id, not by version)PreCompact,PostCompact,SubagentStart,SubagentStop): schema acceptance tested; emit sites wired in compression.ts (both proactive + reactive paths) and sub-agents/runner.ts (all 3 outcome paths)