Hooks, Skills & Subagents

Glove ships three extension primitives: /hook directives that mutate agent state, /skill directives that inject context, and subagent factories the main agent routes to via the auto-registered glove_invoke_subagent tool.

Hooks and skills are parsed out of the user's text in processRequest and dispatched before the model sees the turn. Subagents are addressed by the model itself — the user's @name text reaches the model verbatim and acts as a routing signal that nudges the agent to call the dispatch tool with { name, prompt }.

Builders that register no extensions see no behavioural change.

Three primitives

PrimitiveHow it's invokedTypical use
/hookUser-side /name directive. Handler runs before the model with full AgentControls; can rewrite the text or short-circuit the turn.Force compaction, swap model mid-conversation, cancel a turn, run a one-off side effect.
/skillUser-side /name directive that becomes a synthetic user message marked is_skill_injection: true. Optionally exposed to the agent via glove_invoke_skill.Tone presets, persona overlays, attaching a checklist, pulling in a prompt template.
SubagentFactory builds a fresh child Glove on every invocation. The parent agent calls glove_invoke_subagent with { name, prompt }; the framework runs the child and returns its final text 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.

When a user-side directive binds, the original /name token is replaced with a non-triggerable placeholder of the form [invoked_extension__hook_<name>] or [invoked_extension__skill_<name>]. The placeholder survives in the persisted user message — and in the parsedText handed to handlers — so transcripts can show what the user typed without the directive re-firing on a future parse.

Registering extensions

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

lib/agent.tstypescript
import { Glove, MemoryStore, Displaymanager, createAdapter } from "glove-core";

const agent = new Glove({
  store: new MemoryStore("session"),
  model: createAdapter({ provider: "anthropic" }),
  displayManager: new Displaymanager(),
  systemPrompt: "You are a helpful assistant.",
  compaction_config: { compaction_instructions: "Summarize so far." },
})
  .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"})`,
  })
  .defineSubAgent({
    name: "weather",
    description: "Run the weather subagent. Use for weather questions.",
    factory: async ({ prompt, parentStore, parentControls }) => {
      const subStore = await parentStore.createSubAgentStore?.("weather", false)
        ?? new MemoryStore(`weather-${Date.now()}`);
      return new Glove({
        store: subStore,
        model: createAdapter({ provider: "anthropic" }),
        displayManager: parentControls.displayManager,
        systemPrompt: "You answer weather questions concisely.",
        compaction_config: { compaction_instructions: "Summarize so far." },
      }).build();
    },
  })
  .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 /name tokens replaced by placeholders
  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;
  store: StoreAdapter;
  displayManager: DisplayManagerAdapter;
  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.

Each hook invocation also emits a hook_invoked subscriber event with the bound name.

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. Each invocation emits a skill_invoked subscriber event with source: "user".

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

