Skip to content

Instantly share code, notes, and snippets.

@stevesolun
Last active October 14, 2026 11:44
Show Gist options
  • Select an option

  • Save stevesolun/b148183707f8c96e197ee6c74a10d951 to your computer and use it in GitHub Desktop.

Select an option

Save stevesolun/b148183707f8c96e197ee6c74a10d951 to your computer and use it in GitHub Desktop.
tool-diet: an agent skill that puts tool results on a diet. Measures which tool, CLI, script or MCP results in a session cost the most tokens for the least use, cuts the fat at its source, proves the saving. Install: npx skills@latest add stevesolun/steveskills --skill=tool-diet. Repo: https://github.com/stevesolun/steveskills

tool-diet

An agent skill that puts tool results on a diet: it measures which tool, CLI, script or MCP results in a session cost the most tokens for the least use, cuts the fat at its source, and proves the saving with before and after numbers.

Works in Claude Code, Codex, Copilot, Cursor and any other harness that reads the Agent Skills format. By Steve Solun. MIT.

Install

npx skills@latest add stevesolun/steveskills --skill=tool-diet
npx skills@latest update tool-diet

Or, in Claude Code, as a plugin that updates itself:

claude plugin marketplace add stevesolun/steveskills
claude plugin install steveskills@steveskills

Pick one route per agent; installing both gives you the skill twice.

The idea

Every tool result lands in the context window and competes with your instructions for attention on every turn that follows. Most of that text is never used: a 2,000-line file read for one function, an install log, a list with no limit, the same lookup run three times. That is fat, and it is paid for again on every later turn of the session.

tool-diet does one thing: find the fat, cut it where it is produced, and show the number.

How it runs

The agent reaches for it when a tool result floods the context, the same lookup keeps repeating, a CLI or MCP server returns far more than the task used, or you ask why a session burned tokens. Typing /tool-diet works too.

  1. Measure. scripts/rank-tool-results.py (Python, standard library only) finds the newest session log for the current directory and ranks every tool result by size, flagging repeats, errors and truncated outputs, with totals by tool. Without a log, the agent ranks the results in its context by hand.
  2. Name the fat. Each large result is matched against eight kinds, each with its own cut: Unfiltered, Unbounded, Chatty, Repeated, Truncated, Wrong tool, Raw blob, Missing wrapper.
  3. Cut it where the next session finds it. Cheapest durable cut first: a flag the tool already has, a wrapper script with a --help, a narrower MCP tool or sharper tool description, and only as a last resort one line in the agent instructions.
  4. Prove it. The same lookup, run lean, with tokens before → after per cut and the share of the session's tool tokens recovered.

Sample output

Log: ~/.claude/projects/-home-me-app/3f1c....jsonl
Tool results: 127  Estimated tokens: 118,761

Largest results (top 6, 28% of all result tokens)
  tokens  tool             flags                  call
   8,241  Read                                    Read file_path=~/.claude/projects/.../tool-results/b5wz.txt
   8,145  Read                                    Read file_path=~/.claude/projects/.../tool-results/b4dk.txt
   5,022  Bash                                    Bash command=head -120 CHANGELOG.md && grep -n '^## ' ...
   4,344  Read                                    Read file_path=README.md

