SLOPSHOPPER

cc-lean-system-tools

Moves rarely used built-in tools behind ToolSearch so their schemas stay out of every turn's context

new
v0.2.0no licenseupdated 2026-10-15TrueNight/claude-tools/plugins/cc-lean-system-tools
A shopper browsing a rack in a slop shop
README

cc-lean-system-tools

A Claude Code plugin that shrinks the tool definitions sent with every request by moving rarely used built-in tools behind ToolSearch.

Claude Code sends the full schema of each non-deferred built-in tool on every turn. Some of them are large and seldom used: the Artifact tool alone is about half of all the tool definitions sent upfront. This plugin marks these tools as deferred, so the model sees only their names and loads a schema through ToolSearch when it actually needs the tool:

  • Artifact
  • Workflow
  • ScheduleWakeup
  • ReportFindings
  • ReadNotifications
  • ListAgents
  • AskUserQuestion

In the author's sessions this cut the tool definitions sent with each request by about 80%. Every tool stays available.

Install

At the prompt of a terminal session:

/plugin install cc-lean-system-tools --marketplace TrueNight/claude-tools

Answer y to add the marketplace, then press Enter to install at the user scope.

From a shell:

claude plugin install cc-lean-system-tools --marketplace TrueNight/claude-tools

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with TrueNight/claude-tools, then Add plugin.

Checking it works

/context may keep showing the old "System tools" number. To see what a session really sends, open its transcript (~/.claude/projects/<project>/<session-id>.jsonl) and look at the api-request-shape record: its shape.tools entries without defer_loading are the tools sent in full.

Changing the list

The defer option holds the tool names to defer, and defaults to the list above. Your own list replaces the default rather than adding to it, and an empty list defers nothing.

From a shell, pass the names as one comma-separated string:

echo '{"defer": "Artifact, Workflow, ScheduleWakeup, WebFetch"}' | claude plugin configure cc-lean-system-tools@claude-tools --values-stdin

Inside Claude Code, run /plugin configure cc-lean-system-tools@claude-tools. Or set it under pluginConfigs in ~/.claude/settings.json, as an array or a comma-separated string:

{
  "pluginConfigs": {
    "cc-lean-system-tools@claude-tools": {
      "options": {
        "defer": ["Artifact", "Workflow", "ScheduleWakeup", "WebFetch"]
      }
    }
  }
}

Names match exactly, MCP tools included (mcp__<server>__<tool>). Start a new session for a change to take effect.

To remove a tool from context entirely instead of deferring it, add its bare name to permissions.deny in settings.json.

Notes

  • Subagents may not be able to load a deferred built-in tool: anthropics/claude-code#90085 reports that they have no ToolSearch. If your subagents rely on one of these tools, take it off the list.
  • With a tool deferred, Claude reaches for it less often unprompted, since it only sees the tool's name. Ask for it by name when you want it.
  • Built on the Claude Code plugin hooks API (tool.describe), which is in early access and may change between releases. Written against Claude Code 2.1.300.
Source 1 files
hooks/register.ts 33 lines
1import type { Register } from 'claude-code'
2
3const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
4
5// settings.json can hold the list as an array, but `claude plugin configure`
6// saves it as one comma-separated string.
7const toolNames = (value: unknown): string[] =>
8  (Array.isArray(value) ? value : typeof value === 'string' ? value.split(',') : [])
9    .map(name => String(name).trim())
10    .filter(name => name !== '')
11
12// The `defer` option's default in plugin.json lists built-in tools few
13// sessions call: carrying each full schema on every turn costs more than one
14// ToolSearch load in the session that does. A user's list replaces it.
15export const register: Register = (on, options) => {
16  const tools = toolNames(options.defer)
17  if (tools.length === 0) return
18
19  const deferred = new RegExp(`^(${tools.map(escapeRegExp).join('|')})$`)
20
21  on('tool.describe', { tool: deferred }, async ($, e, next) => ({
22    ...(await next(e)),
23    isDeferred: true,
24  }))
25
26  // A resumed session replays the tool placement it recorded; asking again
27  // applies the deferral there too.
28  on('session.start', ($, e, next) => {
29    $.ui.invalidate('tool.describe')
30    return next(e)
31  })
32}
33