interface SkillContext {
  name: string;
  // when source = "user": user message with bound /name tokens replaced by placeholders.
  // 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. Each agent-side invocation emits a skill_invoked event with source: "agent" and the supplied args.

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 contains the rest of the user message,
    // with the /research-mode token replaced by [invoked_extension__skill_research-mode].
    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
skill_invoked event source"user""agent" (with args)

Content skills (data-driven, sectioned)

defineSkill is the right primitive when the injected content has to be computed at invocation time — environment snapshots, time-aware briefings, anything that closes over runtime state. For coding agents the more common shape is static content: a body of instructions, a reference table, a checklist, sliced into a main file plus accompanying sections. ContentSkill is that shape — pure data, no handler.

Built for agents that may not have filesystem access (web, glovebox, embedded). Skills are loaded lazily through a single glove_read_skill tool whose description embeds a token- budgeted XML listing of every registered skill. The model picks one by name, calls the tool, and the content lands as a tool result. Mirrors Claude Code's on-disk .claude/skills/<name>/SKILL.md convention so you can ship the same skill bundles to either runtime.

content-skills — data typestypescript
interface ContentSkill {
  name: string;
  description: string;
  /** Opaque reader handle (FS path, URL, KV key). Returned alongside content so the agent knows where extended access lives. */
  path?: string;
  /** Main body. Returned when the agent reads with no section. */
  content: string;
  /** Optional named sections (e.g. "api-reference", "examples"). */
  sections?: Record<string, string>;
  /** Higher priority keeps the description in the listing under tight budget. Default 0. */
  priority?: number;
  /** Omit from the listing but still readable by name. Default false. */
  hidden?: boolean;
}

interface SkillReader {
  list(): Promise<SkillSummary[]>;
  read(name: string, section?: string): Promise<SkillReadResult | null>;
}

Registering with useReadSkill

Idiomatic with the useMemoryReader / useContext family from glove-memory. Pass an array of ContentSkills (or a custom SkillReader) and the helper folds the read tool, builds the listing block in its description, and wires user-side /skill-name directives through the existing parser.

lib/agent.tstypescript
import { Glove, MemoryStore, Displaymanager, createAdapter, useReadSkill } from "glove-core";

const agent = new Glove({
  store: new MemoryStore("session"),
  model: createAdapter({ provider: "anthropic" }),
  displayManager: new Displaymanager(),
  systemPrompt: "You are a coding assistant.",
  compaction_config: { compaction_instructions: "Summarize so far." },
});

useReadSkill(agent, [
  {
    name: "python-debug",
    description: "Diagnose Python errors and apply fix patterns.",
    content: "# Python Debugging\nRead tracebacks bottom-up...",
    sections: {
      "api-reference": "# API\npdb.set_trace()...",
      examples: "# Examples\n...",
    },
  },
  {
    name: "git-workflow",
    description: "Branch / commit / PR conventions for this repo.",
    content: "# Git\nNever force-push main...",
  },
]);

agent.build();

// Agent-side: model calls glove_read_skill({ name: "python-debug" })
// User-side: "/python-debug let's debug this" materialises the content
//            as a synthetic user message before the real turn.

The listing budget

The tool description renders an XML-tagged <available_skills> block listing every registered skill. The default budget is 2000 tokens (~2% of a 100K compaction limit). Skills are sorted by priority desc; descriptions fit until the budget is exhausted; everything below the cut falls back to a name-only <skill name="..." /> line so the model still knows every name. If even name-only lines exceed the budget, an <!-- N more skills omitted --> comment is rendered as a fallback.

tuning the budgettypescript
useReadSkill(agent, skills, {
  // Default: 2000 tokens. Tune up if you've raised compaction_context_limit.
  listingBudgetTokens: 4000,

  // Optional: replace the in-memory reader with your own. Useful when the
  // listing or content needs to come from a database, a remote service,
  // or a file system.
  reader: customReader,

  // Optional: override the tool name (default "glove_read_skill").
  toolName: "load_doc",

  // Optional: appended after the listing block in the tool description.
  descriptionSuffix: "Prefer reading api-reference before answering API questions.",

  // Optional: skip the user-side /skill-name wiring. Default true.
  wireUserDirectives: false,
});

The tool contract

glove_read_skill takes { name, section? }. Omitted section returns the main body plus the list of available section names; supplied section returns that slice. Content is wrapped in XML — Claude-family models parse <skill>...<content>...</content></skill> with crisp delimiters that markdown headers can't match. The raw SkillReadResult is also placed on renderData for client-side renderers.

tool result — main bodyxml
<skill name="python-debug" path="/skills/python-debug">
<content>
# Python Debugging

Read tracebacks bottom-up...
</content>
<sections>api-reference,examples</sections>
</skill>

For a section read the wrapper adds a section attribute. Missing skill / missing section both return { status: "error", message: "...", data: null } with the list of known (non-hidden) names. The error message never leaks hidden: true skill names.

Loading skills from disk

For server-side agents that do have a filesystem, the sibling subpath glove-core/content-skills-fs ships loadContentSkillsFromFs — a thin loader that walks a Claude Code-style .claude/skills/ directory and returns a ContentSkill[] ready for useReadSkill. Node-only (uses node:fs), kept in its own subpath so browser bundles don't pull it in.

server-side agent loading skills from .claude/skills/typescript
import { Glove, useReadSkill } from "glove-core";
import { loadContentSkillsFromFs } from "glove-core/content-skills-fs";

const skills = await loadContentSkillsFromFs("./.claude/skills");

// Each subdirectory becomes a ContentSkill. SKILL.md is the main body
// (YAML frontmatter `name` and `description` override the directory
// name). Sibling .md files (api-reference.md, examples.md, ...) become
// sections keyed by filename without the extension.

useReadSkill(agent, skills);
recognised SKILL.md layouttext
.claude/skills/
├── python-debug/
│   ├── SKILL.md           ← optional YAML frontmatter: name, description, priority, hidden
│   ├── api-reference.md   ← section "api-reference"
│   └── examples.md        ← section "examples"
└── git-workflow/
    └── SKILL.md

When to use which skill primitive

Use caseReach for
Static instructions, references, checklists, prompt fragmentsContentSkill + useReadSkill
Live environment snapshots, time-aware briefings, computed contextdefineSkill with a handler
Tone presets, persona overlays, simple text injectionEither works; ContentSkill if it's purely static
Bundled skill ships to multiple runtimes (web + glovebox + CLI)ContentSkill — same bundle, no filesystem assumed

Both coexist on the same agent. useReadSkill registers its skills with exposeToAgent: false internally so glove_invoke_skill never double-lists them; the model discovers content skills exclusively through glove_read_skill.

Subagents

A subagent is an isolated child Glove the main agent routes to via glove_invoke_subagent({ name, prompt }). You register one with a factory — the framework calls it on every invocation, runs the returned runnable with the supplied prompt, and hands its final text back to the parent agent as the tool result.

extensions — subagent typestypescript
type SubAgentFactory = (
  ctx: SubAgentFactoryContext,
) => Promise<IGloveRunnable> | IGloveRunnable;

interface SubAgentFactoryContext {
  /** Subagent name as registered with defineSubAgent. */
  name: string;
  /** The task prompt the parent agent supplied via glove_invoke_subagent. */
  prompt: string;
  /** The parent agent's store. Use createSubAgentStore(name, durable) to derive a child store. */
  parentStore: StoreAdapter;
  /** Full parent agent controls (context, observer, promptMachine, executor, glove, store, displayManager, forceCompaction). */
  parentControls: AgentControls;
}

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

interface DefineSubAgentArgs extends SubAgentOptions {
  name: string;
  factory: SubAgentFactory;
}

The factory contract

The factory runs once per invocation and must return a fully-built IGloveRunnable — i.e. the child Glove must already have build() called on it. The dispatcher then:

