DocsPlugins

Configuration

Three keys control plugins, and all three live in the global config at ~/.config/podcode/config.json.

~/.config/podcode/config.json
{
  "plugin": [
    "/Users/you/.config/podcode/plugins/tool-log.mjs",
    "@acme/podcode-deploy"
  ],
  "plugin_config": {
    "tool-log": { "level": "warn" },
    "acme-deploy": { "endpoint": "https://deploy.internal" }
  },
  "plugin_autodiscover": false
}

plugin

An array of module specifiers, loaded in order. That order is also the hook-pipeline order, so a plugin later in the array sees the values earlier ones returned.

How a specifier is resolved depends entirely on how the string starts:

Specifier looks likeTreated asResolved how
./x.mjs, /abs/x.mjs, C:\x.mjsFile pathResolved against the server's working directory, converted to a file:// URL, imported
anything elsenpm specifierHanded straight to import(), resolved by Node from the server's runtime

Two consequences worth internalizing:

Use absolute paths. A relative specifier resolves against wherever the server happened to start, not against the config file. That directory varies, and a plugin that loads from one launch and vanishes from the next is a miserable thing to debug.

Nothing installs npm packages for you. Adding @acme/podcode-deploy to the array records the name; it does not fetch anything. A bare specifier only works if the package is already resolvable from the server process — the Settings UI reads <server cwd>/node_modules/<pkg>/package.json to show you a version, and shows none when it can't find one. For plugins you write yourself, a local file path is the practical route.

plugin_config

A map of plugin id → an arbitrary object, delivered to that plugin as the config field of server()'s second argument.

JSON
"plugin_config": {
  "tool-log": { "level": "warn", "file": "/tmp/tools.log" }
}

The key is the plugin's id — the string in its default export. Not the filename, not the package name. A mismatch here is quiet: the plugin loads normally and simply receives config: undefined, which is also what a plugin with no block at all receives.

plugin_autodiscover

Off by default. When set to true, the server additionally loads every .js, .mjs, .cjs, or .ts file it finds in:

<server cwd>/.podcode/plugin/
<server cwd>/.podcode/plugins/

Discovered files load alphabetically, after everything in the plugin array. Each one is logged before it is imported, so an unexpected workspace plugin is visible in the log rather than silent.

Note that discovery uses the server's cwd, not the workspace you have open. A project's .podcode/plugins/ directory is not picked up unless the server was started in that project. Plugins load once, globally, and are shared by every workspace and session.

The workspace trust boundary

A project's .podcode/config.json ships inside the repository, which makes it attacker-controlled for any repo you didn't write. Six keys are therefore stripped from the project layer before it merges, and honored only from the global config:

plugin                 arbitrary in-process code load
plugin_autodiscover    a repo could otherwise flip its own trust gate
plugin_config          feeds configuration to loaded plugins
agent                  custom agents define tool + permission profiles
agent_autodiscover     same self-flipping problem
operator_uid           a repo must never claim operator identity

What this means in practice: a plugin cannot ship with a project. You cannot check a plugin into a repo and have teammates pick it up by cloning. Each operator registers plugins in their own global config, deliberately. Whatever the project's config file says about plugins is discarded, and the drop is logged.

Configuring from the app

Settings → Plugins writes the same keys. The Add Plugin form takes an npm package name, the config editor edits that plugin's plugin_config block, and removing a plugin takes it out of the plugin array.

Every one of those changes triggers an automatic server restart — the server is unavailable for a few seconds while it comes back.

Restarting

Plugins load exactly once, at boot. Editing the config or the plugin file changes nothing in a running server. Restart from Settings → Plugins, or from the CLI:

Shell
podcode

Environment variables

VariableDefaultWhat it does
PODCODE_PLUGIN_LOAD_TIMEOUT_MS30000Wall-clock bound on loading all plugins at boot. On expiry the load is abandoned and the server starts with built-ins only

The per-hook timeout — 5 seconds, applied to every individual hook call — is fixed and not configurable.