Truncated results (the full output was saved to a file, so reading it pays twice)
     545  Bash             Bash command=for f in docs/*.md; do cat "$f"; done
     528  Bash             Bash command=for f in adr/*.md; do cat "$f"; done

By tool
  93,901    79%    69 calls  Read
  13,001    11%    16 calls  Bash

The two biggest results here are the agent reading back outputs the harness had already cut and saved: the same text, paid for twice, about 16,000 tokens. The cut is in the first command (fewer files per loop, or a search instead of a dump).

Why not a rulebook

Most "token efficiency" skills are lists of habits: be concise, avoid re-reading, set a budget. A rule the model already follows changes nothing, and a rule it ignores changes nothing either. tool-diet points at a specific call and a token count, and the result of the work is a measurement, so you can tell whether it did anything.

Layout

This gist is the flat view of the skill. The installable source is the repo stevesolun/steveskills, where the three skill files sit like this, which is what npx skills add and the Claude Code plugin expect:

tool-diet/
├── SKILL.md                      # the skill
├── agents/openai.yaml            # Codex and ChatGPT picker metadata
└── scripts/rank-tool-results.py  # ranks a session log; --help for options

License

MIT

MIT License
Copyright (c) 2026 Steve Solun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
interface:
display_name: "Tool Diet"
short_description: "Cut fat tool results, prove the saving"
#!/usr/bin/env python3
"""Rank a session's tool results by size, repeats and errors.
Usage:
rank-tool-results.py [--log PATH] [--top N] [--json]
Without --log, picks the newest session log it can find for the current
directory: Claude Code (~/.claude/projects/<cwd-slug>/*.jsonl) or Codex
(~/.codex/sessions/**/*.jsonl). Any JSONL whose records carry
tool_use/tool_result (Anthropic shape) or function_call/function_call_output
(OpenAI shape) blocks works; wrapper fields around them are ignored.
Tokens are estimated at four characters per token. Stdlib only.
"""
import argparse
import glob
import json
import os
import sys
from collections import defaultdict
CHARS_PER_TOKEN = 4
TRUNCATION_MARKERS = ("Output too large", "persisted-output", "[truncated]")
SIGNATURE_KEYS = ("file_path", "command", "pattern", "query", "url", "path",
"notebook_path", "skill", "prompt", "cmd", "old_string",
"offset", "limit")
def find_log():
"""Newest Claude Code log for this cwd, else newest Codex rollout."""
home = os.path.expanduser("~")
slug = os.getcwd().replace("/", "-").replace("\\", "-")
candidates = glob.glob(os.path.join(home, ".claude", "projects", slug, "*.jsonl"))
candidates += glob.glob(os.path.join(home, ".codex", "sessions", "**", "*.jsonl"),
recursive=True)
if not candidates:
return None
return max(candidates, key=os.path.getmtime)
def text_size(content):
"""Characters of text in a result payload (string, list of blocks, or other)."""
if content is None:
return 0
if isinstance(content, str):
return len(content)
if isinstance(content, list):
total = 0
for block in content:
if isinstance(block, dict):
total += len(block.get("text") or "")
if block.get("type") == "image":
total += 1500 * CHARS_PER_TOKEN # images cost real tokens too
else:
total += len(str(block))
return total
return len(json.dumps(content))
def text_sample(content, limit=400):
if isinstance(content, str):
return content[:limit]
if isinstance(content, list):
return " ".join((b.get("text") or "") if isinstance(b, dict) else str(b)
for b in content)[:limit]
return json.dumps(content)[:limit]
def signature(name, inputs):
"""Tool name plus the one input that identifies the call (full value, so
two calls only count as a repeat when they really are the same call)."""
if isinstance(inputs, str):
try:
inputs = json.loads(inputs)
except ValueError:
return f"{name} {inputs}"
if not isinstance(inputs, dict):
return name
parts = [f"{key}={inputs[key]}" for key in SIGNATURE_KEYS
if isinstance(inputs.get(key), (str, int)) and inputs.get(key) != ""]
if parts:
return f"{name} {' '.join(parts)}"
for value in inputs.values():
if isinstance(value, str) and value:
return f"{name} {value}"
return name
def walk(node, calls, results):
"""Collect tool calls and results from any nesting of dicts and lists."""
if isinstance(node, dict):
kind = node.get("type")
if kind == "tool_use" and "id" in node:
calls[node["id"]] = (node.get("name", "?"), node.get("input"))
elif kind == "function_call" and "call_id" in node:
calls[node["call_id"]] = (node.get("name", "?"), node.get("arguments"))
elif kind == "tool_result" and "tool_use_id" in node:
results.append((node["tool_use_id"], node.get("content"),
bool(node.get("is_error"))))
elif kind == "function_call_output" and "call_id" in node:
results.append((node["call_id"], node.get("output"), False))
for value in node.values():
walk(value, calls, results)
elif isinstance(node, list):
for item in node:
walk(item, calls, results)
def load(path):
calls, results = {}, []
with open(path, encoding="utf-8", errors="replace") as fh:
for line in fh:
line = line.strip()
if not line:
continue
try:
walk(json.loads(line), calls, results)
except json.JSONDecodeError:
continue
return calls, results
def analyse(calls, results):
rows = []
for call_id, content, is_error in results:
name, inputs = calls.get(call_id, ("?", None))
sample = text_sample(content)
rows.append({
"tokens": text_size(content) // CHARS_PER_TOKEN,
"tool": name,
"call": signature(name, inputs),
"error": is_error,
"truncated": any(m in sample for m in TRUNCATION_MARKERS),
})
by_call, by_tool = defaultdict(list), defaultdict(list)
for row in rows:
by_call[row["call"]].append(row["tokens"])
by_tool[row["tool"]].append(row["tokens"])
for row in rows:
row["repeats"] = len(by_call[row["call"]])
total = sum(r["tokens"] for r in rows) or 1
return {
"calls": len(rows),
"total_tokens": total,
"results": sorted(rows, key=lambda r: r["tokens"], reverse=True),
"repeated": sorted(
[{"call": c, "count": len(t), "tokens": sum(t)}
for c, t in by_call.items() if len(t) > 1],
key=lambda r: r["tokens"], reverse=True),
"by_tool": sorted(
[{"tool": n, "calls": len(t), "tokens": sum(t), "share": sum(t) / total}
for n, t in by_tool.items()],
key=lambda r: r["tokens"], reverse=True),
}
def flags(row):
out = []
if row["repeats"] > 1:
out.append(f"repeat x{row['repeats']}")
if row["error"]:
out.append("error")
if row["truncated"]:
out.append("truncated+persisted")
return ", ".join(out)
def report(path, data, top):
print(f"Log: {path}")
print(f"Tool results: {data['calls']} Estimated tokens: {data['total_tokens']:,}")
head = data["results"][:top]
print(f"\nLargest results (top {len(head)}, "
f"{sum(r['tokens'] for r in head) / data['total_tokens']:.0%} of all result tokens)")
print(f"{'tokens':>8} {'tool':<16} {'flags':<22} call")
for row in head:
print(f"{row['tokens']:>8,} {row['tool'][:16]:<16} {flags(row):<22} {row['call'][:70]}")
if data["repeated"]:
print("\nRepeated calls (same call, same input)")
for row in data["repeated"][:top]:
print(f"{row['tokens']:>8,} x{row['count']:<3} {row['call'][:80]}")
truncated = [r for r in data["results"] if r["truncated"]]
if truncated:
print("\nTruncated results (the full output was saved to a file, so reading it pays twice)")
for row in truncated[:top]:
print(f"{row['tokens']:>8,} {row['tool'][:16]:<16} {row['call'][:70]}")
print("\nBy tool")
for row in data["by_tool"][:top]:
print(f"{row['tokens']:>8,} {row['share']:>5.0%} {row['calls']:>4} calls {row['tool']}")
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--log", help="session log (JSONL); default: newest found")
parser.add_argument("--top", type=int, default=15, help="rows per table (default 15)")
parser.add_argument("--json", action="store_true", help="emit JSON instead of text")
args = parser.parse_args()
if args.log and not os.path.isfile(args.log):
sys.exit(f"No file at {args.log}.")
path = args.log or find_log()
if not path:
sys.exit("No session log found for this directory. Pass --log PATH (a JSONL session log).")
calls, results = load(path)
if not results:
sys.exit(f"No tool results recognised in {path}. "
"Expected tool_use/tool_result or function_call/function_call_output records.")
data = analyse(calls, results)
if args.json:
data["log"] = path
json.dump(data, sys.stdout, indent=2)
print()
else:
report(path, data, args.top)
if __name__ == "__main__":
try:
main()
except BrokenPipeError: # output piped into head or similar
sys.exit(0)
name tool-diet
description Put tool results on a diet. Measure which tool, CLI, script or MCP results in a session cost the most tokens for the least use, cut the fat at its source, and prove the saving. Use when a tool result floods the context, the same lookup keeps repeating, a CLI or MCP server returns far more than the task used, or the user asks why a session burned tokens. Not for shortening the agent's own replies.
metadata
author version
Steve Solun
1.0

Tool Diet

Every tool result lands in the context window and competes with the instructions for attention on every turn that follows. A result is lean when the task used nearly all of it. The rest is fat: tokens paid for nothing, on every later turn. Find the fat, cut it where it is produced, prove the saving.

Quote commands and results with secrets redacted: <REDACTED> in place of any key, token or password.

1. Measure

Measure from the session's own tool results, as they came back, not as remembered. If the harness keeps a session log on disk, run scripts/rank-tool-results.py (it finds the newest log for this directory; --help for options). It ranks every result by size, flags repeats, errors and truncated outputs, and totals by tool. Without a log, rank the results in this context by hand: characters divided by four is the token estimate.

For each of the ten largest results, work out what the task used: the lines quoted, the fields acted on, the one fact extracted. The fat is the rest.

Done when each of the ten largest results (or all of them, if fewer) carries the tool, the call, its tokens, and what the task used from it.

2. Name the fat

Match each large result against these kinds. Each reads what it is → the cut.

  • Unfiltered: the whole thing came back when the task wanted a slice (a 2,000-line file for one function, full JSON for one field). → Filter at the source: a search pattern, a line range, a field selector, head.
  • Unbounded: a list, log or history with no limit. → Cap it: -n, --limit, tail, a time window.
  • Chatty: banners, progress bars, ANSI colour, install logs, verbose test runners, full stack traces. → Quiet flags (-q, --silent, --no-progress, CI=1), then tee the full output to a file and keep only the signal lines.
  • Repeated: the same call again, because the first result scrolled away or was never kept. → Save the result to a file once and read the slice, or write the one fact into a note the next step reads.
  • Truncated: the harness cut the output, saved it to a file, and the file was then read in full: paid twice. → Produce less in the first place (Unfiltered and Unbounded usually apply), or write to a file deliberately and read the slice.
  • Wrong tool: a general tool where a specific one exists (cat then grep where a search tool exists, curl plus parsing where the CLI has a field flag, a screenshot where a DOM query answers). → Reach for the specific tool.
  • Raw blob: minified bundles, lockfiles, generated code, base64, binary dumps. → Skip them by default; extract the one field with a parser.
  • Missing wrapper: a multi-step lookup rebuilt by hand in every session. → One script in the repo with a --help, so the lookup is one short command.

3. Cut it where the next session finds it

Before touching the repo, show the user the fat you found and the cut you intend for each; if they are away, proceed. Cut the fattest result first. Prefer the cheapest cut that outlives this session, in this order:

  1. A flag the tool already has. Record nothing: the tool's own --help is the source of truth.
  2. A wrapper script in the repo, with a --help, where the lean call is a pipeline nobody would guess.
  3. A narrower tool, a parameter, or a sharper tool description on the MCP server, so the lean form is the one the agent picks. The description decides which call gets made.
  4. One line in the repo's agent instructions, only for a lean form that most sessions need and none would guess. That file loads on every turn, so everything else stays out of it.

A cut applied only to the current call is right when the fat is local to this task. Say so, so nobody hunts for a durable cut later.

4. Prove it

Run the lookup the lean way and measure it. Report one line per cut: the call, the kind of fat, the cut, tokens before → after. End with one line: tool-result tokens recovered, as a share of the session's total.

Done when every cut has a before and after measurement, every durable cut lives in the environment (the tool's flag, a script in the repo, an MCP description, a line in the agent instructions), and every one-off is labelled as one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment