JSPM

@theglitchking/claude-plugin-runtime

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 30
  • Score
    100M100P100Q72632F
  • License MIT

Shared postinstall + SessionStart + CLI-subcommand runtime for Claude Code plugins distributed via npm. Handles skill symlinking, update policy config, hook registration with plugin/npm dedup, and update nudge/auto-apply.

Package Exports

  • @theglitchking/claude-plugin-runtime

Readme

@theglitchking/claude-plugin-runtime

Shared postinstall + SessionStart + CLI-subcommand runtime for Claude Code plugins distributed via npm. One small package (~13 KB, zero runtime deps) that handles the boilerplate every plugin in the Glitch Kingdom marketplace needs:

  • Skill symlinking — bundled skills in node_modules/ get linked into <project>/.claude/skills/ so Claude Code can discover them.
  • Default policy config — writes <project>/.claude/<plugin>.json with { "updatePolicy": "nudge" } if it doesn't exist.
  • Hook registration with dedup — registers a SessionStart hook in <project>/.claude/settings.json, but skips when the Claude Code plugin marketplace version is already enabled globally, or when the project already has a matching hook.
  • SessionStart update checkoff / nudge / auto policies with a 3s network budget, 6h cache, CI-skip, and plugin/npm dedup at runtime.
  • CLI subcommand registrationupdate, policy, status, relink for terminal parity with the slash commands.

Install

npm install --save @theglitchking/claude-plugin-runtime

Usage

Three entry points. Each plugin wires them up once and inherits every behavior change made to this package.

Postinstall

In scripts/link-skills.js (or whatever you call your postinstall script):

#!/usr/bin/env node
import { runPostinstall } from "@theglitchking/claude-plugin-runtime";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");

runPostinstall({
  packageName: "@theglitchking/my-plugin",
  pluginName: "my-plugin",
  configFile: "my-plugin.json",
  skillsDir: "skills",
  packageRoot,
  hookCommand: "node ./node_modules/@theglitchking/my-plugin/hooks/session-start.js",
});

Wire it in package.json:

{
  "scripts": { "postinstall": "node scripts/link-skills.js" }
}

SessionStart hook

In hooks/session-start.js:

#!/usr/bin/env node
import { runSessionStart } from "@theglitchking/claude-plugin-runtime";

await runSessionStart({
  packageName: "@theglitchking/my-plugin",
  pluginName: "my-plugin",
  configFile: "my-plugin.json",
  reconcile: (projectRoot) => {
    // Plugin-specific setup: .mcp.json reconciliation, scaffolding, etc.
    // Thrown errors are caught and logged — the update check still runs.
  },
});

Register it in the plugin's hooks/hooks.json:

{
  "hooks": {
    "SessionStart": [
      { "hooks": [
          { "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.js\"" }
      ]}
    ]
  }
}

CLI subcommands

In your commander-based CLI:

import { program } from "commander";
import { registerUpdateCommands } from "@theglitchking/claude-plugin-runtime";
import { fileURLToPath } from "node:url";
import { dirname, resolve, join } from "node:path";
import { spawnSync } from "node:child_process";

registerUpdateCommands(program, {
  packageName: "@theglitchking/my-plugin",
  pluginName: "my-plugin",
  configFile: "my-plugin.json",
  onAfterUpdate: (cwd) => {
    const linker = join(cwd, "node_modules", "@theglitchking", "my-plugin", "scripts", "link-skills.js");
    spawnSync(process.execPath, [linker], {
      cwd,
      env: { ...process.env, INIT_CWD: cwd },
      stdio: "inherit",
    });
  },
});

Policy resolution

  1. <ENV_PREFIX>_UPDATE_POLICY env var (one-shot override).
  2. <project>/.claude/<configFile>updatePolicy.
  3. Default: nudge.

ENV_PREFIX defaults to the upper-snake form of pluginName (e.g. semantic-pagesSEMANTIC_PAGES). Override via envPrefix if you want something different.

Env opt-outs

Variable Effect
<PREFIX>_SKIP_LINK=1 Skip skill symlinking in postinstall.
<PREFIX>_SKIP_HOOK_REGISTER=1 Skip settings.json hook registration.
<PREFIX>_UPDATE_POLICY=off|nudge|auto One-shot policy override.

Dedup between plugin and npm install

When a user has both a Claude Code plugin install and the npm dep:

  • At install time: runPostinstall scans ~/.claude/settings.jsonenabledPlugins. If <pluginName>@*: true, it skips registering the project-level hook.
  • At runtime: runSessionStart checks process.env.CLAUDE_PLUGIN_ROOT. If it's set (the hook was invoked by the plugin marketplace) and the project's .claude/settings.json contains a SessionStart entry whose command includes the plugin name, the plugin instance defers to the project-registered one.

Detection is substring-based on the plugin name — no magic tags or marker fields. Any command string in settings.json containing the plugin name is treated as "someone else is handling this," and we step aside.

Full authoring recipe

See docs/PLUGIN_AUTHORING_SCAFFOLD.md for a copy-paste template for a new plugin, including file layout, commander CLI wiring, slash commands, and the testing checklist.

Reference implementation

@theglitchking/semantic-pages is the canonical consumer. Diff its scripts/link-skills.js and hooks/session-start.js against a new plugin to see the minimal per-plugin delta (just the reconcile body and the config field names).

License

MIT