Guides
Host hooks by CLI#
Hooks are host lifecycle callbacks, not MCP tool calls. The hard part is that each CLI exposes hooks differently. agent-connector groups those differences into paradigms so a connector author can write one normalized handler and still get honest per-host behavior.
The hook mental model#
A hook runs because the host emitted an event: session started, a tool is about to run, a permission decision is needed, a tool failed, context was compacted, or a subagent changed state. The model does not choose a hook the way it chooses an MCP tool.
Connector declares a normalized hook handler
|
v
Target host adapter decides the hook paradigm
|
+-- json-stdio: host config calls the home-bin hook command
|
+-- ts-plugin: generated plugin module imports/dispatches handlers
|
+-- mcp-only: no host hook layer, so hooks skip with a warning
|
v
Handler returns context / allow / block / warn where supportedOfficial host surfaces to know first#
Start by separating three facts: MCP tools are model-selected, hooks are host lifecycle callbacks, and each host chooses its own hook transport. The links below are the current public references used by this guide.
| Host family | Native hook surface | How agent-connector connects it |
|---|---|---|
| Claude Code | Settings JSON registers hook events such as PreToolUse with a matcher and command hook entries. | The adapter writes the command entry and dispatches stdin JSON into your normalized defineHook handler. |
| Gemini CLI | Gemini uses its own event vocabulary: BeforeTool, AfterTool, PreCompress, BeforeAgent, and related lifecycle events. | agent-connector maps normalized events such as PreToolUse and PostToolUse to the Gemini event names before writing hooks. |
| Codex CLI | Codex exposes hook configuration separately from MCP server registration, with command hooks for supported lifecycle events. | The Codex adapter renders a hooks.json entry that points at the same home-bin hook dispatcher. |
| OpenCode family | OpenCode loads a plugin module with event functions such as tool.execute.before and permission.ask. | The adapter generates a plugin file that calls the connector runtime; connector authors still write normalized hooks. |
| MCP-only hosts | Some hosts document MCP server registration but do not expose a hook layer to connector packages. | The installer keeps MCP working and reports hooks as unsupported instead of pretending the policy will run. |
Cross-validation before a hook claim#
The full host lists below are generated from adapter metadata, not typed by hand. A host is presented as hook-capable only when the registry reports a non-MCP-only paradigm and the docs drift tests keep the list in lock-step with loaded adapters. The examples table adds external evidence for representative host families.
| Host or family | Local proof | External proof |
|---|---|---|
| Claude Code | json-stdio adapter, settings.json hooks, claude-code tests | Official Claude Code hooks docs |
| Gemini CLI | json-stdio adapter, BeforeTool/AfterTool mapping, gemini-cli tests | Official Gemini CLI hooks reference |
| Codex CLI | hooks.json adapter, codex tests, command hook dispatcher | Official Codex hooks docs |
| OpenCode | ts-plugin adapter, generated plugin module, opencode tests | Official OpenCode plugin docs |
| MCP-only hosts | mcp-only paradigm list is registry-derived and drift-guarded | Host MCP docs where available; hooks intentionally unsupported |
CLI behavior by hook paradigm#
| Paradigm | How it works | Hosts in this repo |
|---|---|---|
json-stdio | The host calls a configured command. agent-connector receives a JSON payload on the home-bin hook entrypoint, normalizes it, runs your handler, and prints the host's expected response shape. | Claude Code, CodeBuddy, Codex CLI, Cursor, VS Code Copilot, JetBrains Copilot, GitHub Copilot CLI, Gemini CLI, Qwen CLI, Kiro, Kimi CLI, Crush, Goose, Hermes, Droid (Factory), OpenHands, Antigravity (IDE), Antigravity CLI (agy), Continue, Amazon Q Developer CLI, Grok Build, Grok CLI, Devin CLI (Cognition), Open Interpreter |
ts-plugin | The package emits a host plugin/module. That module exports the lifecycle functions the host expects and dispatches into the connector runtime. | OpenCode, MiMoCode, Kilo CLI, Kilo Code, OMP, NVIDIA NemoClaw, OpenClaw, Amp |
mcp-only | The host can register MCP servers but exposes no hook layer to agent-connector. Declared hooks are skipped with a warning instead of pretending to run. | Warp, Cline, Trae, Zed, Codebuff, Mux, Pi, Windsurf, Junie, Mistral Vibe |
Write the connector hook once#
The connector author writes against agent-connector's normalized event type. This example blocks one dangerous shell pattern, injects context for this connector's MCP tools, and leaves unsupported host behavior to the adapter.
import {
defineConnector,
defineHook,
} from "@ken-jo/agent-connector/sdk";
const guardRiskyWrites = defineHook("PreToolUse", {
matcher: "Bash|Write|Edit|apply_patch|mcp__acme_db__.*",
handler(evt) {
const input = JSON.stringify(evt.toolInput ?? {});
if (evt.toolName === "Bash" && /\brm\s+-rf\b/.test(input)) {
return {
decision: "deny",
reason: "rm -rf is blocked by acme-db policy.",
};
}
if (evt.toolName.startsWith("mcp__acme_db__")) {
return {
decision: "context",
additionalContext: "acme-db MCP tools are read-only in this connector.",
};
}
},
});
export default defineConnector({
server: { transport: "stdio", command: "node", args: ["./my-mcp-server.mjs"] },
hooks: { PreToolUse: guardRiskyWrites },
});What host config can look like#
These are representative rendered shapes, not extra files you maintain by hand. The stable part is the home-bin command: agent-connector hook <host> <event> --connector <id>.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit",
"hooks": [
{
"type": "command",
"command": "agent-connector hook claude-code PreToolUse --connector acme-db"
}
]
}
]
}
}What happens during dispatch#
- The host emits a native lifecycle payload in its own format.
- The adapter converts that payload into one normalized event shape where the host has a matching concept.
- The connector hook handler receives the event and returns context, allow/block, warning text, or another supported response.
- The adapter translates that response back to the native host contract.
- If the host cannot support that event or response, the installer and doctor surface the limitation as a warning or unavailable capability.
How to customize behavior safely#
- Use
matcherto narrow the hook to specific tool names or MCP tool prefixes before adding logic. - Use
hostsoverrides only when one host has different semantics; the top-level handler stays the mandatory fallback. - Use
platforms.<id>.nativeHooksfor host-specific events that have no normalized event yet, then keep the payload parsing local to that host. - Disable a surface per host with
platforms.<id>.hooks = falsewhen the host behavior is not mature enough for your package.
Beginner safety rules#
Hooks are additive policy, not your only guardrail
If a tool can delete files, run SQL, or mutate production state, the MCP tool handler itself must validate the operation. Hooks can add host-side policy and UX, but an MCP-only host may never run them.- Keep hook handlers fast. A hook sits on the host's interaction path.
- Return small context. Large hook output becomes model context or host UI noise.
- Treat hook payloads as host-specific observations, not guaranteed universal state.
- Test one host from each paradigm before claiming cross-host behavior.