  1. Attaches every parent subscriber to the child for the duration of the run, so streaming events from the child fan out to the parent's consumers (UI, voice, logging) as part of the same stream.
  2. Calls child.processRequest(prompt, signal) — the parent's abort signal is forwarded so a parent-side cancel unwinds the child's Agent.ask loop on the next iteration.
  3. Extracts the last agent message's text from the result and returns it as data.content on the tool result.
  4. Detaches the parent subscribers from the child in a finally block so durable factories don't accumulate duplicate subscribers across invocations.

Sub-stores

The factory typically calls parentStore.createSubAgentStore(name, durable) to derive a child store. With durable: false (the default) every invocation gets a fresh store; with durable: true the same child store is returned for the same namespace so the subagent accumulates message history across calls. MemoryStore implements this out of the box, so the common case is friction-free.

Worked example

defining a code-review subagenttypescript
import { Glove, MemoryStore, createAdapter } from "glove-core";

agent.defineSubAgent({
  name: "reviewer",
  description: "Code review specialist. Use when the user asks for a code review.",
  factory: async ({ prompt, parentStore, parentControls }) => {
    // Derive an isolated store from the parent. durable: false → fresh per call.
    const subStore = await parentStore.createSubAgentStore?.("reviewer", false)
      ?? new MemoryStore(`reviewer-${Date.now()}`);

    // Build a fresh child Glove with its own system prompt.
    // Sharing the parent's display manager lets reviewer tools render in the
    // same UI surface; pass a separate one to keep its UI isolated.
    const child = new Glove({
      store: subStore,
      model: createAdapter({ provider: "anthropic" }),
      displayManager: parentControls.displayManager,
      systemPrompt: "You are a senior code reviewer. Be specific and direct.",
      compaction_config: { compaction_instructions: "Summarize so far." },
    });

    // Fold reviewer-specific tools as needed before returning the built runnable.
    return child.build();
  },
});

// User: "@reviewer please look at PR #123"
// The model sees the full text including "@reviewer", picks
// glove_invoke_subagent, and calls it with { name: "reviewer", prompt: "..." }.
// The dispatcher invokes the factory, runs the child, and returns the child's
// final agent text as the tool result.

Tool result shape

Symmetric with glove_invoke_skill. On success: { status: "success", data: { subagent, content } }. When the factory throws or the child run fails: { status: "error", message: "...", data: null }. Unknown subagent names return an error result listing the registered names.

Subscriber bracket events

Every subagent run is bracketed by a matched pair of subscriber events with guaranteed 1:1 symmetry:

