Hooks, Skills & Mentions

Glove ships three extension primitives: /hook directives that mutate agent state, /skill directives that inject context, and @mention subagents the main agent can route to via a built-in tool.

Hooks and skills are parsed out of the user's text in processRequest and dispatched before the model sees the turn. Mentions, following Claude Code's subagent convention, are not parsed — the user's @name text reaches the model verbatim and acts as a routing signal that nudges the agent to call the auto-registered glove_invoke_subagent tool.

Builders that register no extensions see no behavioural change.

Three kinds of directive

TokenWhat it doesTypical use
/hookRuns a builder-defined handler with full access to agent internals. Can rewrite the user text or short-circuit the turn.Force compaction, swap model mid-conversation, cancel a turn, run a one-off side effect.
/skillMaterialises into a synthetic user message persisted before the real one, marked is_skill_injection: true.Tone presets, persona overlays, attaching a checklist, pulling in a prompt template.
@mentionRegisters a subagent. The main agent calls the auto-registered glove_invoke_subagent tool with a name + prompt; the subagent's output comes back as the tool result.Specialised reviewers, planners, deterministic responders, hand-offs to external agents.

/hook and /skill only bind when the name matches a registered handler — paths like /usr/local/bin survive untouched. @mention tokens are never parsed by glove at all, so emails like a@b.com reach the model unchanged.

Registering extensions

Three new builder methods complement fold. They're chainable and legal at any time, including after build() — the same as fold.

lib/agent.tstypescript
import { Glove } from "glove-core";

const agent = new Glove({ /* store, model, displayManager, systemPrompt, ... */ })
  .defineHook("compact", async ({ controls }) => {
    await controls.forceCompaction();
  })
  .defineHook("stop", async () => ({
    shortCircuit: {
      message: { sender: "agent", text: "Stopped." },
    },
  }))
  .defineSkill({
    name: "concise",
    description: "Tighter, snappier responses",
    exposeToAgent: true,
    handler: async ({ source, args }) =>
      `Be terse. (source=${source}, hint=${args ?? "none"})`,
  })
  .defineMention({
    name: "weather",
    description: "Run the weather subagent. Use for weather questions.",
    handler: async ({ prompt }) => fetchWeather(prompt),
  })
  .build();

await agent.processRequest("/concise tell me about Rust");
await agent.processRequest("/compact what's next?");
// "@weather" reaches the model verbatim. The model then calls
// glove_invoke_subagent({ name: "weather", prompt: "NYC" }).
await agent.processRequest("@weather NYC");

Anything not matching a registered name is left in place. A user sending "ping /me at 3pm" with no /me hook keeps the slash intact.

Hooks

A hook is the most powerful primitive — it runs before the model sees the message and receives an AgentControls handle. Use it when you need to reach into the agent's internals.

extensions — hook typestypescript
type HookHandler = (ctx: HookContext) => Promise<HookResult | void>;

interface HookContext {
  name: string;
  rawText: string;        // original user text
  parsedText: string;     // text with bound tokens removed
  controls: AgentControls;
  signal?: AbortSignal;
}

interface HookResult {
  rewriteText?: string;   // override parsedText for downstream skills + the user message
  shortCircuit?:
    | { message: Message }
    | { result: ModelPromptResult };
}

interface AgentControls {
  context: Context;
  observer: Observer;
  promptMachine: PromptMachine;
  executor: Executor;
  glove: IGloveRunnable;
  forceCompaction: () => Promise<void>;
}

Hooks run sequentially in the order their tokens appear in the message. Returning { rewriteText } replaces the working text passed to subsequent hooks, skills, and the final user message. Returning shortCircuit persists the user message and immediately returns the supplied Message or ModelPromptResult — the model is not called.

Common hook recipes

hook recipestypescript
// Force compaction on demand. Useful for "/compact" before a long question.
agent.defineHook("compact", async ({ controls }) => {
  await controls.forceCompaction();
});

