Skip to content

feat(plugins): plugin / marketplace / hooks system + bilingual docs#13

Merged
woai3c merged 1 commit into
mainfrom
feat/plugin-marketplace-hooks
May 24, 2026
Merged

feat(plugins): plugin / marketplace / hooks system + bilingual docs#13
woai3c merged 1 commit into
mainfrom
feat/plugin-marketplace-hooks

Conversation

@woai3c
Copy link
Copy Markdown
Owner

@woai3c woai3c commented May 24, 2026

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) and anthropics/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

  • New plugins/, hooks/, commands/, and xc plugin CLI surfaces (~3300 lines of core)
  • Existing skills / sub-agents / mcp loaders accept plugin-contributed paths at startup; agent loop emits 10 lifecycle hook events (added PreCompact / PostCompact / SubagentStart / SubagentStop after a survey against Claude Code + Codex); file-based slash commands (commands/<name>.md) register as /<name> with $ARGUMENTS / ${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PLUGIN_DATA} substitution
  • Cross-platform hooks: every hook entry can ship commandWindows / commandDarwin / commandLinux overrides that win over command on the matching OS. Base command is always required as the safety net for any platform an author didn't think about
  • Persistent ${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
  • Real-world verified end-to-end: parses and installs the canonical Anthropic plugins, including monorepo-subdir + multi-line YAML sub-agents + auto-detected .mcp.json + executable slash commands
  • Bilingual docs for the whole extension story — 8 topics × zh + en = 16 files; covers features (skills / mcp / sub-agents / knowledge) that previously had no usage docs at all
  • README zh + en updated to surface the new features and link the docs
  • Pruned ~54 ceremony tests (whole suite, not just new code) by the criterion "would removing this let a real bug slip past everything else?"
  • 587/587 tests pass, typecheck / lint / format clean

What's in

Core (packages/core/src/)

  • plugins/types.ts, paths.ts, manifest.ts (zod, auto-detects .x-code-plugin/plugin.json and .claude-plugin/plugin.json; version optional to match real Claude Code plugins like amplitude that ship without one), installer.ts (git / github / local + monorepo subdir; enforces strictKnownMarketplaces + blockedPlugins policy gates), marketplace.ts (subscribe + cache + reserved-name protection + wire-format normaliser accepting every Claude Code source shape), enable-state.ts (global + project scope), loader.ts with convention-based auto-detection of skills/ / agents/ / commands/ / .mcp.json / hooks/hooks.json, registry.ts, integration.ts, consent.ts
  • hooks/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.ts with decision aggregators
  • commands/types.ts, loader.ts (scans plugin commands/*.md, handles real Claude Code multi-line allowed-tools frontmatter), registry.ts with expandCommandBody that substitutes $ARGUMENTS and ${CLAUDE_PLUGIN_ROOT}

CLI (packages/cli/src/)

  • plugin-cli.ts — non-interactive xc plugin ... subcommands with readline consent prompt and --yes flag
  • index.ts — startup wires plugin loading → integration → HookBus + CommandRegistry into AgentOptions; surfaces load errors via chalk yellow; auto-subscribes anthropic-marketplacegithub:anthropics/claude-plugins-official on first run
  • ui/components/App.tsx/plugin and /plugin marketplace slash command families; default dispatcher checks skill registry then file-based command registry before giving up

Existing loader 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)
  • agent/compression.ts emits PreCompact / PostCompact around both proactive (token-threshold) and reactive ("prompt too long") compaction paths
  • agent/sub-agents/runner.ts emits SubagentStart before delegating, SubagentStop (completed / aborted / failed) on every return path
  • /skill remove redirects to /plugin uninstall for plugin-sourced skills; pluginId field added to SkillDefinition / SubAgentDefinition

CLI surface

Slash Non-interactive
Plugins /plugin {list,info,install,uninstall,enable,disable,search,update,refresh,doctor} xc plugin <same>
Marketplace /plugin marketplace {list,add,remove,refresh,info} xc plugin marketplace <same>
Flags --no-plugins, --no-hooks
Plugin commands /<name> from any installed plugin's commands/<name>.md (with $ARGUMENTS substitution)

xc plugin install runs a readline y/N consent prompt that highlights MCP servers and hook events in red; --yes skips 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.json and anthropics/claude-plugins-official/.claude-plugin/marketplace.json, then installing and loading actual plugins from them. Every gap is fixed:

  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 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 / description / author (often omitting even version) and leave skills / agents / commands / mcpServers / hooks unset — the loader auto-detects skills/, agents/, commands/, .mcp.json, hooks/hooks.json when the manifest is silent.

  3. commands/ not wired (A1) — auto-detected by the loader but no slash-command dispatch path existed. Added packages/core/src/commands/ subsystem with file-based command loader, CommandRegistry, and expandCommandBody for the $ARGUMENTS / ${CLAUDE_PLUGIN_ROOT} substitutions real Claude Code commands use.

  4. 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 \n escapes). 8 plugin sub-agents from 2 real plugins load and become callable via task().

  5. .mcp.json (A3) — verified amplitude plugin's .mcp.json is auto-detected and registered.

  6. strictKnownMarketplaces / blockedPlugins (A5) — both were parsed but never enforced. Installer now checks strictKnownMarketplaces before fetch (reject if marketplace isn't subscribed) and blockedPlugins after manifest parse (reject by plugin id).

  7. Manifest version requirement — relaxed to optional (defaults to "0.0.0") so plugins like amplitude that ship without version install cleanly.

Other fixes 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): shallow-clone the whole repo, copy the subdir into a fresh temp dir, discard the rest
  • github source object form accepts both {owner, repo} and combined {repo: "owner/repo"} (claude-plugins-official uses both)
  • commit recognised as an alias for ref on github sources

End-to-end verification (tmp/verify-real-install.mjs, run manually, kept out of CI to avoid network dependency):

Installing agent-sdk-dev (anthropics/claude-code monorepo) ... OK
Installing pr-review-toolkit (real Claude Code sub-agents, multi-line YAML) ... OK
Installing amplitude (third-party plugin with .mcp.json) ... OK

Loaded 3 plugins
  agent-sdk-dev          commands ✓ | agents ✓
  pr-review-toolkit      commands ✓ | agents ✓
  amplitude              mcp ✓

Sub-agent loading (A2) ............... 8 plugin sub-agents loaded
MCP integration (A3) ................. 1 plugin mcpServer entry
File-based slash commands (A1) ....... 2 plugin commands (/new-sdk-app, /review-pr)
  $ARGUMENTS replaced .................. ✓
  ${CLAUDE_PLUGIN_ROOT} replaced ....... ✓

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 directly
  • Gemini extensions (gemini-extension.json) explicitly rejected with friendly error
  • Reserved marketplace names (anthropic-marketplace, claude-plugins, x-code-official) only accept their canonical GitHub orgs

Docs (docs/, bilingual)

File pair Coverage
skills.{md,en.md} SKILL.md format, /skill commands
sub-agents.{md,en.md} 4 built-ins + custom .md format + constraints
mcp.{md,en.md} mcpServers schema, OAuth flow, /mcp commands
knowledge.{md,en.md} AGENTS.md vs CLAUDE.md fallback, 5-layer load
plugins.{md,en.md} install / manage / troubleshoot
marketplace.{md,en.md} full source-shape table covering every Claude Code wire form
hooks.{md,en.md} 6 events, stdin/stdout protocol, lint example
plugin-authoring.{md,en.md} manifest schema + convention-based auto-detection

README zh + en updated with new feature mentions + "Detailed Usage Docs" section linking the 8 doc pairs above.

Tests

  • 50 test files / 599 tests passing
  • Plugin / hook / commands subsystem: ~72 new tests including a regression test that enumerates every Claude Code source wire form, multi-line YAML frontmatter parsing for commands, $ARGUMENTS substitution, strictKnownMarketplaces + blockedPlugins enforcement, cross-platform hook command pick + base fallback, ${pluginDataDir} auto-create, userConfig install-time prompt persistence + abort, /plugin refresh end-to-end folding new plugin into skill registry
  • ~54 ceremony tests pruned across the whole suite

Known limitations (followup PRs)

  • userConfig's sensitive: true controls display during input (input is not echoed). Values are stored in ~/.x-code/plugins/user-config.json with file mode 0600 on Linux/macOS — owner-only readable. On Windows 0600 is 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.
  • sha integrity-pin field on marketplace sources is parsed but not verified
  • xc plugin update requires an id (no bulk no-arg upgrade)

Highlights

  • Two-scope model user / project across mcp / skill / plugin / auto-memory — ~/.x-code/settings.json (user) + <repo>/.x-code/settings.local.json (project, gitignored). --scope=user|project flag on every enable/disable entry point.
  • /plugin refresh hot-reloads in-process. Rebuilds PluginRegistry + folds new contributions into skill / sub-agent / command / hook registries, invalidates systemPromptCache. Plugin-contributed MCP servers still need /mcp refresh separately (they hold child processes).
  • /plugin list --enabled / --disabled filters.
  • --plugin-debug / XC_PLUGIN_DEBUG=1 mirror plugin / hook / marketplace debugLog lines to stderr live — narrower than DEBUG_STDOUT=1 (which dumps every tag).
  • userConfig install-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 typecheck clean
  • pnpm lint clean (one pre-existing warning unrelated to this PR)
  • pnpm format:check clean
  • pnpm test — 599/599 pass, 4 pre-existing skips
  • Install a local plugin with skills + agents + mcp + hooks via xc plugin install ./fixture, restart, verify /skill list, /mcp list, task tool, and hook emit-sites all see contributions
  • Reserved-name guard rejects anthropic-marketplace with non-anthropics source
  • Hook deny decision blocks the corresponding tool / prompt; modify args + output paths work
  • --no-plugins and --no-hooks escape hatches work
  • Real-world: anthropics/claude-code and anthropics/claude-plugins-official marketplaces (216 plugins total) parse end-to-end
  • Real-world: install agent-sdk-dev + pr-review-toolkit (monorepo subdirs) — commands, agents auto-detected
  • Real-world: install amplitude.mcp.json auto-detected and registered
  • Real-world: 8 sub-agents from real plugins load (validates multi-line YAML description parse)
  • Real-world: 2 plugin commands (/new-sdk-app, /review-pr) load with $ARGUMENTS + ${CLAUDE_PLUGIN_ROOT} substitution working
  • strictKnownMarketplaces rejects non-subscribed-source installs (unit + integration tested)
  • blockedPlugins rejects matching plugin id at install time (unit tested)
  • Cross-platform hook command: schema accepts commandWindows / 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 -p on first substitution; preserved when plugin is uninstalled (data is keyed by plugin id, not by version)
  • 4 new hook events (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)

@woai3c woai3c force-pushed the feat/plugin-marketplace-hooks branch 4 times, most recently from c72b2d7 to 5c259d9 Compare May 24, 2026 14:23
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
@woai3c woai3c force-pushed the feat/plugin-marketplace-hooks branch from 5c259d9 to a855a95 Compare May 24, 2026 15:57
@woai3c woai3c merged commit b310640 into main May 24, 2026
5 checks passed
@woai3c woai3c deleted the feat/plugin-marketplace-hooks branch May 24, 2026 16:00
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.

1 participant