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.
| Token | What it does | Typical use |
|---|---|---|
/hook | Runs 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. |
/skill | Materialises 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. |
@mention | Registers 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.
Three new builder methods complement fold. They're chainable and legal at any time, including after build() — the same as fold.
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.
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.
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.
// 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 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.
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;
}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.
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 }.
| Aspect | User /skill | Agent glove_invoke_skill |
|---|---|---|
| How it lands in context | Synthetic user message before the real turn (is_skill_injection: true) | Tool result on the agent's tool_use |
SkillContext.source | "user" | "agent" |
SkillContext.args | undefined | free-form string the model supplied |
Gated by exposeToAgent | No — user-invoked always works | Yes — only exposed skills are callable |
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.
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;
}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.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 }.
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.
Glove instance with its own model + system prompt and calls subGlove.processRequest(prompt).@status, @help, @version.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:
/ directives from the raw text.rewriteText; honour the first shortCircuit and return.source: "user") — each becomes a synthetic user message persisted before the real one.Message from the stripped text (including any @mentions, untouched) plus any non-text ContentParts the caller passed.Agent.ask. Mentions surface through the agent loop via glove_invoke_subagent tool calls.is_skill_injection flagSkill-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.
{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>;
})}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.
// 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.