Plugins (Beta)
Beta Feature
The plugin system is currently in Beta. The implementation details and configuration definitions may change in future releases. Please use with caution in production environments and stay tuned for updates.
The plugin system allows you to add custom tools to Pythinker Code, extending the AI's capabilities. Unlike MCP servers, plugins are lightweight local toolkits ideal for packaging project-specific scripts and utilities.
What are plugins
A plugin is a directory containing a plugin.json file. Plugins can declare multiple "tools," where each tool is an executable command (Python, TypeScript, shell script, etc.) that the AI can invoke to perform specific tasks.
For example, you can create a plugin to:
- Wrap internal API call scripts
- Provide project-specific code generation tools
- Integrate with proprietary services or database queries
Difference between plugins and Agent Skills:
- Skills: Provide knowledge-based guidance through
SKILL.md; the AI reads and follows the specifications - Plugins: Declare executable tools through
plugin.json; the AI can directly invoke tools to get results
Installing plugins
Use the pythinker plugin command to manage plugins.
Install from a local directory
pythinker plugin install /path/to/my-pluginInstall from a ZIP file
# Local ZIP file
pythinker plugin install my-plugin.zip
# Remote ZIP URL (including GitHub/GitLab archive download links)
pythinker plugin install https://example.com/my-plugin.zip
pythinker plugin install https://github.com/user/repo/archive/refs/heads/main.zipInstall from a Git repository
# Install the root plugin
pythinker plugin install https://github.com/user/repo.git
# Install a plugin from a subdirectory (multi-plugin repo)
pythinker plugin install https://github.com/user/repo.git/plugins/my-plugin
# Specify a branch (use browser-style GitHub URL without .git)
pythinker plugin install https://github.com/user/repo/tree/develop/plugins/my-pluginWhen a Git repository has no plugin.json at the root, Pythinker Code checks the root and its immediate subdirectories, then lists available plugins for you to choose from.
List installed plugins
pythinker plugin listView plugin details
pythinker plugin info my-pluginRemove a plugin
pythinker plugin remove my-pluginCreating a plugin
Creating a plugin requires three steps:
- Create a directory
- Write a
plugin.jsonfile - Implement the tool scripts
Directory structure
my-plugin/
├── plugin.json # Plugin configuration (required)
├── config.json # Plugin config (optional, for credential injection)
└── scripts/ # Tool scripts
├── greet.py
└── calc.tsplugin.json format
{
"name": "my-plugin",
"version": "1.0.0",
"description": "My custom plugin for project X",
"config_file": "config.json",
"inject": {
"api_key": "api_key",
"endpoint": "base_url"
},
"tools": [
{
"name": "greet",
"description": "Generate a greeting message",
"command": ["python3", "scripts/greet.py"],
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name to greet"
}
},
"required": ["name"]
}
}
]
}Field descriptions
| Field | Description | Required |
|---|---|---|
name | Plugin name; lowercase letters, numbers, and hyphens only | Yes |
version | Plugin version; semantic version format | Yes |
description | Plugin description | No |
config_file | Config file path for credential injection | No |
inject | Credential injection mapping; key is target path, value is source variable name | No |
tools | List of tools | No |
Tool field descriptions
| Field | Description | Required |
|---|---|---|
name | Tool name | Yes |
description | Tool description | Yes |
command | Command to execute; array of strings | Yes |
parameters | Parameter definition in JSON Schema format | No |
Credential injection
If your plugin needs to call LLM APIs, you can use the inject configuration to automatically receive Pythinker Code's credentials.
inject configuration example
{
"config_file": "config.json",
"inject": {
"llm.api_key": "api_key",
"llm.endpoint": "base_url"
}
}Supported injection variables
| Variable | Description |
|---|---|
api_key | LLM provider API key; supports OAuth tokens and static API keys |
base_url | LLM API base URL |
config.json template
{
"llm": {
"api_key": "",
"endpoint": ""
}
}During installation, Pythinker Code injects the currently configured API key and base URL into the specified config file. If OAuth is configured, a valid token is automatically obtained and injected. Later, when the application starts, Pythinker Code will also try to write the latest credentials (such as the refreshed OAuth token) into the configuration file of the installed plugin.
TIP
Generally, there is no need to reinstall the plugin in order to update credentials: after switching the LLM provider or re-authorizing, restarting Pythinker Code will automatically refresh the credentials in the configuration file. The plugin tool will also obtain the currently valid credentials through environment variables when it is actually run. The plugin needs to be reinstalled only when the configuration structure of the plugin itself (such as config_file or inject mapping) is modified.
About inject keys
The keys under inject (for example, llm.api_key) are also exposed as environment variable names to your plugin tool subprocesses. Because these names contain dots, some runtimes cannot access them using the usual identifier syntax (for example, $llm.api_key is not valid in POSIX shells), but you can still read them via map/dictionary access, such as:
- Node.js:
process.env["llm.api_key"] - Python:
os.environ["llm.api_key"]
If you prefer env-var-friendly names that work smoothly across shells and tooling, consider using keys like LLM_API_KEY or LLM_ENDPOINT in your own plugins instead of dotted names, and structure your config file accordingly.
Tool script specification
Tool scripts receive parameters via standard input and return results via standard output.
Input format
Scripts receive a JSON object from stdin:
{
"name": "World"
}Output format
Content written to stdout by the script is returned to the Agent as a string. If structured output is needed, emitting JSON text is recommended:
{
"content": "Hello, World!"
}Python example
#!/usr/bin/env python3
import json
import sys
params = json.load(sys.stdin)
name = params.get("name", "Guest")
result = {"content": f"Hello, {name}!"}
print(json.dumps(result))TypeScript example
#!/usr/bin/env tsx
import * as readline from "readline";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
let input = "";
rl.on("line", (line) => {
input += line;
});
rl.on("close", () => {
const params = JSON.parse(input);
const name = params.name || "Guest";
console.log(JSON.stringify({ content: `Hello, ${name}!` }));
});Complete example
{
"name": "sample-plugin",
"version": "1.0.0",
"description": "Sample plugin demonstrating Skills + Tools",
"tools": [
{
"name": "py_greet",
"description": "Generate a greeting message (Python tool)",
"command": ["python3", "scripts/greet.py"],
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name to greet"
},
"lang": {
"type": "string",
"enum": ["en", "zh", "ja"],
"description": "Language"
}
},
"required": ["name"]
}
},
{
"name": "ts_calc",
"description": "Evaluate a math expression (TypeScript tool)",
"command": ["npx", "tsx", "scripts/calc.ts"],
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate"
}
},
"required": ["expression"]
}
}
]
}Plugin installation location
Plugins are installed in the ~/.pythinker/plugins/ directory. Each plugin is an independent subdirectory containing the complete plugin.json and script files.
Note
Plugins and MCP servers are complementary extension mechanisms:
- MCP: Suitable for services that need to run continuously, complex tool orchestration, or cross-process communication
- Plugins: Suitable for simple script wrappers, project-specific tools, or rapid prototyping
Marketplace plugins (Claude/Codex compatible)
In addition to the script-tool plugins above, Pythinker can install and activate artifact plugins from marketplaces — the same plugin format used by Claude Code (.claude-plugin/plugin.json) and Codex. An artifact plugin contributes skills, subagents, slash commands, hooks, and MCP servers to a session.
Marketplaces
A marketplace is a catalog (marketplace.json) listing plugins and their sources. Manage marketplaces with pythinker plugin marketplace:
# Add a marketplace (GitHub owner/repo, git/URL, or a local path)
pythinker plugin marketplace add anthropics/claude-plugins-official
pythinker plugin marketplace add /path/to/local/marketplace --name local
# List, refresh, or remove
pythinker plugin marketplace list
pythinker plugin marketplace refresh claude-plugins-official
pythinker plugin marketplace remove localInstalling marketplace plugins
# Install a plugin from a configured marketplace
pythinker plugin marketplace install ponytail@claude-plugins-official
# (equivalent positional form)
pythinker plugin marketplace install ponytail claude-plugins-official
pythinker plugin marketplace installed # list installed marketplace plugins
pythinker plugin marketplace uninstall ponytail@claude-plugins-officialPlugins install into ~/.pythinker/plugins/cache/<marketplace>/<plugin>/<version>/. If the same plugin or marketplace is already present in a Claude Code (~/.claude/plugins) or Codex (~/.codex/plugins) install, Pythinker symlinks to it instead of copying — no redundant downloads.
Activation policy
Pythinker auto-detects plugins installed for Claude Code (~/.claude/plugins) and Codex (~/.codex/plugins) — no symlink or manual copy needed. Their safe artifacts (skills, commands, agents) activate automatically because they are model-invoked, never auto-run; their executable artifacts (hooks, MCP servers) auto-execute, so they stay opt-in. Detection reads the plugins in place (no copy/symlink) and de-duplicates by name, so there is no redundancy.
Tune this via [plugins] in ~/.pythinker/config.toml:
[plugins]
# Auto-detect Claude/Codex plugins' skills, commands, and agents. On by default.
# Set false to ignore external plugins entirely.
discover_external = true
# Also run external plugins' hooks and MCP servers (they auto-execute). Opt-in.
external_exec = false
# Empty enables all discovered plugins; a non-empty list enables only those named
# (by "name" or "name@marketplace").
enabled = []
# Plugins to turn off by name. Excluded even when `enabled` would allow them —
# this is how `pythinker plugin disable <name>` works under the all-on default.
disabled = []Toggle plugins without editing the file or uninstalling them:
pythinker plugin disable ponytail # adds to [plugins].disabled
pythinker plugin enable ponytail # removes it againPlugin-contributed hook and MCP commands may reference the plugin's own directories via ${CLAUDE_PLUGIN_ROOT} / ${PYTHINKER_PLUGIN_ROOT} (the versioned install dir) and ${CLAUDE_PLUGIN_DATA} / ${PYTHINKER_PLUGIN_DATA} (a persistent per-plugin data dir); both are expanded on load.
Plugin dependencies
A plugin may declare dependencies in its plugin.json ("name" or "name@marketplace"). Installing a plugin from a marketplace also installs its transitive dependencies from the same marketplace; cross-marketplace dependencies are blocked (install them from their own marketplace first). At load time, a plugin whose dependencies are not present and enabled is disabled, so it never half-activates.
Plugin options (userConfig)
A plugin may declare userConfig options and reference them as ${user_config.KEY} in its MCP server configs and hook commands. Provide values per plugin in config:
[plugins.options.my-plugin]
api_base = "https://example.test"An MCP server or hook that references an option with no configured value is skipped (it never runs with a blank), and the value is filled in on load.
Not yet ported
${user_config.KEY} substitution in skill/agent/command content, the PYTHINKER_PLUGIN_OPTION_* hook environment variables, keychain-backed storage for sensitive options, and an interactive enable-time prompt are not implemented yet — they require changes outside the plugin subsystem. Today, option values (including any marked sensitive) are read from config.