Skip to content

feat(plugin): native Claude Code plugin + dual-mode npx install#3

Open
mh0pe wants to merge 6 commits into
ChristopherKahler:mainfrom
mh0pe:feat/native-plugin
Open

feat(plugin): native Claude Code plugin + dual-mode npx install#3
mh0pe wants to merge 6 commits into
ChristopherKahler:mainfrom
mh0pe:feat/native-plugin

Conversation

@mh0pe

@mh0pe mh0pe commented Jun 14, 2026

Copy link
Copy Markdown

What

Makes the repo installable both as a native Claude Code marketplace plugin and via the existing npx commands, from one committed source. Adds root .claude-plugin/plugin.json + .claude-plugin/marketplace.json (single-plugin, source: "."), so:

claude plugin marketplace add ChristopherKahler/base
claude plugin install base@base

works, while npx @chrisai/base --global|--local|--workspace keeps working unchanged.

Single source of truth

The committed framework/command/skill/hook/mcp tree at the repo root uses ${CLAUDE_PLUGIN_ROOT} macros and is the sole source. bin/install.js reads from that root tree for every install mode and substitutes ${CLAUDE_PLUGIN_ROOT} with the real install target during copy (round-tripping to the old ~/.claude/... semantics for --global). There is no parallel src/ copy to drift out of sync (only the two pure-JSON src/templates/ data files remain, used by --workspace).

MCP bootstrapping

A marketplace/git plugin has no install step, so the base-mcp npm deps aren't present at launch and node mcp/index.js would fail with ERR_MODULE_NOT_FOUND. A committed fail-open SessionStart hook (hooks/install-mcp-deps.py) idempotently installs the MCP deps into ${CLAUDE_PLUGIN_DATA} and links them onto the MCP's module-resolution path (base-mcp is ESM, and Node's ESM loader ignores NODE_PATH, so a node_modules symlink is the operative mechanism; .mcp.json also sets NODE_PATH as a CJS fallback). It short-circuits when deps exist and never blocks the session if npm / ${CLAUDE_PLUGIN_DATA} is unavailable.

Verified: after the bootstrap, the MCP boots cleanly (BASE MCP Server running on stdio), no ERR_MODULE_NOT_FOUND.

First-session caveat: Claude Code may spawn the MCP concurrently with the SessionStart hook, so on a brand-new install base-mcp becomes available from the next session (or after a restart) once deps are linked.

CI

Adds .github/workflows/plugin-install.yml: claude plugin validate . --strict (the CI-grade manifest gate) plus an install smoke (marketplace add + install base@base + list). Both verified to run auth-free.

Notes

  • Includes the CLAUDE_PROJECT_DIR workspace-resolution fix (the hooks need it to resolve the workspace correctly in a plugin layout); the no-env else-branch is byte-equivalent to the original default. Project-runtime refs (@.base/...) are untouched.
  • npx mode is unaffected: it writes its own absolute-path .mcp.json and runs its own npm install; the plugin-only ${CLAUDE_PLUGIN_DATA} env and bootstrap hook don't leak into npx installs, and no literal ${CLAUDE_PLUGIN_ROOT} appears in npx output.
  • Ref: https://code.claude.com/docs/en/plugins-reference

🤖 Generated with Claude Code

mh0pe and others added 6 commits June 13, 2026 23:14
- Add .claude-plugin/plugin.json (name=base, version=3.1.5 from package.json)
- Add .claude-plugin/marketplace.json for claude plugin marketplace add
- Copy src/commands -> commands/ with namespace-strip (base: prefix removed)
- Copy src/skill/base.md -> skills/base/base.md
- Copy src/framework -> base-framework/ (tasks, templates, context, etc.)
- Copy src/hooks -> hooks/ with CLAUDE_PROJECT_DIR-first WORKSPACE_ROOT fix
- Copy src/packages/base-mcp -> mcp/ with CLAUDE_PROJECT_DIR-first WORKSPACE_PATH fix
- Wire hooks/hooks.json: 5 UserPromptSubmit + 1 SessionStart (apex-insights excluded)
- Add .mcp.json registering base-mcp via node ${CLAUDE_PLUGIN_ROOT}/mcp/index.js
- Rewrite @~/.claude/base-framework/ -> @${CLAUDE_PLUGIN_ROOT}/base-framework/ in
  commands/, skills/, base-framework/ plugin-native tree (66 refs)
- src/ tree untouched: npx install reads src/ which retains @~/.claude/ refs
- bin/install.js: add copyFileExpandingMacro() to expand ${CLAUDE_PLUGIN_ROOT}
  during any copy, ensuring no literal placeholder in npx-installed output

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the duplicate src/ framework/command/hook/mcp trees that
shadowed the root plugin-native tree (commands/, base-framework/,
skills/, hooks/, mcp/). The root tree uses ${CLAUDE_PLUGIN_ROOT}
macros and is the single committed source of truth.

bin/install.js now reads from the root tree for all install modes
(--global, --local, --workspace). copyFileExpandingMacro is now
exercised for every copy path, substituting ${CLAUDE_PLUGIN_ROOT}
with the resolved install target so no literal placeholder survives
in npx-installed output.

Retained: src/templates/ (workspace.json + operator.json) — these
are the npx workspace-mode templates; they contain no macros and
have no equivalent in the root tree.

package.json files[] updated to ship the root tree instead of src/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add committed hooks/install-mcp-deps.py (SessionStart hook) that
installs @modelcontextprotocol/sdk deps into CLAUDE_PLUGIN_DATA and
symlinks PLUGIN_ROOT/mcp/node_modules -> PLUGIN_DATA/node_modules so
Node ESM can resolve bare specifiers from the importing file's directory.

The MCP server lives at mcp/index.js (not mcp/base-mcp/index.js as in
the install-skills-dir branch). The symlink is placed at mcp/node_modules
adjacent to mcp/index.js so ESM's directory-walk resolution succeeds.

NODE_PATH added to .mcp.json env (CJS fallback; Node ESM requires the
symlink bridge since ESM does not honour NODE_PATH at import time).

- Idempotent: skips install if sdk marker already present; re-asserts symlink
- Fail-open: warns to stderr and exits 0 on any error (npm missing, env unset)
- Keeps 5 UserPromptSubmit hooks + satellite-detection SessionStart hook
- Adds install-mcp-deps as second SessionStart hook entry
- npx-mode safety: hook is harmless/fail-open when CLAUDE_PLUGIN_DATA unset

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… symlink

- .gitignore: add slash-less `mcp/node_modules` entry so the SessionStart
  symlink (created by install-mcp-deps.py) is properly ignored.
  The existing `node_modules/` pattern matches directories only; a trailing
  slash does NOT match symlinks, causing the link to appear as untracked.
- .github/workflows/plugin-install.yml: validate + auth-free install smoke
  on every push/PR (identical shape to paul/seed/carl native workflows).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mh0pe

mh0pe commented Jun 14, 2026

Copy link
Copy Markdown
Author

Related PRs — part of a coordinated cross-repo offer (native Claude Code plugin + dual-mode npx install) applied across the carl/base/paul/seed framework forks:

Reviewing them together is recommended; the same change pattern is mirrored per repo.

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