// Swap to a stronger model for one specific turn.
agent.defineHook("opus", ({ controls }) => {
  controls.glove.setModel(opusAdapter);
  return; // no rewrite, no short-circuit
});

// Cancel the turn entirely with a canned response.
agent.defineHook("cancel", async () => ({
  shortCircuit: {
    message: { sender: "agent", text: "Cancelled — nothing was sent to the model." },
  },
}));

// Rewrite the user message before any skill / model call.
agent.defineHook("formal", async ({ parsedText }) => ({
  rewriteText: parsedText.replace(/\bgonna\b/g, "going to"),
}));

Skills

Skills inject context. When a /skill token binds, its handler returns a string or ContentPart[]; Glove turns that into a synthetic user-role message persisted via context.appendMessages before the real user message, marked with is_skill_injection: true so consumers can style or filter them in the transcript.

extensions — skill typestypescript
type SkillHandler = (ctx: SkillContext) => Promise<string | ContentPart[]>;

interface SkillContext {
  name: string;
  // when source = "user": user message after token stripping.
  // when source = "agent": same as args ?? "" (the model-supplied string).
  parsedText: string;
  args?: string;             // model-supplied free-form args (only when source = "agent")
  source: "user" | "agent";
  controls: AgentControls;
}

interface SkillOptions {
  description?: string;       // shown to the agent in the invoke-skill tool
  exposeToAgent?: boolean;    // default false
}

// defineSkill takes an object form mirroring fold(GloveFoldArgs).
interface DefineSkillArgs extends SkillOptions {
  name: string;
  handler: SkillHandler;
}

Letting the agent pull skills mid-turn

Set exposeToAgent: true on a skill and Glove auto-registers a single glove_invoke_skill tool. Its description lists every exposed skill (with the description you supply) and is rebuilt in place whenever a new exposed skill is defined — so additions registered post-build() are immediately visible to the model.

exposing a skill to the agenttypescript
agent.defineSkill({
  name: "research-mode",
  description: "Switch to long-form research mode with citations",
  exposeToAgent: true,
  handler: async ({ source, args, parsedText }) => {
    if (source === "agent") {
      // Agent invoked via glove_invoke_skill — args is the model-supplied string.
      return `Switch into research mode. Focus: ${args ?? "general"}.`;
    }
    // source === "user" — parsedText is the rest of "/research-mode <text>".
    return `Switch into research mode. User said: ${parsedText}`;
  },
});

// User can invoke it inline:
//   "/research-mode tell me about ribosomes"
// or the agent can invoke it as a tool:
//   glove_invoke_skill({ name: "research-mode", args: "ribosome assembly" })

Tool result for glove_invoke_skill on success with a string handler return is { status: "success", data: { skill, content } }. When the handler returns a ContentPart[], text parts are joined into data.content (visible to the model) and the full part list is preserved on renderData (visible to client renderers, mirroring the MCP-bridge convention). On unknown or unexposed names the tool returns { status: "error", message: "Skill ... is not available", data: null }.

User-invoked vs agent-invoked

AspectUser /skillAgent glove_invoke_skill
How it lands in contextSynthetic user message before the real turn (is_skill_injection: true)Tool result on the agent's tool_use
SkillContext.source"user""agent"
SkillContext.argsundefinedfree-form string the model supplied
Gated by exposeToAgentNo — user-invoked always worksYes — only exposed skills are callable

Mentions (Subagents)

Mentions are Glove's subagent surface, modelled directly on Claude Code's subagent convention. Defining one auto-registers a single glove_invoke_subagent tool the main agent can call with { name, prompt }. The handler runs in isolation, returns text (or ContentPart[]), and that output comes back as the tool result.

The user's @name text in the original message is not parsed or stripped. It reaches the model verbatim and acts as a routing signal — when the agent sees @reviewer please look at this and glove_invoke_subagent in its tool list, it picks the right subagent and writes the task prompt itself. This matches how Claude Code routes subagents: one tool, one mechanism, whether the invocation came from the user or from the agent's own decision.

