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.json → type: "command" |
Rust can't dynamically load foreign code safely |
| Claude Code |
TypeScript (but sandboxed) |
settings.json → command handler |
Process isolation = security boundary |
| Copilot |
Unknown (compiled) |
hooks.json → bash/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:
- A CLI binary (any language, any runtime)
- 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.
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 events —
hook_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
-
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)
-
runner.go instrumentation — call hooks.Emit(PreToolUse) before Registry.Execute; respect block decisions.
-
Compaction — PreCompact / PostCompact in compaction.go.
-
Subagent tools — SubagentStart/Stop in delegatecoding.go, spawngeneral.go.
-
cometmind/internal/plugins/ — plugin management
store.go — discover installed plugins, validate manifests
commands.go — register plugin slash commands as agent-callable tools
-
Settings — cometmind.hooks / cometmind.plugins in cometline-settings.json + Zod schema.
-
Settings UI — new Plugins section in Cometline.
-
Docs + examples — ship examples/plugins/hello-hooks/ with a block-rm script and a session-notify script.
-
Tests — matcher, block tool, timeout, malformed JSON, disabled hooks no-op, exit code handling, context cancellation kills subprocess.
Acceptance criteria
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:
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.tsmodules) 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
import()aplugin.tsmodule 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.cometmind/internal/agent/runner.goruns tools viaRegistry.Executewith no before/after hooks, permission gates, or user-script callbacks.registry.goand shipping a CometMind binary; community cannot add a small automation script the way Claude Code hooks allow.hooks.jsonwithtype: "command". We should meet them where they are.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.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:
hooks.json→type: "command"settings.json→commandhandlerhooks.json→bash/powershellBeforeTool→ shell commandplugin.ts(TypeScript SDK)import()at runtime — outlier, not the targetplugin.jsonimport()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):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:
codex-rs/hooks/src/engine/command_runner.rscodex-rs/config/src/hook_config.rscodex-rs/core-plugins/src/loader.rs10 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:Every harness adapter is declarative config only. The plugin is the CLI binary. See:
hooks.jsononlyplugin.json+hooks.json+ slash commandsCometMind today (no equivalent)
JobProgressTrackerjob_progress_hook.go)Proposed behavior
Design principle: one unified model — CLI binary + JSON manifest
A CometMind plugin is:
plugin.jsonmanifest declaring hooks, commands, and metadataNo 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.
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:
{workspace}/.cometmind/hooks.json~/.cometmind/hooks.json~/.cometmind/plugins/{id}/plugin.json){workspace}/.cometmind/plugins/{id}/No separate "hooks phase" vs "plugins phase" — they are the same system. A standalone
hooks.jsonis just a plugin without its own binary directory. A plugin is ahooks.jsonbundled with a binary.2. Hook events & lifecycle
All hook events use the same subprocess execution engine:
SessionStartSessionStartUserPromptSubmitUserPromptSubmitPreToolUseRegistry.ExecutePreToolUsePermissionRequestPermissionRequestPostToolUsePostToolUsePreCompactPreCompactPostCompactPostCompactSubagentStartdelegate_coding_task/spawn_general_agentSubagentStartSubagentStopSubagentStopTurnStopdoneSSE)StopTurnErrorSubprocess 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:
012{"block": true}(simpler scripts can use exit code instead of JSON)Runtime guarantees:
timeout_secin manifest), enforced viacontext.WithTimeoutos.Process.Kill()on parent context cancellation (session abort)parallel: trueopt-in for independent hooks (future)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.jsonmanifest:Install paths:
~/.cometmind/plugins/or{workspace}/.cometmind/plugins/cometmind plugin add <path>(future)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.Openis 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
hook_started/hook_finishedfor debugging..codex/hooks.json,.claude/hooks, or Copilothooks.json.5. Interaction with existing features
mcp_{server}_{tool}in PreToolUse matchersUserPromptExpansionequivalent later for/skillexpansionsource: job_workerin payloadSubagentStart/Stophooks; no hooks inside external harness unless bridgedplatform: discordin payloadImplementation sketch
cometmind/internal/hooks/— new packageconfig.go— loadplugin.json/hooks.jsonfrom discovery pathsmatcher.go— regex/glob matching on event + tool_nameexecutor.go— subprocess spawner: stdin JSON pipe, stdout JSON parse, timeout viacontext.WithTimeout,killon cancel, exit code handlingschema.go— JSON schema definitions for hook input/output (contract tests)runner.go— hook chain orchestrator (serial, abort on block)runner.goinstrumentation — callhooks.Emit(PreToolUse)beforeRegistry.Execute; respect block decisions.Compaction —
PreCompact/PostCompactincompaction.go.Subagent tools —
SubagentStart/Stopindelegatecoding.go,spawngeneral.go.cometmind/internal/plugins/— plugin managementstore.go— discover installed plugins, validate manifestscommands.go— register plugin slash commands as agent-callable toolsSettings —
cometmind.hooks/cometmind.pluginsincometline-settings.json+ Zod schema.Settings UI — new Plugins section in Cometline.
Docs + examples — ship
examples/plugins/hello-hooks/with a block-rm script and a session-notify script.Tests — matcher, block tool, timeout, malformed JSON, disabled hooks no-op, exit code handling, context cancellation kills subprocess.
Acceptance criteria
~/.cometmind/hooks.jsonand/or{workspace}/.cometmind/hooks.jsonplugin.jsonmanifest + CLI binary under~/.cometmind/plugins/{id}/PreToolUseandPostToolUsefire around built-in tool execution with JSON context on stdinUserPromptSubmitfires before the agent loop; hook can block or append system contextPreCompact/PostCompactfire around context compactionSubagentStart/SubagentStopfire for delegate/spawn subagentsblock: truein hook stdout prevents tool execution (same UX as permission deny)append_systemin hook stdout injects context into model promptplugin_{id}_{command})Out of scope
opencode plugin— use git + local install firsttool.execute.beforeas JS export) — requires Node.js process; subprocess covers thisparallel: truelater)Related issues
SessionStart)Related code & references
CometMind (Go backend):
cometmind/internal/agent/runner.gocometmind/internal/tools/registry.gocometmind/internal/agent/compaction.gocometmind/internal/event/event.gocometmind/internal/mcp/manager.gocometmind/internal/skills/skills.goCodex — Rust agent core, subprocess hooks:
Plannotator — multi-harness CLI binary plugin: