npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aeriondyseti/plugin-kit

v2.3.0

Published

Typed toolkit for Claude Code plugins: hook events, styled output, and shared widgets for mods via the plugin-kit plugin.

Readme

@aeriondyseti/plugin-kit

A typed toolkit for Claude Code plugins: hook scripts, styled output, and widgets that mods share through the plugin-kit plugin. Covers all 33 hook events as of Claude Code 2.1.283. Formerly @aeriondyseti/hook-kit.

  • One class per hook event with two static methods: parse() reads and validates stdin, emitOutput() writes the response JSON and exits.
  • A small OutputBuilder for styled multi-line text — boxes, tables, lists, dividers, icons, colors via tag markup.
  • Widgets (@aeriondyseti/plugin-kit/widgets): meters, clocks, counters, lists and tags, validated and drawn the same way in hook output and in mods.
  • A testing subpath (@aeriondyseti/plugin-kit/testing) with testHook, mockXxx input factories, and normalized result fields so you can assert result.wasDenied instead of spelunking the payload.

Documentation: the wiki, also in docs/. This README is the quick tour.

Which part do you need?

| You're writing | Use | Section | | --- | --- | --- | | A hook script: a command hook in settings.json or a plugin's hooks.json | the npm package: typed events, OutputBuilder, /testing | Hook scripts | | A mod (a plugin with function hooks) that should look like other mods and follow the user's kit settings | the plugin-kit plugin through $.kit | Widgets in a mod: $.kit | | A mod that must stand alone, with no plugin to install | the widget code vendored into your mod | Widgets in a mod: vendored | | Hook typings | hook scripts: this package; mods: Claude Code's own claude-code types | Hook typings | | Hook logic in a mod, or moved there from a script | the adapter (vendor adapter) | Hooks in mods | | A status line | plugin-kit statusline, or /statusline | Status line | | Tests from real sessions | plugin-kit record, run, fixtures; vendor testing for mods | Fixtures and mod tests | | A new hook or mod, a check, CI types, a release | plugin-kit new, doctor, types, release | Tooling |

Install

npm install @aeriondyseti/plugin-kit

Requires Node 20+. ESM-only. Mods don't install it: they use the plugin ($.kit) or a vendored copy, both below.

A minimal hook

#!/usr/bin/env node
import { PreToolUse, runHook } from '@aeriondyseti/plugin-kit';

runHook(() => {
    const input = PreToolUse.parse();
    const cmd = String((input.tool_input as { command?: unknown }).command ?? '');

    if (/\brm\b.*-rf?\s+\//.test(cmd)) {
        PreToolUse.emitOutput({
            decision: 'deny',
            reason: 'Refusing dangerous rm on the filesystem root.',
        });
    }

    PreToolUse.emitOutput({});
});

Wire it up in your settings.json:

