ReviewOS

also looking at this

stacks/buddy

fix(setup): make custom plugins able to run at all

#1416
Merged glennmichael123 wants to merge fix/json-plugins into main
5 files +401 -31
9 days ago

loadCustomPlugins read .buddy/plugins/*.json, called JSON.parse, and pushed the result straight onto the plugin list:

const pluginConfig = JSON.parse(fs.readFileSync(path.join('.buddy/plugins/', file), 'utf8'))
plugins.push(pluginConfig)

SetupHook.handler is typed (context: SetupContext) => Promise<void> | void. JSON has no way to carry a function.

So every custom plugin loaded, announced itself with 🔌 Loaded plugin, and then failed on each hook with hook.handler is not a function — caught by the hook runner and logged as an ordinary hook failure.

What it was meant to be

The documented example was never executable, and didn't pretend otherwise:

"handler": "// Custom JavaScript function"

Its configuration block held webhook_url and api_key, and all three built-in plugins post webhooks. So what a JSON plugin was for isn't in doubt. Hooks now declare an action:

"action": { "type": "webhook", "url": "https://…", "body": { "channel": "#builds" } }

Declarative rather than executable, deliberately. Reading a string out of a config file and running it would turn any .buddy/plugins/*.json committed to a repository into arbitrary code execution on whoever runs buddy setup.

handler stays for the built-in plugins, which are TypeScript and supply a real function. A hook with neither is skipped with a warning rather than a TypeError.

Validation and payload

Plugin files are validated as they load, and a rejection names the file and the problem. A plugin still written the old way is told what to write instead:

⚠️ Ignoring plugin custom.json: hook "notify" declares a handler as text. JSON cannot carry a function — use "action": { "type": "webhook", "url": "..." } instead.

The payload is a fixed shape — the event and the repository, plus whatever body adds. Setup is holding a token by the time hooks run, and the URL is chosen by a file in the repository, so nothing else on the context goes out. There's a test asserting the token can't appear in it.

Why it survived

None of this had coverage. test/plugin-architecture.test.ts builds a plugin object, calls loadPlugin, then asserts on the object it just built:

// Create custom plugin configuration (without handler function since it can't be serialized)
...
expect(customPlugin.name).toBe('custom-integration')

That comment is the defect — worked around rather than surfaced. loadCustomPlugins was never called by a test at all. It is now, against real files on disk.

Docs

Corrected in README.md, in docs/guide/configuration.md — which showed a plugin with no hooks key whatsoever — and in CLAUDE.md, whose embedded copy is auto-loaded as project instructions, so a wrong example there misleads every future run.

Tests

2034 → 2047.

Lint ✓ · typecheck ✓ · check:docs ✓ · 2047 pass / 0 fail

🤖 Generated with Claude Code

https://claude.ai/code/session_01UWouahBJT3msK7V2VDYY6T

5 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.