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.
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.
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.
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.
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.
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.
HookType enum across every agent.no build step, no scaffold, no framework. just node, a plugin file, and a settings entry.
plugsdk is a single npm package. zero runtime dependencies. esm-only, node 18+.
one object: name, hooks, tools. handlers receive a normalized event shape regardless of which agent fired it.
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.
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.
a guardrail that blocks rm -rf / across claude code and opencode.
compare what you'd write by hand against what plugsdk gives you.
// 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') } } } }
// 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.
logs every tool call, blocks dangerous shells, exposes a custom env-reader tool — across every agent. copy this file, name it, ship it.
every meaningful event an agent emits, normalized to a single canonical name. filled dot = the agent supports it. empty dot = adapter falls back gracefully.
each adapter implements 5 methods. add a new one in an afternoon — every plugin written against plugsdk works with it on day one.
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.
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.
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.
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.
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.
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.
one hook file. every editor your team uses. every agent that ships next.