{
    "hooks": {
        "PreToolUse": [
            {
                "matcher": "Bash",
                "hooks": [{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-tool-use.ts" }]
            }
        ]
    }
}

Running a .ts file directly needs Node 22.18+ (type stripping is on by default there). On older Node, compile the hook first or run it with tsx.

Events

One class per event, each with parse() and emitOutput(opts). Every emitOutput accepts the common options — toUser (shown to the user), continue: false + stopReason (halt Claude), suppressOutput, terminalSequence — plus the event-specific ones below. toClaude always means "add to Claude's context" (additionalContext); deny + reason always means "block this" (decision: "block").

| Event | Fires | Event-specific options | | ----- | ----- | ---------------------- | | PreToolUse | before a tool call | decision (allow/deny/ask/defer), reason, updatedInput, toClaude | | PermissionRequest | a permission dialog is about to show | decision: 'allow' + updatedInput/updatedPermissions, or decision: 'deny' + reason/interrupt | | PermissionDenied | auto mode denied a tool call | retry | | PostToolUse | after a tool call succeeds | deny, reason, toClaude, updatedToolOutput | | PostToolUseFailure | after a tool call fails | toClaude | | PostToolBatch | after a batch of parallel tool calls resolves | toClaude | | UserPromptSubmit | the user submits a prompt | deny, reason, toClaude, suppressOriginalPrompt, sessionTitle | | UserPromptExpansion | a slash command / MCP prompt expands | deny, reason, toClaude, suppressOriginalPrompt | | SessionStart | a session starts, resumes, clears, compacts or forks | toClaude, initialUserMessage, sessionTitle, watchPaths, reloadSkills | | SessionEnd | a session ends | — | | Setup | --init / --init-only / --maintenance | toClaude | | Stop | Claude finishes responding | deny, reason, toClaude | | StopFailure | a turn ends on an API error | — | | SubagentStart | a subagent spawns | toClaude | | SubagentStop | a subagent finishes | deny, reason, toClaude | | TeammateIdle | an agent-team teammate is about to idle | deny, reason | | TaskCreated | a task is created | deny, reason | | TaskCompleted | a task is marked complete | deny, reason | | PreCompact | before compaction | deny, reason | | PostCompact | after compaction | — | | PreModelSwitch | before a model change | decision (allow/deny/ask), reason | | PostModelSwitch | after a model change | toClaude | | Notification | Claude Code notifies the user | toClaude | | MessageDisplay | streamed assistant text is displayed | displayContent | | Elicitation | an MCP server asks the user for input | action, content, reason | | ElicitationResult | the user answered an elicitation | action, content, reason | | ConfigChange | a settings file or skill changes | deny, reason | | InstructionsLoaded | a CLAUDE.md / rules file loads | — | | CwdChanged | the working directory changes | watchPaths | | FileChanged | a watched file changes | watchPaths | | DirectoryAdded | a directory is added mid-session | — | | WorktreeCreate | a worktree is needed (replaces git worktree add) | worktreePath (required; printed as plain text) | | WorktreeRemove | a worktree is removed | — |

Each class's source file opens with a short note on when to reach for it.

Hook typings

In a hook script, every event's input and options are exported types, in Claude Code's own field names (snake_case input, camelCase options):

import { PreToolUse, type PreToolUseInput, type PreToolUseEmitOptions } from '@aeriondyseti/plugin-kit';

export function handle(input: PreToolUseInput): PreToolUseEmitOptions {
    if (input.tool_name === 'Bash' && String(input.tool_input.command).includes('rm -rf')) {
        return { decision: 'deny', reason: 'No rm -rf.' };
    }
    return {};
}

PreToolUse.emitOutput(handle(PreToolUse.parse()));

CommonHookInput, HookEventName / HOOK_EVENT_NAMES, PermissionMode, PermissionUpdate and the other shared types are exported too.

In a mod, don't use these: Claude Code types every hook itself. The same settings-hook events arrive as classic.<Event>, already typed from the claude-code declarations the engine lays beside your mod:

import type { Register } from 'claude-code';

export const register: Register = (on) => {
    on('classic.UserPromptSubmit', ($, e, next) => {
        // e.prompt, e.session_id, e.cwd, ... typed by Claude Code
        return next(e);
    });
};

One difference to know: in a mod, classic.PreToolUse carries the tool call in the engine's shape (e.tool, narrowed per tool), not the script's tool_name / tool_input.

The kit's own types reach a mod the same way: listing plugin-kit under dependencies lays its contract into your .claude-plugin/types/, so $.kit and its Kit* types are typed with nothing installed. A vendored kit brings its types with it (import type { Widgets } from './kit/index.ts').

Styled output

import { ICONS, OutputBuilder, PostToolUse } from '@aeriondyseti/plugin-kit';

const toUser = new OutputBuilder()
    .appendBox(`${ICONS.check} ${input.tool_name}`, { title: '● PostToolUse', color: 'green' })
    .appendTable(rows, { headers: ['key', 'value'], color: 'green' });

PostToolUse.emitOutput({ toUser });

Colors and modifiers also work via inline tags:

builder.appendLine('<color:"red"><bold>boom</bold></color>');

Available icons: check cross warn info arrow bullet dot star. Available colors: black red green yellow blue magenta cyan white gray. Available modifiers: bold dim italic underline.

Widgets

Small, typed pieces of state — text, counter, meter, clock, list, tags — with validation a model can learn from, and two ways to draw them.

import { parseWidget, renderWidgetLine, widgetJsonSchema } from '@aeriondyseti/plugin-kit/widgets';

const parsed = parseWidget('Health', { type: 'meter', value: 88 });
// { ok: false, error: 'Health: meter needs max: the value is drawn as a bar out of it' }

renderWidgetLine('Health', { type: 'meter', value: 88, max: 100 }); // 'Health 88/100'
widgetJsonSchema(); // a tool input schema whose descriptions are the catalog

In hook output, OutputBuilder draws them as plugin-kit does in a mod:

new OutputBuilder().appendWidgets({
    Health: { type: 'meter', value: 7, max: 10, color: 'red' },
    Suspicion: { type: 'clock', value: 2, of: 6 },
});
// Health ▰▰▰▰▰▰▰▱▱▱ 7/10
// Suspicion ◆◆◇◇◇◇

The subpath is pure (no Node, no dependencies), which is what lets a mod use it too, in either of two ways.

| | $.kit (plugin dependency) | Vendored | | --- | --- | --- | | Install | your users install plugin-kit (automatic with the allowlist) | nothing | | Look | the user's kit settings (glyphs, barWidth, listLimit) | whatever you pass | | Long lists | fold behind +N more; the kit answers the press | shown whole unless you track folding | | Calls | await $.kit.render(...) | describeWidgets(...), synchronous | | Updates | upgrade the plugin | re-run add-kit --vendor |

Both end in the same hydrate, which turns the kit's plain-JSON description into elements and gives every Button its onPress. Your own buttons get their handler by key: hydrate(tree, h, { save: () => ... }).

Widgets in a mod: $.kit

In your plugin's folder:

npx @aeriondyseti/plugin-kit add-kit

That adds { "name": "plugin-kit", "marketplace": "aeriondyseti-plugins" } to your plugin.json dependencies and copies hydrate.ts beside your hooks module. For installing your plugin to install the kit too, add "allowCrossMarketplaceDependenciesOn": ["aeriondyseti-plugins"] to your marketplace's marketplace.json. Then:

import type { Register } from 'claude-code';
import { hydrate } from './hydrate.ts';

export const register: Register = (on) => {
    on('ui.render', { component: 'Pane', requestId: 'stats' }, async ($, e, next) => {
        const { Box } = $.ui.resolve(e);
        const tree = await $.kit.render({
            id: `my-mod:${e.requestId}`,
            widgets: {
                Health: { type: 'meter', value: 88, max: 100, color: 'red', group: 'Body' },
                Clues: { type: 'list', value: ['a torn ticket', 'wet boots'] },
            },
        });
        return <Box>{hydrate(tree, h)}</Box>;
    });
};

$.kit also has line({ widgets }) for a status line, parse({ name, widget }) to validate a model's input, and catalog() for a prompt table and a tool schema. The plugin's README covers the rest: installing it, the user's settings, and restyling the kit from another plugin.

Widgets in a mod: vendored

In your plugin's folder:

npx @aeriondyseti/plugin-kit add-kit --vendor

That copies the widget code and hydrate into hooks/kit/ (with an index.ts to import from) and leaves plugin.json alone. Then:

import type { Register } from 'claude-code';
import { describeWidgets, hydrate, type Widgets } from './kit/index.ts';

export const register: Register = (on) => {
    on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
        const { Box } = $.ui.resolve(e);
        const widgets: Widgets = {
            Health: { type: 'meter', value: 7, max: 10, color: 'red' },
            Suspicion: { type: 'clock', value: 2, of: 6 },
        };
        const tree = describeWidgets(widgets, { id: 'my-mod:above', barWidth: 8 });
        return <Box>{hydrate(tree, h)}</Box>;
    });
};

The rest of the subpath is there too: parseWidget, loadWidgets, renderWidgetLine, widgetTable, widgetJsonSchema. Re-run the command after upgrading to refresh the copy.

Testing your hooks

import { describe, expect, it } from 'vitest';
import { PreToolUse } from '@aeriondyseti/plugin-kit';
import { mockPreToolUse, testHook } from '@aeriondyseti/plugin-kit/testing';
import { handle } from './pre-tool-use.js';

it('denies rm -rf', () => {
    const result = testHook(
        mockPreToolUse({ tool_name: 'Bash', tool_input: { command: 'rm -rf /' } }),
        () => handle(PreToolUse.parse()),
    );
    expect(result.wasDenied).toBe(true);
    expect(result.toClaude).toContain('rm');
});

TestHookResult carries normalized fields so you don't have to walk the payload yourself:

| Field | Meaning | | -------------- | -------------------------------------------------------------------------- | | wasDenied | a deny decision (any event) or top-level decision === 'block' | | wasAllowed | explicit allow, or no blocking/ask/defer signal at all | | wasAsked | permissionDecision === 'ask' | | wasDeferred | permissionDecision === 'defer' | | toUser | payload.systemMessage | | toClaude | additionalContext → deny reason → top-level reason |

Negative paths (malformed stdin, wrong hook_event_name) surface as a thrown HookParseError:

