Write a plugin

A plugin is a versioned directory. Use a skill for instructions, MCP for tools, or an extension for sidebar pages, themes, UI patches and lifecycle hooks. Extensions can include MCP tools in the same package. Channel plugins run as separately managed services and need host integration.

A skill package

plugins/writing-style/
  shinbot-plugin.json
  SKILL.md
  LICENSE
{
  "schema_version": 1,
  "id": "writing-style",
  "name": "Writing style",
  "version": "1.0.0",
  "description": "Apply a personal writing guide.",
  "kind": "skill",
  "license": "MIT",
  "files": ["SKILL.md", "LICENSE"]
}

Write clear instructions in SKILL.md: when to use the skill, the information it needs, the steps to follow and how to check the result. Skills load on demand. Include a complete license in LICENSE.

An MCP tool

The Python runner has the standard library and MCP dependencies available. This example normalizes whitespace without touching files or the network.

server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Text tools")


@mcp.tool()
def normalize(text: str) -> str:
    """Collapse repeated whitespace in text."""
    return " ".join(text.split())


if __name__ == "__main__":
    mcp.run()

shinbot-plugin.json:

{
  "schema_version": 1,
  "id": "text-tools",
  "name": "Text tools",
  "version": "1.0.0",
  "description": "Normalize whitespace in text.",
  "kind": "mcp",
  "license": "MIT",
  "files": ["server.py", "LICENSE"],
  "entrypoint": "server.py",
  "tools": [{
    "name": "normalize",
    "description": "Collapse repeated whitespace in text.",
    "input_schema": {
      "type": "object",
      "properties": {"text": {"type": "string"}},
      "required": ["text"]
    }
  }]
}

Activation checks the running MCP server’s tool list against the manifest. Tool names must be unique lowercase identifiers. The namespaced form plg__<plugin-id>__<tool-name> must fit within 64 characters.

The files list must match the package exactly, excluding the manifest itself. Packages cannot contain symlinks, hidden files or paths outside the directory. The package limit is 2 MB, with at most 100 declared files and 100 tools.

Credentials and settings

Declare a configuration array when a tool needs an API token or an account setting:

"configuration": [
  {
    "key": "SHINBOT_PLUGIN_EXAMPLE_TOKEN",
    "label": "API token",
    "description": "Create a token for the account you want to connect.",
    "secret": true,
    "required": true
  }
]

Keys must begin with SHINBOT_PLUGIN_ and use uppercase letters, digits and underscores. The UI and shinbot plugins configure PLUGIN VERSION prompt for these fields. Required values must be set before MCP activation. Read them from the plugin process environment, not tool arguments. Do not print them or include them in exceptions.

The gateway stores settings outside the Computer workspace, in an authenticated-encryption vault readable only by the gateway user. The vault key is stored in a separate mode-0600 file in the same control volume; host administrators and full backups can access both. Legacy JSON settings migrate on use. Native GitHub inbox and Google Calendar requests resolve their credentials inside the control service. It passes only the selected plugin’s declared settings to its process. Settings are shared by versions from the same marketplace and excluded from task arguments and executor journal payloads. A local package is bound to its exact content hash and must be configured again when it changes. A different marketplace or local replacement cannot inherit another source’s credentials. This does not isolate mutually untrusted plugins running under the shared Computer identity.

Set "requires_confirmation": true on a manifest tool to require owner approval even with automatic Computer access. An owner can remember an approval for matching arguments from chat. The GitHub plugin uses this for all write operations. A plugin can return an MCP CallToolResult with isError: true and _meta: {"shinbot_outcome": "uncertain"} when an external write may have completed without acknowledgement. The Computer records an uncertain receipt and pauses the task for owner review. Use ordinary MCP errors for rejected inputs that did not cause a change. Inspect marketplaces/official/plugins/github/ for a complete example using the existing HTTPX and MCP dependencies.

Test and activate

shinbot plugins install /path/to/text-tools
shinbot plugins activate text-tools 1.0.0

Start a new conversation and ask for a small test with a known result. Executable activation requires extension and Computer permission and a completed security scan with no high or critical findings. Scan reports are tied to the exact package digest, scanner version and ShinBot policy. Check failures, timeouts, malformed input and interrupted execution before sharing a tool that changes external state.

ShinBot pins active versions when a conversation starts. Publishing a newer version does not change a running conversation. Disable a plugin with shinbot plugins disable text-tools to block further calls, including calls from existing conversations. Activate a previously installed version to roll back.

Publish a marketplace

Put packages under plugins/<id>/ in a GitHub repository. From a ShinBot source checkout, generate the catalog:

uv run python scripts/build_marketplace.py /path/to/your-shinbot-plugins

Commit the packages, license and generated shinbot-marketplace.json to your repository. Users can add it with:

shinbot marketplace add your-name/your-shinbot-plugins

The catalog records each package ID, version, directory and content hash. Refresh resolves a repository commit and verifies the package against that hash. Never change the bytes of a released plugin version; publish a new version instead.

The official repository is shinbot-official-plugins. Submit a pull request with a small purpose, clear instructions, a license and a way to reproduce the tool’s behavior. Public personal repositories use the same format.

Execution and dependencies

Executable plugins run in the Computer with access to its workspace and outbound network. They do not receive mounted gateway data, provider credentials, browser profiles or the Docker socket. This is a shared trust boundary: it is not a separate VM or per-plugin network sandbox.

The manifest’s requires list must be empty in the default runner. Additional dependencies need a host license review and a prepared runner image. A plugin cannot install arbitrary packages into the gateway or runtime. Supported manifest license identifiers are MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD, Unlicense and CC0-1.0.

ShinBot can write, test and stage its own skills and tools. Activation remains a reviewed change; newly activated tools become available in a new conversation.

Direct MCP connections and standard skills

POST /api/plugins/mcp/connect accepts id, name, version, server and access. server selects stdio with command, args and env, or http/sse with url and headers. Discovery runs in the unprivileged Computer worker. ShinBot stores schemas in an immutable plugin manifest and saves credentials separately. Reconnecting with changed configuration or schemas requires a new version. Direct MCP tools require confirmation.

POST /api/plugins/skills/install accepts a command such as npx skills add owner/repo --skill NAME, the verified upstream license, and access. The pinned Skills CLI runs in a temporary workspace directory. Its text files become an immutable skill package; their content hash determines the version. POST /api/plugins/skills/import accepts a text files map instead for host-side imports. Neither import path executes skill scripts.

Use PUT /api/plugins/{id}/access with {"mode":"shared","bots":[]} or {"mode":"bots","bots":["BOT_ID"]}. Access is stored separately from the package. Task creation selects available versions, and dispatch checks current access again. The plugins read_skill agent tool checks both task selection and current bot access before returning instructions or reference files. This is routing control within a trusted single-owner installation, not process isolation.

Agent tool schemas live in runtime/src/tools.ts; orchestration and approval checks live in src/shinbot/core.py. Keep those schemas, system instructions and setup endpoints aligned when adding capabilities. bots save_template validates and stores portable profiles in the local template library. Saving a template does not publish it to a repository.

Native toolkits

Projects and Kanban use kind: builtin. Their backend operations and UI are shipped with ShinBot, while their manifests are distributed in the official marketplace and installed explicitly. A native package must match the bundled files byte for byte. Custom packages use kind: extension for their own UI and hooks, or MCP and skills for tools and instructions. Native toolkit code changes currently require a ShinBot release.

The Projects toolkit owns repository registration, worktree lifecycle and delegation policy. Core task records carry working-directory and parent-task references so ordinary file tools, commands, cancellation and durable scheduling apply to project work. Git operations run under the Computer worker identity. Repository registration and worktree creation do not execute setup scripts or install project dependencies.

Chat and tool hooks

Declare handlers alongside UI or tools, or in an extension containing only hooks:

{
  "permissions": ["hooks"],
  "hooks": [
    {"event": "before_chat", "entrypoint": "hooks.py", "timeout": 10},
    {"event": "before_tool", "entrypoint": "hooks.py", "timeout": 10},
    {"event": "after_tool", "entrypoint": "hooks.py", "timeout": 10},
    {"event": "after_response", "entrypoint": "hooks.py", "timeout": 10}
  ]
}

Each Python entrypoint exports a synchronous handle(event, context) function. It receives event_id, task_id, bot_id, data and a persistent data_dir under the Computer workspace. Declare credentials through configuration; only that plugin’s settings are supplied to its process. Return a JSON object or None.

Event context["data"] Allowed return fields
before_chat Submitted content, accumulated context text context to supply model instructions; block with a reason to stop the turn
before_tool tool, args args to replace arguments; block with a reason to stop the call
after_tool tool, args, completed result result to replace the result given to the model
after_response answer, status, error Empty object; observe the finished turn without replacing its answer
def handle(event, context):
    data = context["data"]
    if event == "before_chat":
        return {"context": data["context"] + "\nReport which checks actually ran."}
    if event == "before_tool" and data["tool"] == "run_command":
        if "git push --force" in data["args"].get("command", ""):
            return {"block": "This workflow does not allow force pushes."}
    return {}

The string check above illustrates a hook, not a shell security policy. ShinBot still evaluates approvals after a hook changes arguments. A hook result cannot approve an action or change the selected tool. before_tool and after_tool wrap completed tool calls throughout the model’s tool loop. Failed or denied tools do not trigger after_tool.

Hooks run in deterministic plugin-ID order, using the versions pinned to the conversation. Later hooks see earlier changes. New conversations select newly activated versions. Removing bot access or disabling a plugin stops its future hooks and tool calls. A hook already executing can finish.

Computer records an invocation before running it and reuses completed results after reconnects and restarts. An interrupted invocation is marked uncertain and is not executed again automatically. Before-hook failures stop the turn or tool with a reason. After-hook failures leave the original tool result or answer available and create a plugin event. Use the stable event_id as an idempotency key when calling another service: no local journal can guarantee an external system’s outcome after a lost connection.

Hooks have Computer file and network access and run automatically once activated. Activation grants that execution; their internal operations do not each produce a chat approval. Keep them short, use the existing runtime dependencies, and set a timeout between 1 and 60 seconds. Return at most 200 KB. Context and blocking text are limited to 20,000 characters.