DocsPlugins

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:

  1. Named export instead of default. export const plugin = {...} fails validation.
  2. A misspelled hook name. "tool.before" is not a hook; unknown keys are ignored without a warning.
  3. No restart. Plugins load once at boot. Editing the file changes nothing until you restart.
  4. plugin_config keyed by filename. It's keyed by the plugin's id, and a mismatch just delivers config: undefined.
  5. Registered in the project config. .podcode/config.json has 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 lineWhat happenedFix
plugin: import failedThe module didn't import — bad path, syntax error, or an unresolvable dependencyCheck 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 serverUse export default, and check both fields
plugin: server() threwserver() raised during init. The plugin is skipped entirelyThe error object is in the log line
plugin: tool hook threw at initserver() succeeded but the tool hook threw. The plugin loads with no toolsWrap tool construction, or move failure-prone setup into server()
plugin: hook threwA hook threw at runtime. Contribution dropped, pipeline continuesNothing breaks, but your hook did nothing that call
exceeded 5000ms; dropping its contributionA single hook call ran past the 5s boundCache it, or move the work off the hot path
plugin: replacing duplicate idTwo plugins share an id. Last one loaded winsRename one
plugin: refusing to override protected built-in toolA tool id collided with a security-gated built-inRename your tool
plugin: bootstrap failed; continuing without pluginsThe whole load blew its 30s budget. No plugins are activeFind 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:

Shell
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.