plugsdk
universal plugin sdk · live on npm

write a plugin once.
deploy it everywhere.

one hook file, every ai agent. plugsdk normalizes 18 hook types across claude code, opencode, windsurf — and whatever ships next. no rewrites, no fork-per-agent, no lost weekends translating json shapes.

show me how source on github
3 adapters live 18 hook types mit license zero deps
$ npm install plugsdk
v1.0.x
·
11.9 kB tarball
when you reach for it

three moments
you'll thank yourself.

every team hits these walls within the first week of building agent plugins. plugsdk is the answer you wish you'd had on day zero.

shipping a guardrail

you need to block rm -rf / in every editor your team uses.

your devs are split across claude code, cursor, windsurf, opencode. one shell-execute hook should cover all four — but each tool wants its own hook shape.

we wrote it once in plugsdk. four agents covered before lunch.

auditing tool calls

compliance asks: what touched production yesterday?

you need every pre_tool_use event, with session id and input payload, written to your audit log — regardless of which agent fired it.

one normalized event shape feeding one log pipeline. legal stopped emailing.

exposing a custom tool

your internal API needs an mcp-style tool for every agent — including the ones not built yet.

define tools: { ... } once. new adapter ships → your tool is already wired. no migration, no rewrite.

future-proofing tool surface is the real benefit. that's the lock-in killer.

why it matters

the agent landscape
is fracturing.

every agent vendor is shipping their own hook format, their own tool registration shape, their own settings file. write a plugin for one, you've written it for one. that's a tax on every line of plugin code you ship.

without plugsdk

you build it n times.

  • 01relearn each agent's hook lifecycle from scratch.
  • 02maintain 4 codebases for the same logical feature.
  • 03diverge over time as each fork rots independently.
  • 04watch a new agent ship and start the loop again.
  • 05ship anything else this quarter.
with plugsdk

you build it once.

  • 01one canonical HookType enum across every agent.
  • 02adapters translate i/o shape — your code stays clean.
  • 03auto-detect which agent is calling at runtime.
  • 04new agent ships → write a 60-line adapter, ship same plugin.
  • 05get back to writing the actual feature.
1 plugin
written, not three. tested, not three.
// reduce surface
3 adapters
claude · opencode · windsurf shipping today.
// cover the field
18 hooks
every event the agent emits, normalized.
// no gaps
~60 lines
to add a brand-new agent adapter.
// future-proof
how you wire it

four steps,
five minutes.

no build step, no scaffold, no framework. just node, a plugin file, and a settings entry.

01

install once.

plugsdk is a single npm package. zero runtime dependencies. esm-only, node 18+.

npm install plugsdk
02

define your plugin.

one object: name, hooks, tools. handlers receive a normalized event shape regardless of which agent fired it.

import { definePlugin, HookType } from 'plugsdk'
03

pick an adapter.

each agent gets a thin runner that handles its native i/o protocol — stdin/stdout for claude, json files for opencode, extension api for windsurf.

import { runClaudeHook } from 'plugsdk/adapters/claude'
04

wire it once.

point the agent's settings file at your script. the runner auto-detects which hook type fired and dispatches to your handler. you never see the wire format.

// .claude/settings.json → hooks → command: "node my-plugin.js"
before · after

same feature.
half the code.

a guardrail that blocks rm -rf / across claude code and opencode. compare what you'd write by hand against what plugsdk gives you.

before · two files, two protocols
// claude-guard.js — reads stdin, writes stdout
const input = JSON.parse(await readStdin())
if (input.tool_name === 'Bash') {
  const cmd = input.tool_input?.command ?? ''
  if (/rm\s+-rf\s+\//.test(cmd)) {
    process.stdout.write(JSON.stringify({
      hookSpecificOutput: {
        permissionDecision: 'deny',
        permissionDecisionReason: 'dangerous rm'
      }
    }))
    process.exit(0)
  }
}

