@theglitchking/claude-plugin-runtime
v0.1.0
Published
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.
Maintainers
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>.jsonwith{ "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 check —
off/nudge/autopolicies with a 3s network budget, 6h cache, CI-skip, and plugin/npm dedup at runtime. - CLI subcommand registration —
update,policy,status,relinkfor terminal parity with the slash commands.
Install
npm install --save @theglitchking/claude-plugin-runtimeUsage
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
<ENV_PREFIX>_UPDATE_POLICYenv var (one-shot override).<project>/.claude/<configFile>→updatePolicy.- Default:
nudge.
ENV_PREFIX defaults to the upper-snake form of pluginName (e.g.
semantic-pages → SEMANTIC_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:
runPostinstallscans~/.claude/settings.json→enabledPlugins. If<pluginName>@*: true, it skips registering the project-level hook. - At runtime:
runSessionStartchecksprocess.env.CLAUDE_PLUGIN_ROOT. If it's set (the hook was invoked by the plugin marketplace) and the project's.claude/settings.jsoncontains 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
