6. Agents, Skills, Slash Commands, MCP
6.1 Agents (subagents)
The shape — tools/AgentTool/loadAgentsDir.ts:162
type AgentDefinition = BuiltInAgentDefinition | CustomAgentDefinition | PluginAgentDefinitionAll three have:
agentType: string— thesubagent_typethe caller specifies.whenToUse: string— the blurb shown in the AgentTool prompt.tools: string[]— allowlist,['*']for all.disallowedTools?: string[]— denylist.getSystemPrompt: (params?) => string— the system prompt the forked query will use.- Optional
mcpServers,hooks,model,effort,requiredMcpServers,omitClaudeMd.
Built-in agents — tools/AgentTool/built-in/*.ts
Six hardcoded modules:
- generalPurposeAgent.ts —
GENERAL_PURPOSE_AGENT, the default whensubagent_typeis omitted. Prompt reproduced verbatim in doc 07. - exploreAgent.ts —
EXPLORE_AGENT, gated behindfeature('BUILTIN_EXPLORE_PLAN_AGENTS')+ GrowthBooktengu_amber_stoat. - planAgent.ts —
PLAN_AGENT, same gating. - statuslineSetup.ts — helper for configuring the custom status line.
- claudeCodeGuideAgent.ts — answers Claude Code product / SDK / API questions.
- verificationAgent.ts — adversarial verifier for non-trivial implementations (see doc 07 on the "Verification Agent Contract").
Custom and plugin agents
Custom agents are markdown files with YAML frontmatter loaded from ~/.claude/agents/, .claude/agents/, or /etc/claude-code/agents/. The frontmatter schema (AgentJsonSchema, loadAgentsDir.ts:73-99) validates:
name: my-agent
description: when to use it
tools: [Bash, Read, Edit] # or ['*']
disallowedTools: [Write]
mcpServers: [slack, {my: {command: python, args: ['-m', 'x']}}]
requiredMcpServers: [slack] # skip agent if not available
hooks: { PreToolUse: ... }
model: sonnet | haiku | opus
effort: low | high | <number>getAgentDefinitionsWithOverrides() (loadAgentsDir.ts:296-349) loads all three sources concurrently, prioritizes (built-ins first, then plugins, users, projects, flags, managed), filters by requiredMcpServers via filterAgentsByMcpRequirements(), and returns { activeAgents, allAgents }.
Agent dispatch — tools/AgentTool/AgentTool.tsx + runAgent.ts
When the model calls AgentTool({subagent_type, prompt, ...}):
AgentTool.callvalidates the schema, resolves theAgentDefinition.resolveAgentTools()(agentToolUtils.ts:122-170) computes the effective tool pool (wildcard expansion, denylist application, context filtering).- If
isolation: 'worktree',createAgentWorktree()creates a temporary git worktree; cwd is switched for the child; on completion the worktree is cleaned up if no changes, otherwise path + branch are returned (AgentTool.tsx:592-600). - If
isolation: 'remote'(ant-only),registerRemoteAgentTask()schedules a CCR sandbox run. - Otherwise
runAgent()(runAgent.ts:1-84) constructs the subagent context — forkedreadFileState, independent abortController — and callsquery()with the agent's system prompt and tools. - The child's streaming messages are bridged to the parent via a
LocalAgentTask(for background), written to a sidechain transcript, and surfaced to the user through the footer/teammates panel. - On completion, cleanup: worktree removal, MCP server teardown.
Fork subagents — tools/AgentTool/forkSubagent.ts
When isForkSubagentEnabled() and the caller omits subagent_type:
- A synthetic
FORK_AGENTdefinition is used (forkSubagent.ts:60-71). Its system prompt is threaded in viatoolUseContext.renderedSystemPrompt— not re-called — so the cached prefix matches byte-for-byte and the fork shares the parent's prompt cache. - The fork inherits the parent's full message history via
buildForkedMessages()(forkSubagent.ts:99+). tools: ['*']withuseExactTools: trueso the tool schema block is byte-identical too.isInForkChild()prevents recursive forking (forkSubagent.ts:78-89).- The AgentTool prompt shifts to "fork-aware" examples and adds a "When to fork" section (see doc 07).
Forks are cheap because of cache sharing; fresh subagents (subagent_type specified) are expensive but start with clean context.
Verification agent — the adversarial-verifier contract
Gated by tengu_hive_evidence. When enabled, constants/prompts.ts:390-395 adds a hard rule to the session-specific guidance: non-trivial implementations (3+ file edits, backend/API changes, infra changes) require an independent verifier run before reporting completion, and the verifier can only return PASS / FAIL / PARTIAL — you cannot self-assign PARTIAL. On PASS you spot-check 2-3 commands from its report.
6.2 Skills
What is a skill?
A markdown file with YAML frontmatter + body, loaded into a PromptCommand (types/command.ts:24-56):
type PromptCommand = CommandBase & {
type: 'prompt'
progressMessage: string
contentLength: number
getPromptForCommand(args: string, context: ToolUseContext): Promise<ContentBlockParam[]>
context?: 'inline' | 'fork'
agent?: string // agent type when forked
}Four discovery sources
- Bundled — registered at build time via
registerBundledSkill()(skills/bundledSkills.ts:53-99). Files extracted on first invocation. - Disk-based —
~/.claude/skills/(user),.claude/skills/(project),/etc/claude-code/skills/(managed), loaded byloadMarkdownFilesForSubdir('skills', cwd). - Plugin skills —
getPluginSkills()from installed plugins. - MCP skills — MCP prompts converted to
PromptCommandwithsource: 'mcp'.
Skill frontmatter
name: commit
description: Create a git commit with staged changes
whenToUse: "Use when the user wants to commit changes"
model: sonnet
effort: low
hooks: { PreToolUse: ... }
paths: ['*.py', 'src/**'] # glob patterns for auto-discoveryInvocation
SkillTool.call resolves the skill and dispatches based on context:
- inline (default) —
command.getPromptForCommand(args, context)returnsContentBlockParam[]which are expanded directly into the current conversation. - fork —
executeForkedSkill()(SkillTool.ts:122-150) runs the skill in a forked subagent with its own token budget. Theagentfield picks the subagent type.
Presentation
Skills are listed inside SkillTool's prompt in the default mode. Under feature('EXPERIMENTAL_SKILL_SEARCH'), they are injected as separate <system-reminder> attachments ("Skills relevant to your task: …") per turn, with DiscoverSkillsTool as the search gateway — same cache-preservation idea as ToolSearch.
The user-invocable form is simply /skill-name — e.g. /commit, /review-pr, /simplify. The guidance at constants/prompts.ts:382-384 tells the model:
/<skill-name> (e.g., /commit) is shorthand for users to invoke a user-invocable skill. When executed, the skill gets expanded to a full prompt. Use the SkillTool tool to execute them. IMPORTANT: Only use SkillTool for skills listed in its user-invocable skills section - do not guess or use built-in CLI commands.
6.3 Slash commands
Three kinds — types/command.ts:175-206
type Command = CommandBase & (PromptCommand | LocalCommand | LocalJSXCommand)| Type | Behavior |
|---|---|
PromptCommand |
Injects a rendered prompt into the conversation. Model handles it. Skills are this kind. |
LocalCommand |
Runs client-side without the model. E.g. /clear, /cost, /exit. |
LocalJSXCommand |
Like LocalCommand but renders an Ink UI overlay. E.g. /status, /model picker. |
CommandBase provides: name, description, aliases, whenToUse, isEnabled(), isHidden, userInvocable, loadedFrom ('commands_DEPRECATED' | 'skills' | 'plugin' | 'managed' | 'bundled' | 'mcp'), allowedTools.
Registry — commands.ts
A hardcoded set plus lazy-loaded feature-gated ones:
- Hardcoded:
commit,clear,color,help,memory,status, … - Feature-gated:
proactive,brief,bridge,remoteControlServer,voice,agentsPlatform(ant-only),workflowsCmd(WORKFLOW_SCRIPTSgate),clearSkillIndexCache(EXPERIMENTAL_SKILL_SEARCH). - Skill-based: user/project skills via
getSkillDirCommands(), bundled viagetBundledSkills(), plugin viagetPluginSkills().
All are merged into one Command[] array consumed by the model and the slash-command parser.
User-defined slash commands
There is no separate mechanism — a skill / name is the entry form. Write ~/.claude/skills/mycmd.md and /mycmd args works automatically.
6.4 Memory system
The four memory types — memdir/memoryTypes.ts:14
const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'] as constThese are the only types (legacy untyped files degrade gracefully). Each type has a defined when_to_save, how_to_use, body_structure, and multiple examples in the prompt (TYPES_SECTION_INDIVIDUAL and TYPES_SECTION_COMBINED). Reproduced in doc 07.
Storage layout
~/.claude/projects/<project-slug>/memory/
├── MEMORY.md # index, loaded every turn, capped at 200 lines / 25 KB
├── <topic>.md # one file per memory, with frontmatter
└── auto/<topic>.md # auto-extracted by session memory
MEMORY.md is a hand-curated index of one-line pointers (- [Title](file.md) — one-line hook). The recall-side flow (memdir/findRelevantMemories.ts) reads MEMORY.md, scores topic files for relevance to the current user message, then loads the matches on demand.
Directory existence is guaranteed by ensureMemoryDirExists() (memdir/memdir.ts:129). The guidance bullet at memdir/memdir.ts:116-117 is:
This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence).
Added because Claude was burning turns on ls / mkdir -p before writing.
Write path (auto-memory extraction)
services/extractMemories/ runs as a Stop hook after the model completes a turn. It:
- Checks whether extraction is due (token threshold, tool-call count since last extraction).
- Constructs an extraction prompt via
buildExtractCombinedPrompt()/buildExtractAutoOnlyPrompt(). - Spawns a perfect fork via
runForkedAgent()(sharing parent cache). - The fork uses FileWrite / FileRead / FileEdit to update
MEMORY.mdand topic files. - Logs extraction metrics.
Session memory
services/SessionMemory/ is a parallel track — a background-updated transcript of session highlights. Updated by a forked subagent when shouldExtractMemory() fires (based on token delta + tool-call count since last extraction).
CLAUDE.md — utils/claudemd.ts
Project-level instructions loaded in priority order:
/etc/claude-code/CLAUDE.md(managed, global).~/.claude/CLAUDE.md(user, private).CLAUDE.md,.claude/CLAUDE.md,.claude/rules/*.md(project, checked in).CLAUDE.local.md(per-project private).
Supports @include file directives. Files are applied so the closest/latest wins.
Read-only agents (Explore, Plan) pass omitClaudeMd: true to skip this block — they don't need lint/commit/PR guidance, and dropping it is a noticeable token savings.
6.5 MCP integration
Client — services/mcp/client.ts
Claude Code is an MCP client. It supports stdio, SSE, HTTP, WebSocket, and in-process transports (services/mcp/InProcessTransport.ts, SdkControlTransport.ts). For each connected server:
fetchToolsForClient()convertsListToolsResultintoMCPToolinstances.ListMcpResourcesTool+ReadMcpResourceToolwrap resource discovery / reading.ListPromptsResultis converted intoPromptCommands (i.e. skills) withsource: 'mcp'.
Output handling
MCP tool output can be large. mcpContentNeedsTruncation() + truncateMcpContentIfNeeded() cap oversized results; binary outputs can be persisted to disk with a preview returned.
Elicitation — services/mcp/elicitationHandler.ts
When an MCP server needs user interaction (e.g. OAuth completion), it sends an ElicitRequest (form or URL). Handler flow:
executeElicitationHooks()— hooks may auto-respond.- Otherwise UI prompt.
- Server emits
ElicitationCompleteNotificationwhen done.
Channel permissions (experimental)
Gated by tengu_harbor_permissions. Instead of UI prompts, permission requests are sent to integrated channels (Telegram, iMessage, Discord) via MCP notifications. Users reply with a structured format (y|yes|n|no <5-letter-id>, channelPermissions.ts:75). The channel server emits the parsed response as a notification — Claude Code never regex-parses free text. The 5-letter ID is drawn from a set that avoids profanity (ID_AVOID_SUBSTRINGS).
Approval flows
Custom agents' mcpServers frontmatter is subject to strictPluginOnlyCustomization lock (runAgent.ts:118-127): user-authored custom agents can be blocked from spawning new MCP servers to prevent breakout, while plugin / policy / built-in agents still can.
6.6 How these layers compose
A user typing /commit-push-pr triggers:
- The slash parser resolves it to a skill (
commands.ts→ skill registry). SkillToolis invoked with that skill's name.- Skill frontmatter says
context: 'inline'→getPromptForCommand()returns the rendered prompt (which contains a fullgit status/git diff/git logplan and a HEREDOC example for the commit message). - The model then uses
BashTool(with Bash permissions allowinggit *) to execute the steps. - If the skill is
context: 'fork', the whole thing runs in a fork sharing cache — output returned as a single message.
A user typing "search the codebase for all callers of X" might trigger:
- Model calls
AgentTool({ description: 'find callers', subagent_type: 'Explore', prompt: '…' }). runAgentspawns a fork with the Explore agent's system prompt and a restricted tool pool.- The Explore agent makes many
Grep/Readcalls. - Completion returns one summary message to the parent; raw tool output is kept out of the parent's context.