Skip to content

feat(cometmind): plugin system via CLI binary + JSON manifest (subprocess model — Codex, Plannotator convergence) #25

Description

@Tomlord1122

Summary

Add a first-class extensibility layer to CometMind — a unified CLI binary + JSON manifest plugin system — so users and teams can automate policy, inject context, register custom tools, and observe the agent loop without forking the monorepo.

Why CLI binary + JSON? CometMind is a Go backend service, not a Node.js process. An OpenCode-style TypeScript SDK plugin system (in-process plugin.ts modules) is architecturally impossible without embedding a JS runtime. The entire coding-harness ecosystem — Codex (Rust), Claude Code, Copilot, Gemini CLI — has independently converged on the same pattern for the same reason: when your agent core is a compiled language, the natural plugin boundary is a subprocess. Event fires → spawn subprocess → JSON stdin → parse JSON stdout → apply outcome.

Today Cometline/CometMind has Skills (prompt templates), MCP (external tool servers), and hard-coded Go tools. There is no hook surface around turns, tool calls, compaction, or subagents, and no plugin install path.

Problem

  • CometMind is Go — in-process plugins are impossible without embedding a runtime. Unlike OpenCode (Node.js), which can import() a plugin.ts module at runtime, Go is a compiled language. An OpenCode-style plugin SDK would require embedding Bun, Node, or WASM — a massive dependency for marginal benefit. The subprocess model is the natural Go-native solution.
  • No lifecycle interception — the agent loop in cometmind/internal/agent/runner.go runs tools via Registry.Execute with no before/after hooks, permission gates, or user-script callbacks.
  • Extensions require Go changes — new tools mean editing registry.go and shipping a CometMind binary; community cannot add a small automation script the way Claude Code hooks allow.
  • Ecosystem convergence on subprocess — Codex (Rust), Claude Code, Copilot, and Gemini CLI all use the same subprocess-based hook model. Users from any of these harnesses will expect the same: hooks.json with type: "command". We should meet them where they are.
  • MCP ≠ hooks — MCP adds tools to the main agent (cometmind/internal/mcp/) but does not fire on session start, prompt submit, or post-turn memory extract; it is the wrong abstraction for lint-on-save, block-rm -rf, or desktop notifications.
  • Skills ≠ plugins — Agent Skills (cometmind/internal/skills/) are markdown prompts invoked via /skill-name, not executable extensions.

Reference models

Why the ecosystem converged on subprocess

Every major coding harness whose core is a compiled language chose subprocess-based hooks:

Harness Core language Plugin model Why subprocess?
Codex Rust hooks.jsontype: "command" Rust can't dynamically load foreign code safely
Claude Code TypeScript (but sandboxed) settings.jsoncommand handler Process isolation = security boundary
Copilot Unknown (compiled) hooks.jsonbash/powershell Language-agnostic by design
Gemini CLI Unknown (compiled) BeforeTool → shell command Same — subprocess is the universal interface
OpenCode Node.js plugin.ts (TypeScript SDK) Only one that can import() at runtime — outlier, not the target
CometMind Go → CLI binary + plugin.json Go can't import() JS. Subprocess is the natural boundary.

The pattern is universal: when your agent core is a compiled language, the plugin boundary is a subprocess.

Codex hooks (cleanest reference — Rust, like our Go)

Codex's hook system in Rust (codex-rs/hooks/) is the purest implementation of the subprocess model. Same architectural constraints as CometMind (Go):

// hooks.json — declarative config
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "ExitPlanMode",
      "hooks": [{
        "type": "command",
        "command": "plannotator",
        "timeout": 345600
      }]
    }]
  }
}

Execution flow: event fires → select matching handlers by regex matcher → serialize event context as JSON → pipe to subprocess stdin → run command with timeout + kill_on_drop → parse JSON stdout → apply outcome (block, inject context, modify input).

Key sources:

10 hook events: PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SessionStart, UserPromptSubmit, SubagentStart, SubagentStop, Stop.

3 handler types: Prompt (inject text), Command (shell subprocess), Agent (spawn subagent).

Plannotator: one CLI binary, 6+ harnesses, zero SDK code

Plannotator proves the subprocess model scales across harnesses. It ships a single CLI binary (plannotator) + web UI, with thin declarative JSON adapters per harness — no SDK code needed for any harness:

                    ┌──────────────────┐
                    │   plannotator    │  ← single CLI binary (any language)
                    │   + web UI       │
                    └────────┬─────────┘
                             │
   ┌─────────────┬───────────┼───────────┬─────────────┐
   │             │           │           │             │