expect(() => testHook(wrongEvent, () => PreToolUse.parse())).toThrow(HookParseError);

Tooling

npx @aeriondyseti/plugin-kit new hook PreToolUse   # a hook + test + settings entry
npx @aeriondyseti/plugin-kit new mod my-mod --kit  # a mod that validates and passes its tests
npx @aeriondyseti/plugin-kit doctor                # what will break it, and the fix
npx @aeriondyseti/plugin-kit types                 # Claude Code's mod types, for CI tsc
npx @aeriondyseti/plugin-kit release minor         # bump, CHANGELOG, commit, tag (never pushes)

See Tooling.

Fixtures and mod tests

Record what real sessions send, then replay it in seconds. Add plugin-kit record as a command hook (it saves each payload under .claude/fixtures/<Event>/ and never changes what Claude does), then:

plugin-kit run "node .claude/hooks/pre-tool-use.ts" .claude/fixtures
# ✓ .claude/fixtures/PreToolUse/20261006-145042-Bash.json  PreToolUse  denied  60ms
#     to Claude: refused: rm -rf /

In unit tests, loadFixtures(dir) feeds testHook. For mods, plugin-kit fixtures .claude/fixtures --out tests/fixtures.ts makes them importable, and plugin-kit vendor testing adds helpers for claude plugin test: mountTarget (valid component props), kitStub (a stand-in for the plugin-kit plugin) and replayAll($, fixtures). See Fixtures and Testing.

Hooks in mods

@aeriondyseti/plugin-kit/adapter runs hook logic written for scripts in a mod. Vendor it (plugin-kit vendor adapter), and one policy serves both:

// a policy: handle(input) => emit options, typed by this package
on('classic.UserPromptSubmit', ($, e, next) => toClassic('UserPromptSubmit', guardPrompt(e)) ?? next(e));
on('classic.PreToolUse', ($, e, next) => toClassic('PreToolUse', guardBash(fromToolCall(e))) ?? next(e));

fromCommandOutput runs an existing script unchanged through $.process.run. See Hooks in Mods.

Status line

{ "statusLine": { "type": "command", "command": "plugin-kit statusline" } }
Opus 5.5 · plugin-kit · main · #42
ctx ▰▰▰▰▰▰▰▱▱▱ 72% · 5h 38% · week 91% · $1.23 · +120 -34

Configure it in .claude/statusline.json from 25 sources (model, context, usage windows, cost, git, PR, cache, ...), each drawn as a widget with labels, colors and warn/alert thresholds; plugin-kit statusline --check previews and lints a config. In code, composeStatusLine takes your own sources too. See Status Line.

Examples

Runnable dogfood hooks with colocated tests live in examples/hooks/. Each hook exports a pure handle(input) function so its policy is testable without touching stdin, with an import.meta.url guard that drives the real parse/emit only when run as a script.

| Hook | Shows | | ---------------------------------------------------------------- | ----------------------------------------------------------- | | pre-tool-use.ts | Deny branch, OutputBuilder with box + table, icons | | pre-tool-use-advanced.ts | decision: 'ask' and updatedInput rewrites | | post-tool-use.ts | Binary deny: true, toClaude context injection | | user-prompt-submit.ts | Prompt-level deny, divider + list | | session-start.ts | Context-injection pattern (no deny concept) |

Project direction

  • docs/ — the full documentation, published to the wiki with npm run docs:wiki.

  • ROADMAP.md — features under consideration for future releases.

  • CHANGELOG.md — release history, Keep a Changelog format.

  • TECH-DEBT.md — known shortcuts and the context behind them, so contributors know what's intentional vs. what's waiting.

License

MIT.