// opencode-guard.js — different shape entirely
export default {
  hooks: {
    'tool.execute.before'(input, output) {
      if (input.tool === 'bash' &&
          /rm\s+-rf\s+\//.test(input.args?.command)) {
        throw new Error('dangerous rm')
      }
    }
  }
}
after · one plugin, every agent
// guard.js — works everywhere
import { definePlugin, HookType } from 'plugsdk'
import { runClaudeHook } from 'plugsdk/adapters/claude'

const plugin = definePlugin({
  name: 'shell-guard',
  hooks: {
    [HookType.SHELL_EXECUTE]: async (event) => {
      const cmd = event.input?.command ?? ''
      if (/rm\s+-rf\s+\//.test(cmd)) {
        return { behavior: 'block', reason: 'dangerous rm' }
      }
      return { behavior: 'allow' }
    }
  }
})

runClaudeHook(plugin)
// same plugin → swap adapter for opencode/windsurf.
full example

an audit-logger,
start to finish.

logs every tool call, blocks dangerous shells, exposes a custom env-reader tool — across every agent. copy this file, name it, ship it.

my-plugin.js

  
.claude/settings.json — wire it once

  
the hook surface

18 events,
one vocabulary.

every meaningful event an agent emits, normalized to a single canonical name. filled dot = the agent supports it. empty dot = adapter falls back gracefully.

claude code
opencode
windsurf
·
tool · session · file · chat · failure · agent
where it ships

three adapters live.
the rest is one PR away.

each adapter implements 5 methods. add a new one in an afternoon — every plugin written against plugsdk works with it on day one.

straight answers

things you'll
actually ask.

+ is this just an abstraction layer i'll regret?

no. plugsdk is ~1,100 lines total, zero runtime deps, and every adapter is <100 lines you can read in one sitting. the canonical event shape is a thin object (toolName, input, sessionId, …) that maps cleanly onto every agent's native shape — there's no clever runtime, no plugin registry, no metaprogramming. if an adapter ever rots, you swap it for your own and your plugin code doesn't change.

+ what happens when an agent emits a hook the others don't have?

the canonical HookType covers every event we've seen across the field. if claude has pre_compact and opencode doesn't, the opencode adapter never dispatches that hook — your handler simply doesn't fire there. no synthetic events, no fake fallbacks. you can also branch on event.agent if you genuinely need agent-specific behavior.

+ why not just use mcp?

mcp is great for tools — exposing capabilities to an agent. plugsdk's tools: field maps cleanly to mcp where the agent supports it. but mcp doesn't cover hooks — the agent's lifecycle events, permission decisions, file-edit interception, session tracking. that's the gap plugsdk fills. use both: tools via mcp where supported, hooks via plugsdk everywhere.

+ how do i add a custom adapter?

implement AgentAdapter — five methods: detect(), getHookType(), translateHookInput(), translateHookOutput(), getToolPayload(). the existing claude/opencode/windsurf adapters are your reference. typical adapter is 60–80 lines. once it's written, every plugin already in the ecosystem works on your new agent — that's the whole point.

+ production-ready?

the runtime is. it's a single class with no external state and an fs-test shape that you can pin and audit. the adapter set is the moving part — claude and opencode are stable; windsurf is beta because the extension api still shifts. version-pin in your package.json like anything else, watch the changelog when you upgrade.

the only abstraction
worth shipping is the
one you'd write yourself.

plugsdk is what you'd build by hand on the second project, after writing the same hook three times and noticing. we just wrote it once so you don't have to. small core, thin adapters, no magic.

every line is on github. every adapter is <100 lines. nothing imports a runtime you can't read in a sitting. if a shape breaks, fork the adapter — your plugin code stays put.

the goal isn't lock-in. it's unlock.

ship the plugin.
ignore the agent.

one hook file. every editor your team uses. every agent that ships next.

read the four steps npm ↗ github ↗