┌──┴──┐    ┌────┴────┐ ┌────┴────┐ ┌────┴────┐  ┌─────┴─────┐
│Codex│    │ Claude  │ │Copilot  │ │ Gemini  │  │ OpenCode  │  ...
│hooks│    │ hooks   │ │hooks    │ │ hooks   │  │plugin.ts  │
│.json│    │.json    │ │.json    │ │.toml    │  │(SDK-only) │
└─────┘    └─────────┘ └─────────┘ └─────────┘  └───────────┘

Every harness adapter is declarative config only. The plugin is the CLI binary. See:

CometMind today (no equivalent)

Extension What it does Gap
Skills Prompt templates No code execution
MCP Remote tools No turn lifecycle
Built-in tools Go implementations Requires binary change
JobProgressTracker Internal nudge Not user-pluggable (job_progress_hook.go)

Proposed behavior

Design principle: one unified model — CLI binary + JSON manifest

A CometMind plugin is:

  1. A CLI binary (any language, any runtime)
  2. A plugin.json manifest declaring hooks, commands, and metadata

No embedded JS runtime. No language-specific SDK. No phased rollout — the subprocess model IS the plugin model from day one. This is the natural choice for a Go backend.

// ~/.cometmind/plugins/plannotator/plugin.json
{
  "id": "plannotator",
  "name": "Plannotator",
  "version": "0.21.3",
  "description": "Interactive plan & code review with visual annotations",
  "hooks": {
    "PreToolUse": [{
      "matcher": "ExitPlanMode",
      "command": "plannotator",
      "timeout_sec": 345600
    }],
    "PostToolUse": [{
      "matcher": "EnterPlanMode",
      "command": "plannotator improve-context",
      "timeout_sec": 5
    }]
  },
  "commands": {
    "review": {
      "command": "plannotator review $ARGUMENTS",
      "description": "Open interactive code review"
    }
  }
}

This is exactly how Plannotator works across Codex, Claude, Copilot, and Gemini — one binary, declarative config per harness.


1. Hook configuration & discovery

Search paths (project → global), similar to Skills:

Location Scope
{workspace}/.cometmind/hooks.json Project-level hook config
~/.cometmind/hooks.json Global hook config
~/.cometmind/plugins/{id}/ Installed plugin (CLI binary + plugin.json)
{workspace}/.cometmind/plugins/{id}/ Project-scoped plugin

No separate "hooks phase" vs "plugins phase" — they are the same system. A standalone hooks.json is just a plugin without its own binary directory. A plugin is a hooks.json bundled with a binary.


2. Hook events & lifecycle

All hook events use the same subprocess execution engine:

CometMind event When Codex equivalent
SessionStart Session created / first turn SessionStart
UserPromptSubmit Before agent loop processes user message UserPromptSubmit
PreToolUse Before Registry.Execute PreToolUse
PermissionRequest Before tool requiring user approval PermissionRequest
PostToolUse After tool result persisted PostToolUse
PreCompact Before context compaction summarize PreCompact
PostCompact After summary written PostCompact
SubagentStart delegate_coding_task / spawn_general_agent SubagentStart
SubagentStop Subagent finished SubagentStop
TurnStop Turn completes (done SSE) Stop
TurnError Turn failed / max steps

Subprocess execution contract

Every hook command receives event context on stdin as JSON and returns outcomes on stdout as JSON:

Input (stdin):

{
  "hook_event": "PreToolUse",
  "session_id": "...",
  "workspace_path": "/abs/path",
  "turn_id": "...",
  "tool_name": "write_file",
  "tool_input": { "path": "...", "content": "..." },
  "model_id": "gpt-5.1",
  "provider_id": "openai",
  "platform": "desktop",
  "job_id": null
}

Output (stdout):

{
  "block": false,
  "block_reason": null,
  "append_system": "extra context for the model",
  "modify_tool_input": null,
  "status_message": "Security scan passed"
}

Exit code convention:

Exit code Meaning
0 Success — stdout parsed as outcome JSON
1 Hook failed — error logged, operation continues (non-blocking)
2 Block operation — equivalent to {"block": true} (simpler scripts can use exit code instead of JSON)
Other Treated as error, logged, operation continues

Runtime guarantees:

  • Timeout per handler (timeout_sec in manifest), enforced via context.WithTimeout
  • os.Process.Kill() on parent context cancellation (session abort)
  • Stderr captured and logged at WARN level
  • Max hook chain depth: 10 (prevent infinite recursion loops)
  • Default serial execution; parallel: true opt-in for independent hooks (future)
  • Project hooks require workspace trust (same bar as run_command)

Security: hooks run as the CometMind process user with workspace cwd; default off until user enables in settings.


3. Plugin packages (CLI binary model)

A plugin is a directory containing a CLI binary and a plugin.json manifest:

~/.cometmind/plugins/my-plugin/
  plugin.json        # id, name, version, hooks, commands
  my-plugin          # CLI binary (or symlink to $PATH)
  hooks/             # optional: additional hook scripts
  skills/            # optional: bundled agent skills (markdown)

Install paths:

  • Local: drop folder under ~/.cometmind/plugins/ or {workspace}/.cometmind/plugins/
  • CLI: cometmind plugin add <path> (future)
  • UI: Settings → Plugins → Install from path / marketplace (later)

No npm/JS runtime, no WASM, no embedded scripting. The subprocess model is language-agnostic by design — and the only natural choice for a Go backend. If someone needs <10ms inline logic later, Go's plugin.Open is a viable escape hatch — but this is not a core design goal.

Plugin slash commands register as agent-callable tools with namespaced ids: plugin_{id}_{command}.


4. Cometline integration

  • Settings → CometMind → Plugins & Hooks — list enabled plugins, hook events, test runner ("fire sample PreToolUse").
  • Optional SSE eventshook_started / hook_finished for debugging.
  • Electron — no hook execution in renderer; CometMind sidecar only.
  • Import helpers (stretch) — wizard to import a subset of .codex/hooks.json, .claude/hooks, or Copilot hooks.json.

5. Interaction with existing features

Feature Interaction
MCP Hooks see MCP tools as mcp_{server}_{tool} in PreToolUse matchers
Skills UserPromptExpansion equivalent later for /skill expansion
Jobs worker (#23) Worker turns fire same hooks with source: job_worker in payload
ACP / harness delegation (#22) SubagentStart/Stop hooks; no hooks inside external harness unless bridged
Discord gateway Hooks run server-side; platform: discord in payload

Implementation sketch

  1. cometmind/internal/hooks/ — new package

    • config.go — load plugin.json / hooks.json from discovery paths
    • matcher.go — regex/glob matching on event + tool_name
    • executor.go — subprocess spawner: stdin JSON pipe, stdout JSON parse, timeout via context.WithTimeout, kill on cancel, exit code handling
    • schema.go — JSON schema definitions for hook input/output (contract tests)
    • runner.go — hook chain orchestrator (serial, abort on block)
  2. runner.go instrumentation — call hooks.Emit(PreToolUse) before Registry.Execute; respect block decisions.

  3. CompactionPreCompact / PostCompact in compaction.go.

  4. Subagent toolsSubagentStart/Stop in delegatecoding.go, spawngeneral.go.

  5. cometmind/internal/plugins/ — plugin management

    • store.go — discover installed plugins, validate manifests
    • commands.go — register plugin slash commands as agent-callable tools
  6. Settingscometmind.hooks / cometmind.plugins in cometline-settings.json + Zod schema.

  7. Settings UI — new Plugins section in Cometline.

  8. Docs + examples — ship examples/plugins/hello-hooks/ with a block-rm script and a session-notify script.

  9. Tests — matcher, block tool, timeout, malformed JSON, disabled hooks no-op, exit code handling, context cancellation kills subprocess.

Acceptance criteria

  • Users can define command hooks in ~/.cometmind/hooks.json and/or {workspace}/.cometmind/hooks.json
  • Users can install a plugin as a directory with plugin.json manifest + CLI binary under ~/.cometmind/plugins/{id}/
  • PreToolUse and PostToolUse fire around built-in tool execution with JSON context on stdin
  • UserPromptSubmit fires before the agent loop; hook can block or append system context
  • PreCompact / PostCompact fire around context compaction
  • SubagentStart / SubagentStop fire for delegate/spawn subagents
  • block: true in hook stdout prevents tool execution (same UX as permission deny)
  • append_system in hook stdout injects context into model prompt
  • Non-zero exit codes logged at WARN level; operation continues (graceful degradation)
  • Parent context cancellation kills all running hook subprocesses
  • Max hook chain depth guard (10)
  • Hooks can be disabled globally in settings without deleting config files
  • Plugin slash commands register as namespaced tools (plugin_{id}_{command})
  • Example plugin in repo docs works on a sample workspace

Out of scope

  • Embedded JS/WASM runtime — unnecessary for a Go backend; the subprocess model is the natural solution
  • npm marketplace / auto-install like opencode plugin — use git + local install first
  • OpenCode-style SDK (tool.execute.before as JS export) — requires Node.js process; subprocess covers this
  • Full Codex/Claude hook parity (20+ events, prompt/MCP-as-handler) — incremental
  • Hooks inside delegated OpenCode/Claude Code harnesses (#22)
  • Cometline UI plugins (Svelte/Electron extensions)
  • Unsigned remote plugin download without explicit user consent
  • Hook parallelism (start serial, add parallel: true later)

Related issues

Related code & references

CometMind (Go backend):

Codex — Rust agent core, subprocess hooks:

Plannotator — multi-harness CLI binary plugin:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions