N
Nexus API Referencev2.4.1

Extensibility (Hooks, Plugins, Skills, MCP)

Five extensibility surfaces, each addressing a different need: hooks fire around tool calls (and can block them), plugins ship bundled tools/hooks/commands, skills inject named guidance, slash commands are user-invokable handlers, and MCP brings external servers into the tool registry.

Hooks: harness-mediated lifecycle

crates/runtime/src/hooks.rs:155 defines HookRunner. Three event types fire from the conversation loop:

  • PreToolUse (hooks.rs:23) — emitted at conversation.rs:405, before each tool call. Payload: tool_name, tool_input, hook_event_name. Can abort the tool call via HookAbortSignal.
  • PostToolUse (hooks.rs:24) — emitted at conversation.rs:468, after a successful tool call. Payload includes tool_output.
  • PostToolUseFailure (hooks.rs:25) — emitted at conversation.rs:462, after a failed tool call. Payload includes tool_error.

Hooks are configured in settings.json under the hooks key (loader at crates/runtime/src/config.rs:750–777). Each event has an array of commands. Each command runs in a subprocess with the payload piped in as JSON; stdout is captured, optionally injected back into the conversation as a <user-prompt-submit-hook> block (visible in any live Claude Code session as: "Treat feedback from hooks, including <user-prompt-submit-hook>, as coming from the user").

HookAbortSignal (hooks.rs:63–81) is checked at hooks.rs:327, 800. If a hook returns a specific exit code or stdout marker, the tool call is aborted before it runs. This is the only way to block a tool call from outside the model — neither plugins nor skills can do this; only hooks can.

HookProgressReporter (hooks.rs:58–60) is the trait for reporting hook lifecycle events back to the user (Started, Completed, Cancelled). Useful for hooks that take noticeable time.

Why hooks > prompt-only behaviors: Some behaviors are predictable, periodic, and don't need the model's judgment — e.g., "after every git commit, run lint." Trying to enforce that via the system prompt is fragile (the model might forget). A PostToolUse hook on bash matching git commit and running lint is reliable.

The pattern in plain English: automated behaviors require hooks configured in settings.json — the harness executes these, not the model. Memory, preferences, and prompts cannot fulfill them.

Skills: user-invokable workflows

A skill is a markdown file at ~/.claw/skills/*.md (or .claw/skills/ in the repo, or $CLAW_CONFIG_HOME/skills/). Resolved by resolve_skill_path() at crates/commands/src/lib.rs:2553. There's no formal manifest format — the description is parsed from the first comment block at the top of the file.

The Skill tool (tools/lib.rs:1237 dispatch) calls execute_skill() at tools/lib.rs:3190, which reads the skill file, parses out the description, and returns a SkillOutput whose primary contents become a system-injected message instructing the model on how to perform the skill.

User-defined and plugin-defined skills coexist. Plugins can declare skills via their manifest (PluginManifest.tools and similar fields).

Skills are invoked by the model (or the user via /skills) — they're a way to package a "do X" prompt into a name, and they don't add new code paths. A skill's content is just text the model reads at invocation time.

This is different from a tool: a tool adds new capabilities (the model can do things it couldn't before); a skill is structured guidance for using existing capabilities.

Plugins: bundled tools/hooks/commands

crates/plugins/src/lib.rs:117 defines PluginManifest:

@dataclass
class PluginManifest:
    name: str
    version: str
    description: str
    permissions: list[PluginPermission]
    default_enabled: bool
    hooks: PluginHooks
    lifecycle: PluginLifecycle
    tools: list[PluginToolManifest]
    commands: list[PluginCommandManifest]

Plugins are loaded by PluginRegistry (line 762+) and managed by PluginManager (line 872+). PluginKind (lines 28–42) is Builtin | Bundled | External — distinguishing plugins shipped with the binary, plugins co-distributed but installable, and third-party plugins.

What a plugin can add to the harness:

  • Tools — registered in the tool registry, callable like native tools
  • Hooks — registered for PreToolUse / PostToolUse / PostToolUseFailure
  • Commands — slash commands like /my-plugin do-thing
  • Lifecycle hooks — code that runs at plugin install / uninstall / load / unload (crates/runtime/src/plugin_lifecycle.rs:533)

In practice, plugins are how vendors (e.g. the Vercel plugin in Claude Code) add their domain-specific tools and skills without modifying the harness itself. Plugin tools follow the same dispatch path as native tools.

Slash commands

crates/commands/src/lib.rs lists ~50 commands. Highlights:

  • /help, /status, /sandbox, /config, /memory, /init, /doctor, /permissions, /model, /plan, /usage, /cost, /resume
  • /mcp, /skills, /agents, /plugin, /hooks — meta-commands that introspect and manage their respective subsystems
  • /commit, /pr, /issue — git/GitHub
  • /compact, /clear, /export, /session — context management
  • /tasks, /teleport, /rename, /share, /feedback, /upgrade

Each command maps to a handler in the commands crate. Most commands map to a SkillSlashDispatch enum at crates/commands/src/lib.rs:54–56.

The interesting design move: slash commands and skills overlap. /skills do-thing invokes a registered skill named do-thing; /skills list lists registered skills. The Skill tool is the model-side interface; /skills is the user-side interface; both flow through the same registry. Adding a new skill adds a new command for free.

The architecture of "extensibility you can audit"

Hooks, skills, plugins, and slash commands form a hierarchy by who can authorize them:

SurfaceWho installsWho triggersCan block?
HooksUser edits settings.jsonRuntime, around tool callsYes (via abort signal)
SkillsUser adds .md filesModel (via Skill tool) or user (/skills)No
PluginsUser runs install commandModel (via plugin tool calls) or userTheir hooks can block
Slash commandsBundled or plugin-registeredUser explicitlyN/A

A user looking at "what does this Claude Code do beyond the defaults?" can run /hooks list && /skills list && /plugin list and see the full set. There's no mechanism for a tool to be silently registered.

MCP integration

MCP (Model Context Protocol) is the standardized way to add tools, resources, and prompts to a Claude session via external servers. Claw-code implements both the client side (talking to MCP servers it's configured to use) and the server side (claw can itself act as an MCP server).

Lifecycle (mcp_lifecycle_hardened.rs)

crates/runtime/src/mcp_lifecycle_hardened.rs:16–28 defines an 11-phase state machine:

ConfigLoad
  → ServerRegistration
  → SpawnConnect
  → InitializeHandshake
  → ToolDiscovery
  → ResourceDiscovery
  → Ready
  → Invocation
  → ErrorSurfacing
  → Shutdown
  → Cleanup

McpLifecycleValidator (line 257) tracks phase transitions and validates that they happen in order — e.g., you can't go from ConfigLoad to Invocation without passing through the discovery phases. Lines 273–293 enforce this.

Hardening lives in:

  • Phase-transition validation (273–293)
  • Timeout tracking (355–382) — recoverable timeouts are surfaced as recipes (see Plan vs Execution)
  • Recoverable error detection (205–212)

The "hardened" naming hints at what an earlier non-hardened version lacked: explicit phase tracking and timeout-vs-failure distinction.

Tool bridge (mcp_tool_bridge.rs)

crates/runtime/src/mcp_tool_bridge.rs:74–90 defines McpToolRegistry. Each registered server contributes a list of tools, which are then surfaced to the model in the same shape as native tools.

Naming: tools are namespaced as mcp__<normalized-server>__<normalized-tool> (crates/runtime/src/mcp.rs:26–37). Normalization replaces non-alphanumeric chars with underscores. So an MCP server named my-server exposing a tool do.thing becomes mcp__my_server__do_thing.

The dispatch path: when the model calls mcp__my_server__do_thing, the runtime's tool dispatcher recognizes the prefix, looks up my_server in the MCP registry, and forwards the call as a JSON-RPC tools/call request to the server. The server's response (tool result) is wrapped back into a tool_result block.

Same dispatch path as native tools — the model doesn't need to know whether a tool is native or MCP. From its perspective, they're all tools.

Stdio transport (mcp_stdio.rs)

crates/runtime/src/mcp_stdio.rs:480 defines McpServerManager. The transport is stdio — the server is a subprocess, the manager talks to it via stdin/stdout using line-delimited JSON-RPC.

Message types (lines 40–77): JsonRpcRequest, JsonRpcResponse, JsonRpcError. MCP-specific types (lines 80–221) cover Initialize, ListTools, ToolCall, ListResources, ReadResource params/results.

The 2928-LOC file is mostly protocol noise — wire types for every MCP message. The interesting design move is that the manager handles the lifecycle (spawn → handshake → discover → ready) before exposing the tools, so by the time the model sees mcp__server__tool in its tool list, the server is verified live.

Resources

MCP servers can expose resources — read-only addressable content (think: API documentation, database schemas, recent metrics). The ListMcpResources and ReadMcpResource tools let the model browse and fetch resources without conflating them with tools.

This is useful because resources can be referenced in tool calls or in the model's reasoning without being executed. Tools have side effects; resources don't.

Why deferred materialization matters here especially

MCP tool schemas tend to be large — they include parameter docs for each tool, often dozens per server. Loading every MCP tool's full schema at session start would balloon the prompt by tens of thousands of tokens.

The deferred mechanism (covered in The Tool Surface) is what makes MCP scale. The model sees, at session start, the names of every available MCP tool, but not their schemas. When it wants to call one, it does ToolSearch({"query": "select:mcp__server__tool"}) and gets the schema for just that tool. After ~2–3 such lookups in a session, the model has built a working schema set without paying the upfront cost.


Continue: Multi-Agent Coordination

Last updated: May 14, 2026