As large language models become more important for companies, the overall cost of enterprise models is skyrocketing. An inefficient call from an employee might not seem too dangerous, but multiply that by the number of prompts per month and the number of employees in your company.
In this article, we will:
- Understand tokens and how costs are calculated in major LLMs.
- Review Code Claude token optimization techniques, like hooks.
- Learn ways to save tokens both in the prompt’s input and output.
- Explore what the context window is and how we can modify it.
Here are the numbers for the cost reduction in the exercise we ran in this article:
An Introduction to Token Costs
Large language models process language by splitting text into tokens. Like humans splitting food during digestion, models need tokenization to properly understand and process text. A token is the smallest unit of meaningful text, which isn’t always a word. But as a rule of thumb, 100 tokens equal 75 words.
You can see how a text is split into tokens with a tool like Tokenizer from OpenAI:
Most LLMs have a quota based on tokens spent, either a fixed limit per billable period or a cost per token.
Let’s break down the ways tokens are spent in a prompt:
- Input tokens are the text, files, and instructions you send to the model.
- Output tokens are the answers, code, or responses the model generates.
- Hidden tokens are the output tokens that represent the model’s internal reasoning. They’re called hidden since you can’t really see them in the actual response.
Cost, however, is not 100% predictable because LLMs return non-deterministic results. This means that they generate the output by predicting a probability distribution, so the result may vary even with the same input.
Now that we know how tokens relate directly to costs, let’s look at ways to reduce token usage.
#1 Hooks: Let the Code Do It
Imagine that you’re using Claude Code for a software project with tests in it. Ideally, it will work in a loop like “fix bugs, run the tests, and read the results”. But test suites can print thousands of lines per run, so you might want to filter out only the important lines.
You don’t need an LLM to do this text processing. A script can do this deterministic work much better, faster, and cheaper.
This is where hooks come in handy.
In Claude Code, hooks are scripts, endpoints, or MCP tool calls meant to be run outside of the model. They perform specific tasks without the model needing to reason, saving tokens in the process.
Every Claude Hook has three elements that can be configured:
- Trigger: the moment in the lifecycle the hook is set off.
- Matcher: a condition that is evaluated and, when passed, it executes the action.
- Action: represents the code part, either a shell command, MCP, or HTTP call.
The triggers are tied to the lifecycle of a prompt, so they are evaluated at specific times:
This is where each trigger is set off:
- SessionStart – Whenever a session begins or resumes.
- UserPromptSubmit – Whenever the user hits enter and submits a prompt.
- PreToolUse – Whenever Claude has decided on a tool call, but before the tool runs.
- PostToolUse – After the tool runs.
- Stop – Whenever Claude finishes responding.
Keep in mind this is a reduced list. You can review the full lifecycle steps here.
This way, you can use hooks that let you save tokens like the following:
A PreToolUsethat filters the input file to trim all the important parts (grep, tail, etc.).- A
PostToolUsethat filters the tool result to only display the error line in a log.
Exercise: Filter out the Input
Imagine we have a products.csv file with 100 products like the following:
id,name,category,color,price,stock
SKU-1000,Ultra Runner 0,Training,White,64.64,377
SKU-1001,Ultra Tee 1,Running,Grey,104.38,15
SKU-1002,Ultra Trainer 2,Football,Grey,140.4,287
SKU-1003,Pro Tee 3,Outdoor,White,109.84,142
We want our LLM to process the following instruction:
Read products.csv with the Read tool, then tell me the average price and the total stock value (price × stock) of the 10 most expensive products.
If we run the query as-is, it will process a 100-row CSV as input, but it actually only needs 10 products for the calculation.
Let’s use a PreToolUse hook that filters our products prior to sending them. Add the code in .claude/hooks/top10_filter.py:
#!/usr/bin/env python3
"""When TOP10_FILTER=1, replace reads of products.csv with its 10 most expensive rows."""
import csv
import json
import os
import sys
data = json.load(sys.stdin)
tool_input = data.get("tool_input", {})
path = tool_input.get("file_path", "")
if os.environ.get("TOP10_FILTER") != "1" or not path.replace("\\", "/").endswith("/products.csv"):
sys.exit(0) # hook off, or another file: no change
with open(path, newline="") as f:
rows = list(csv.DictReader(f))
top10 = sorted(rows, key=lambda r: float(r["price"]), reverse=True)[:10]
out_dir = os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", "."), ".claude", "tmp")
os.makedirs(out_dir, exist_ok=True)
filtered = os.path.join(out_dir, "products_top10.csv")
with open(filtered, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(top10)
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {**tool_input, "file_path": filtered},
"additionalContext": "A hook pre-filtered products.csv to its 10 most "
"expensive products, sorted by price (highest first).",
}
}))
Register our hook in .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/top10_filter.py"]
}
]
}
]
}
}
Now we run the same prompt with the hook enabled and disabled, and compare the input tokens:
Read products.csv with the Read tool, then tell me the average price and the total stock value (price × stock) of the 10 most expensive products.
For details on how we automated these tests, and access to our scripts, check out the Appendix at the end of the article.
Our results are:
In our scenario, using an input filter on a PreToolUse hook saved around 40% of the tokens.
#2 Use Context Efficiently
LLMs are stateless by definition, but to remember previous interactions, they use a set of information called a context window that the model can read.
Picture an agent that adds 1000 tokens to the context window every request. The first prompt has 1000 input tokens, the second 2000, and so on. But since every request re-sends everything stored before, by prompt 20 the total input accounted for can be up to 210,000 tokens.
In Claude Code, the /context command helps you understand what’s in the context window: system prompt, tools, instruction files, and conversation.
Prompt caching helps to soften this: when you send repeated content (system prompts, long documents, conversation histories), modern LLMs do not bill them at the usual rate. Instead, they store the important parts in a KV-tensor cache.
However, you don’t have direct control over the cache, but you do over the context. Let’s see how you can control the context to save tokens in your LLMs.
Clear context between unrelated tasks (/clear)
The /clear command removes the whole conversation history (file reads, command output, tool results) from the context window.
For example, we tried calculating token consumption for two consecutive unrelated tasks in Claude Code:
Task 1:
Read products.html and products.json in full, then summarize the product categories and price ranges.
Task 2:
Create a new file slugify.py wit a function that converts a string into a URL slug, add a pytest test for it in test_slugify.py, and run the test.
The first task involves processing a huge file that will end up in the context window, and the second task is completely unrelated. If we drag the same context from the first task to the second, it will include thousands of tokens from the original data inputs.
Here’s a comparison of token consumption when we run them sequentially versus when we run /clear in between to delete the context:
In this scenario, using /clear after one prompt nearly halved the token usage:
Compact with a Focus
Over time, the context window keeps growing, which reduces efficiency and increases costs. To avoid this, most major LLMs automatically perform compacting over time.
Compacting is a process that performs the following tasks:
- Writes a summary of the current context window.
- Deletes old and stale messages.
- Replaces the old context window with the summarized version.
The /compact command in Claude Code allows you to manually compress long conversation histories into a summary, similar to the automatic process.
There are several things to keep in mind when using /compact:
- Compacting costs tokens, but it generally pays off.
- The resulting context window will be much smaller, and cache will need to be rebuilt.
- Summaries from this session will lose detail.
One important tip when using /compact is to include specific instructions (e.g., “keep the API design decisions”) to specify which elements of your context to keep. That way, you can reduce context size while keeping the critical parts the LLM needs to understand.
#3 Simplify Input, Simplify Output
As we mentioned before, LLM costs are directly tied to what you send and what you get back.
Simplifying the Input
The most common token waste in the input is sending an entire file, document, or dataset when the model only needs a fraction of it.
Additionally, pay attention when sending files to an LLM to the format being used:
- HTML has many tags and a low information-density ratio. Instead, transform it into text prior to sending it to an LLM.
- JSON is meant to be readable and parseable by machines, but it constantly repeats keys (labels) for each entry. Instead, transform the information to CSV.
- API responses directly passed to an LLM may be inefficient. Consider filtering out information first, so you pass only what the model needs.
As we saw in the first section, Claude hooks also help save input tokens. For example, consider using a PreToolUse hook to trim, transform, or filter your input files before sending them to your LLM.
Example: Compare Three Input Formats
To show the difference in tokens spent across three inputs: HTML, JSON, and CSV, we asked claude code the following:
Read products.csv in full with the Read tool and reply only 'done'. Don't analyze it.
We also used an empty prompt as a baseline:
Reply only 'done'.
The data in all 3 files products.html, products.json, and products.csv are the same, but the token usage on the HTML version is 70% higher than in the csv version:
Simplifying the Output
As we saw earlier, output tokens make up a huge part of AI costs. Not only that, major LLMs charge output tokens at 3x or even 8x the price of the rest. Simplify output by stating from the start how responses should be returned.
You can instruct Claude to use a different output style by editing the CLAUDE.md file:
## Response style
- Be concise. No preamble, no restating my request.
- Don't narrate each step. Report only results, problems, and questions.
- When a task is done, summarize in at most 3 bullets.
- Don't paste code back into the reply after editing a file; I can see the diff.
Note that while CLAUDE.md itself is part of the input and costs tokens on every request, it’s cached, so it’s cheap and can save lots of tokens in the long run. Most major LLMs charge a fraction of the cost of cached context (up to even 90% less).
Another option is to use skills that translate the model language into simple English, like ASD-STE100, which can drastically reduce token usage.
Example: Output Example
Let’s compare Claude Code’s output when using a skill for Simple English. Create a new skill at .claude/skills/simple-english/SKILL.md with the following specification:
---
name: simple-english
description: Answer in short, plain Simple English. Use only when the user invokes /simple-english or explicitly asks for a Simple English answer.
---
# Simple English
Write the answer so anyone can read it quickly.
- Use short sentences and common words.
- If you must use a technical term, explain it in a few plain words.
- Give at most 5 bullet points, one sentence each.
- No introduction, no summary, no closing offer.
- No code blocks unless a fix cannot be explained in words.
- Keep every fact accurate. Simple does not mean vague.
Now in your Claude session, you will see that there’s a new option called /simple-english where you can use the skill.
We’ll run the same prompt with and without the skill, and compare the token usage:
Review generate_products.py and suggest improvements. /simple-english Review generate_products.py and suggest improvements.
Here’s the comparison in output tokens when asked to provide feedback on code improvements of a file. The simple english one used almost half of the output tokens:
Other trends in Token Optimization
We have seen several basic techniques to optimize token costs, but the tech community is already applying other more advanced trends.
Model Routing
Did you know model costs aren’t the same? Most of the agent’s work doesn’t need the latest state-of-the-art model and can be done with a simpler, cheaper model.
See the comparison in pricing for the latest models of Claude:
| Model | Input | Cache hits | Cache writes (5 min) | Output | Output vs. Haiku 5.5 |
| Claude Fable 5.1 | $10.00 | $0.25 | $12.50 | $50.00 | 100× |
| Claude Opus 5.5 | $4.00 | $0.20 | $5.00 | $20.00 | 40× |
| Claude Sonnet 5.5 | $2.00 | $0.10 | $2.50 | $10.00 | 20× |
| Claude Haiku 5.5 (prompts up to 100K tokens) | $0.10 | $0.01 | $0.125 | $0.50 | 1× |
| Claude Haiku 5.5 (prompts over 100K tokens) | $0.50 | $0.05 | $0.625 | $2.50 | 5× |
With that in mind, you can implement model routing in your Claude session by running it like this:
CLAUDE_CODE_SUBAGENT_MODEL=haiku claude --model sonnet --advisor opus
In this command:
- Sonnet is the main worker: drives the session and edits your files.
- Haiku (a small model) is in charge of subagent work, like searching the codebase or running tests.
- Opus is used for tasks that require reasoning, or as guidance before committing to an approach.
Portal is Spotify’s developer platform that builds model routing at scale. It includes a feature called AiKA Modes: agents that run on temporary runtimes, without the need to manage separate infrastructure. For each mode, the developer specifies:
- Model
- Instructions
- MCP tools
For example, you can define a bulk reader mode for reading multiple files and a code writer for predictable snippets. A plugin called Shunt intercepts Claude Code calls and delegates repetitive reads and code generation to those modes.
System One Models
LLMs are experienced in text, both in input and output. But sometimes, you just need to analyze probabilities.
A System One model is a kind of AI that returns probabilistic decisions instead of returning text. For example, you send the System One Model a ticket from your product and need it to decide whether it’s an issue, a feature, or an improvement.
Because these models don’t write, output tokens cost almost nothing.
In September, TypeSafe AI introduced Jev, often coined as the first System One Model. It works with three primitives:
- Noul: it answers the question “is this true?” (e.g., is this ticket an issue?)
- Choice: picks an item from the list (e.g., which ticket type is this?)
- Score: you define a set of levels (called a rubric), and the model returns the score of your question (e.g., is this ticket title descriptive enough?)
A week after Jev’s launch, researchers from Stanford and NVIDIA released CLM-8B, the first Contrastive Language Model, which represents the open alternative to System One Models
Final Thoughts
Most of the tokens companies pay for in LLMs are not spent on thinking. They are spent reading unnecessary elements or carrying history from several hours ago.
While LLMs are essential for fuzzy problems, code is the answer for crisp calculations with a clear result.
Ask yourself the following questions when working with LLMs:
- Could this step be done with code?
- Is all this input needed for the task to be completed?
- Is the format of the input optimized for saving tokens?
- Is my context still relevant for the next task?
LLM Tokens and Claude Code Hooks: Common Questions
What is a token in large language models?
What is the difference between input and output tokens?
What are Claude hooks?
Appendix: Code and testing methodology
Initial setup
To run these tests, you’ll need:
- Python 3.9 or later and the
pytestlibrary. - Claude Code installed and signed in (
claude --versionshould work) jqfor reading the JSON output (brew install jqon macOS,sudo apt install jqon Debian/Ubuntu)git(used bytest_clear.shto reset the folder between runs)
Note on cost: every script calls Claude Code several times. On an API key this is billed per token; on a Pro or Max plan it counts toward your usage limits.
Generate the product files
Once all is installed, generate the products csv, json, and html files with this script:
import csv
import json
import random
random.seed(42)
CATEGORIES = ["Running", "Football", "Training", "Outdoor", "Lifestyle"]
COLORS = ["Black", "White", "Blue", "Red", "Grey"]
products = [
{
"id": f"SKU-{1000 + i}",
"name": f"{random.choice(['Ultra', 'Pro', 'Flex', 'Aero', 'Core'])} "
f"{random.choice(['Runner', 'Trainer', 'Boot', 'Jacket', 'Tee'])} {i}",
"category": random.choice(CATEGORIES),
"color": random.choice(COLORS),
"price": round(random.uniform(20, 220), 2),
"stock": random.randint(0, 500),
}
for i in range(100)
]
# CSV
with open("products.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=products[0].keys())
writer.writeheader()
writer.writerows(products)
# JSON (pretty-printed, as most APIs and exports deliver it)
with open("products.json", "w") as f:
json.dump({"products": products}, f, indent=2)
# HTML (a realistic page: styling, classes, wrapper markup)
rows = "\n".join(
f' <tr class="product-row" data-sku="{p["id"]}">\n'
f' <td class="cell cell-id">{p["id"]}</td>\n'
f' <td class="cell cell-name"><a href="/products/{p["id"]}">{p["name"]}</a></td>\n'
f' <td class="cell cell-category">{p["category"]}</td>\n'
f' <td class="cell cell-color">{p["color"]}</td>\n'
f' <td class="cell cell-price"><span class="currency">€</span>{p["price"]}</td>\n'
f' <td class="cell cell-stock">{p["stock"]}</td>\n'
f' </tr>'
for p in products
)
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Product Catalog</title>
<style>
.catalog-table {{ border-collapse: collapse; width: 100%; font-family: sans-serif; }}
.cell {{ padding: 8px 12px; border-bottom: 1px solid #ddd; }}
.product-row:hover {{ background: #f5f5f5; }}
</style>
</head>
<body>
<div class="page-wrapper">
<header class="site-header"><nav class="main-nav"><a href="/">Home</a></nav></header>
<main class="content">
<table class="catalog-table">
<thead>
<tr><th>ID</th><th>Name</th><th>Category</th><th>Color</th><th>Price</th><th>Stock</th></tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</main>
</div>
</body>
</html>"""
with open("products.html", "w") as f:
f.write(html)
print("Wrote products.csv, products.json, products.html")
Save it as generate_products.py and run it as:
$ python3 generate_products.py
Register the hook
Register the hook for the first test in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/hook_filter_tests.py"]
}
]
}
]
}
}
Take the code for the hook that was provided earlier in the article, and save it in the hook_filter_tests.py file.
Commit a baseline
The second test, the one testing the /clear command, makes use of git clean and git checkout to reset the scenario. Init a new repository and save the current status in a baseline commit:
$ git init -q
$ printf ".venv/\n.claude/tmp/\n" > .gitignore
$ git add . && git commit -qm "baseline"
Test 1: test_hook.sh
The script we used to test the input filter hook is:
#!/bin/bash
TASK="Read products.csv with the Read tool, then tell me the average price and the total stock value (price × stock) of the 10 most expensive products."
SUM='{input: (.usage.input_tokens + .usage.cache_read_input_tokens + .usage.cache_creation_input_tokens), output: .usage.output_tokens, answer: .result}'
for run in 1 2 3; do
echo "== Run $run: full CSV"
claude -p "$TASK" --output-format json | jq "$SUM"
echo "== Run $run: with hook"
TOP10_FILTER=1 claude -p "$TASK" --output-format json | jq "$SUM"
done
Run it as:
$ bash test_hook.sh
Test 2: test_clear.sh
The script we used to test savings when using the /clear command is:
#!/bin/bash
TASK_A="Read products.html and products.json in full, then summarize the product categories and price ranges."
TASK_B="Create slugify.py with a function that converts a string into a URL slug, add a pytest test for it in test_slugify.py, and run the test."
SUM='.usage | {input_total: (.input_tokens + .cache_read_input_tokens + .cache_creation_input_tokens), cache_read: .cache_read_input_tokens, output: .output_tokens}'
reset() { git clean -fdq && git checkout -q .; }
echo "== Task B after Task A, same session (no /clear)"
reset
claude -p "$TASK_A" > /dev/null
claude -p --continue "$TASK_B" --permission-mode acceptEdits --output-format json | jq "$SUM"
echo "== Task B in a fresh session (/clear equivalent)"
reset
claude -p "$TASK_B" --permission-mode acceptEdits --output-format json | jq "$SUM"
Run it as:
$ bash test_clear.sh
Test 3: test_input_formats.sh
The script we used to test the token usage of different input formats is:
#!/bin/bash
A="Review generate_products.py and suggest improvements."
for run in 1 2 3; do
echo "== Run $run"
echo -n "Default: "
claude -p "$A" --output-format json | jq '.usage.output_tokens'
echo -n "Simple English: "
claude -p "/simple-english $A" --output-format json | jq '.usage.output_tokens'
done
Run it as:
$ bash test_input_formats.sh
Example 4: test_output_formats.sh
The script we used to test the effect of simplifying the output is:
#!/bin/bash
A="Review generate_products.py and suggest improvements."
for run in 1 2 3; do
echo "== Run $run"
echo -n "Default: "
claude -p "$A" --output-format json | jq '.usage.output_tokens'
echo -n "Simple English: "
claude -p "/simple-english $A" --output-format json | jq '.usage.output_tokens'
done
Run it as:
$ bash test_output_formats.sh