Troubleshooting
Nearly every plugin problem presents the same way: nothing happens. The system is deliberately fail-soft — a broken plugin is removed, not escalated — so the log is where the explanation lives.
Start here
Almost every "my plugin doesn't work" turns out to be one of these five:
- Named export instead of default.
export const plugin = {...}fails validation. - A misspelled hook name.
"tool.before"is not a hook; unknown keys are ignored without a warning. - No restart. Plugins load once at boot. Editing the file changes nothing until you restart.
plugin_configkeyed by filename. It's keyed by the plugin'sid, and a mismatch just deliversconfig: undefined.- Registered in the project config.
.podcode/config.jsonhas its plugin keys stripped. Registration only counts in~/.config/podcode/config.json.
Reading the log
Every failure below logs exactly one line. Search the server log for plugin:.
| Log line | What happened | Fix |
|---|---|---|
plugin: import failed | The module didn't import — bad path, syntax error, or an unresolvable dependency | Check the path and try node --input-type=module -e 'import("<abs path>")' |
plugin: module has no valid default export {id, server} | It imported, but the default export isn't an object with a string id and a function server | Use export default, and check both fields |
plugin: server() threw | server() raised during init. The plugin is skipped entirely | The error object is in the log line |
plugin: tool hook threw at init | server() succeeded but the tool hook threw. The plugin loads with no tools | Wrap tool construction, or move failure-prone setup into server() |
plugin: hook threw | A hook threw at runtime. Contribution dropped, pipeline continues | Nothing breaks, but your hook did nothing that call |
exceeded 5000ms; dropping its contribution | A single hook call ran past the 5s bound | Cache it, or move the work off the hot path |
plugin: replacing duplicate id | Two plugins share an id. Last one loaded wins | Rename one |
plugin: refusing to override protected built-in tool | A tool id collided with a security-gated built-in | Rename your tool |
plugin: bootstrap failed; continuing without plugins | The whole load blew its 30s budget. No plugins are active | Find the slow one; raise PODCODE_PLUGIN_LOAD_TIMEOUT_MS only as a stopgap |
Symptom → cause
Nothing in the log at all, not even a warning. The specifier never reached the loader. Either plugin is in the project config rather than the global one, or the JSON has a typo that dropped the key. Confirm the array is in ~/.config/podcode/config.json.
The plugin logs at boot, but my hook never runs. The hook key is misspelled. Unknown keys are ignored silently, so this failure is invisible by design. The four exact strings are tool, chat.params, tool.execute.before, and tool.execute.after.
config is undefined inside server(). plugin_config is keyed by the plugin's id, not the filename or package name. Log plugin.id and compare it to the key character for character.
My tool.execute.before throws, and the tool runs anyway. Working as implemented. Hook throws are caught and the pipeline continues with the pre-throw value. There is no cancellation mechanism in the hook API — see the note in the hook reference.
My tool doesn't appear to the model. Either the tool hook threw at init (check for tool hook threw at init), it returned something that isn't an array — anything else is ignored — or the id collided with a protected built-in and was refused.
It worked yesterday and doesn't today. A relative specifier resolves against the server's working directory, which changes with how you launched it. Switch to an absolute path.
My .ts plugin won't import. .ts passes the auto-discovery filename filter, but whether it actually imports depends on your Node runtime's type-stripping support. Ship .mjs, or compile to .js before registering it.
maxOutputTokens from chat.params has no effect. It's clamped to the model's real output limit afterwards. Lowering works; raising past the ceiling does not.
Isolating a plugin
The pipeline runs in load order, so bisecting is quick. Empty the plugin array, restart, and confirm the behavior is gone. Add plugins back one at a time — the first one that reproduces it is your culprit, and if two behave differently together than apart, they're fighting over the same value in the pipeline.
To check a module in isolation without a server:
node --input-type=module -e 'const m = await import("/abs/path/plugin.mjs"); console.log(m.default?.id, typeof m.default?.server)'Printing your id and function means it will pass validation. Anything else — including undefined undefined — is exactly what the loader would have rejected.