extensions — mention typestypescript
type MentionHandler = (ctx: MentionContext) => Promise<string | ContentPart[]>;

interface MentionContext {
  name: string;
  prompt: string;            // task prompt the agent supplied via the tool
  controls: AgentControls;
  signal?: AbortSignal;
}

interface MentionOptions {
  description?: string;       // shown to the agent in the invoke-subagent tool
}

// defineMention takes an object form mirroring fold(GloveFoldArgs).
interface DefineMentionArgs extends MentionOptions {
  name: string;
  handler: MentionHandler;
}

Registering a subagent

defining a subagenttypescript
agent.defineMention({
  name: "reviewer",
  description: "Code review specialist. Use when the user asks for a code review.",
  handler: async ({ prompt }) => {
    // The subagent runs in isolation — `prompt` is its only input.
    // Common pattern: spin up another Glove instance with its own system prompt.
    return await reviewerGlove.processRequest(prompt).then(r => r.messages[0]?.text ?? "");
  },
});

// User: "@reviewer please look at PR #123"
// Model sees the full text including "@reviewer", picks glove_invoke_subagent,
// and calls it with { name: "reviewer", prompt: "review PR #123 ..." }.
// The handler's return text becomes the tool result.

Tool result shape

Symmetric with glove_invoke_skill. On success with a string handler return: { status: "success", data: { subagent, content } }. For ContentPart[] returns, text parts are joined into data.content and the full part list is preserved on renderData. Unknown subagent names return { status: "error", message: "...", data: null }.

Context isolation

Subagents do not see the parent conversation. The only channel from parent to subagent is the prompt string the agent supplies — the handler is responsible for whatever context the subagent needs. If you spin up a sub-Glove inside the handler, give it its own system prompt and store. This isolation matches Claude Code's subagent context model and keeps the parent context window from bloating with the subagent's intermediate work.

Common patterns

How parsing works

processRequest walks the incoming text once, looking only for /name directive tokens (regex (^|\\s)\\/([A-Za-z][\\w-]*)(?=\\s|$)). For every match it asks the hook then skill registry whether the name binds. Bound tokens are removed (with surrounding whitespace collapsed); unbound tokens stay in place. @name tokens are not parsed — they pass through to the model verbatim.

The dispatch order on a single turn is:

  1. Parse / directives from the raw text.
  2. Run hooks in document order. Apply any rewriteText; honour the first shortCircuit and return.
  3. Materialise skills (source: "user") — each becomes a synthetic user message persisted before the real one.
  4. Build the real user Message from the stripped text (including any @mentions, untouched) plus any non-text ContentParts the caller passed.
  5. Hand the message to Agent.ask. Mentions surface through the agent loop via glove_invoke_subagent tool calls.

The is_skill_injection flag

Skill-materialised messages set is_skill_injection: true on Message. Use it in your transcript renderer to distinguish them from real user turns — render in a muted style, collapse them by default, or filter them out entirely. Pair it with the existing is_compaction flag for similar treatment.

rendering exampletsx
{messages.map((m, i) => {
  if (m.is_skill_injection) {
    return <div key={i} className="skill-injection">{m.text}</div>;
  }
  if (m.is_compaction) return null;
  return <div key={i}>{m.sender}: {m.text}</div>;
})}

Forcing compaction

controls.forceCompaction() calls Observer.runCompactionNow(), which is the same body as tryCompaction minus the token-threshold guard. It's safe to call whenever a hook fires. Subscribers see the usual compaction_start / compaction_end events.

API surface

glove-core/extensionstypescript
// Builder methods
defineHook(name: string, handler: HookHandler): this;
defineSkill(args: DefineSkillArgs): this;
defineMention(args: DefineMentionArgs): this;

// Standalone helpers (rarely needed)
import { parseTokens, formatSkillMessage, createSkillInvokeTool } from "glove-core";

For full type signatures see the Core API page.