  • subagent_invoked — fired by the Executor immediately before the dispatcher runs. Carries { name, prompt }.
  • subagent_completed — fired by the Executor after the dispatcher resolves, errors, or aborts. Carries { name, status: "success" | "error", message? }.

The bracket fires from the Executor (not the dispatcher tool) so that even when an abort signal short-circuits the dispatcher's promise chain, the closing bracket still arrives. Anything the child emits between the open and close brackets — text_delta, tool_use, tool_use_result, even nested subagent brackets — belongs to that subagent run, because the parent's subscribers are temporarily attached to the child for the duration.

Match against the exported SUBAGENT_DISPATCH_TOOL_NAME constant (value: "glove_invoke_subagent") when filtering tool events you want to attribute to subagent dispatch.

Context isolation

Subagents do not see the parent conversation. The only channel from parent to child is the prompt string the agent supplies — the factory is responsible for whatever context the child needs. This isolation keeps the parent context window from bloating with the subagent's intermediate work and matches Claude Code's subagent context model.

Common patterns

  • Fresh-per-call subagent — factory builds a brand new Glove with createSubAgentStore(name, false) each call. Best for stateless reviewers, planners, classifiers.
  • Durable subagent — factory calls createSubAgentStore(name, true) so the child carries message history across invocations. Best for long-running assistants the parent agent dispatches to repeatedly.
  • Deterministic responder — factory returns a tiny Glove with a no-op model adapter that always returns a canned message; bypasses any LLM call inside the subagent.
  • External agent / API proxy — factory returns a minimal IGloveRunnable that proxies processRequest to another service.
  • Multiple in one message — "@reviewer @architect please discuss this design" — both names reach the model, and it decides whether to call both subagents (in sequence or in parallel via separate tool calls).

For memory tools specifically, see the Memory guide and prefer the subagent-delegation pattern: the entity / episodic / resources tools belong on focused retrieval subagents rather than directly on the main agent.

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 replaced in place with a non-triggerable placeholder; unbound tokens stay untouched. @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, emitting hook_invoked for each. Apply any rewriteText; honour the first shortCircuit and return.
  3. Materialise skills (source: "user"), emitting skill_invoked for each — each becomes a synthetic user message persisted before the real one.
  4. Build the real user Message from the placeholder-substituted text (including any @mentions, untouched) plus any non-text ContentParts the caller passed.
  5. Hand the message to Agent.ask. Subagents surface through the agent loop via glove_invoke_subagent tool calls bracketed by subagent_invoked / subagent_completed events.

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>;
})}

The pre_modified_text field

When a hook's rewriteText changes the user's message before the model sees it, the original text is preserved on Message.pre_modified_text. Use this in transcript renderers that want to show the user the text they actually typed (rather than the rewritten version the model received).

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 (also available on the runnable post-build)
defineHook(name: string, handler: HookHandler): this;
defineSkill(args: DefineSkillArgs): this;
defineSubAgent(args: DefineSubAgentArgs): this;

// Auto-registered dispatch tool name — match against this constant
// when filtering tool events.
import { SUBAGENT_DISPATCH_TOOL_NAME } from "glove-core";
// SUBAGENT_DISPATCH_TOOL_NAME === "glove_invoke_subagent"

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

// Content skills — pure-data, lazy-loaded, sectioned.
import {
  useReadSkill,
  createMemorySkillReader,
  createReadSkillTool,
  renderSkillListing,
  renderReadSkillDescription,
  renderSkillReadResultXml,
  defaultListingBudgetTokens,
  READ_SKILL_TOOL_NAME,
  DEFAULT_LISTING_BUDGET_TOKENS,
  type ContentSkill,
  type SkillReader,
  type SkillSummary,
  type SkillReadResult,
} from "glove-core";

// FS loader for Claude Code-style .claude/skills/<name>/SKILL.md layout.
// Subpath because it uses node:fs.
import { loadContentSkillsFromFs } from "glove-core/content-skills-fs";

For full type signatures see the Core API page.