August 29, 2026 · 15 min read
Experiments with Token Usage and Optimization
I use coding agents quite a lot in day-to-day development, and at some point, as limits started to become more noticeable, I got curious about how efficiently I was actually using tokens.
My overall usage was very high, but that number by itself did not tell
me much. I wanted to understand, at least for my own workflow, where
most of the tokens were going. Was it my prompts? AGENTS.md and other
instructions? Long sessions? Or the work the agent does after I send a
request?
There are many more ready-made tools for this now. For Codex, for example, there is ccusage. It reads local Codex session logs and can show usage by day, month, and individual session, including cached tokens. Codex support is currently marked as beta because the Codex CLI log format is still changing.
For Claude Code, the same ccusage
can read local Claude logs and produce daily, weekly, monthly, and
session reports. Claude Code itself also has more built-in tools for
working with context: /context, compaction, CLAUDE.md, skills, and
subagents.
When I started collecting these statistics, there were fewer ready-made options for this kind of detailed breakdown. I also wanted more than total usage. I wanted to understand not just how many tokens were being used, but what they were being used for inside a session. So I built part of the audit myself.
The custom tracker is not really the point of this article. Today, if I only wanted to count usage, I would probably use an existing tool. What turned out to be more interesting was what the collected data showed.
Before measuring anything, my first suspect was my own prompts. I often
describe a task in quite a lot of detail. There is also AGENTS.md,
persistent instructions, and context left over from previous steps. At
first glance, all of that looks expensive.
But after the prompt, the agent also starts collecting context on its own. It runs grep, reads files, searches for usages, runs builds or tests, and receives logs. Before measuring it, I had no idea how much text was accumulating on that side.
How I collected the statistics
The main dataset came from local Codex session logs. I did not just want total usage. I wanted to be able to look at individual turns and connect them to what was happening in the session.
I made a small hook that saved a local snapshot before each new turn:
turn_id, the current repository, a prompt hash, a few additional
fields, and, when needed, the raw payload.
The rough flow looked like this:
Codex session
↓
hook
↓
capture metadata
↓
Codex session logs
↓
token_audit.py report
↓
report.json / report.md
I deliberately kept the hook as simple as possible:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
python3 "$SCRIPT_DIR/token_audit.py" capture \
--repo-root "${PWD}" \
--store-raw-payload
I quickly gave up on the idea of doing any serious calculation inside the hook itself. It was easier to save the state first and build a report later from the accumulated data.
A simplified version of capture looked roughly like this:
import hashlib
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
raw = sys.stdin.read()
payload = json.loads(raw) if raw.strip() else {}
prompt = payload.get("prompt", "")
repo_root = Path(payload.get("cwd") or ".").resolve()
record = {
"captured_at": datetime.now(timezone.utc).isoformat(),
"turn_id": payload.get("turn_id"),
"repo_root": str(repo_root),
"prompt_preview": prompt[:240],
"prompt_hash": (
hashlib.sha1(prompt.encode("utf-8")).hexdigest()
if prompt else ""
),
}
audit_dir = repo_root / ".token-audit"
audit_dir.mkdir(exist_ok=True)
with (audit_dir / "hook-captures.jsonl").open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
The real script stored a few more fields: the branch, the size of the
repo instructions, nearby handoff files, and the raw payload. The
principle was the same: the hook records the state of the turn, while a
separate report command works with the logs.
The report was run separately:
python3 ./tools/token_audit.py report \
--repo-root "$PWD"
That produced a structure roughly like this:
.token-audit/
├── hook-captures.jsonl
├── raw-hook-payloads/
└── latest-report/
├── report.json
└── report.md
The report command went through the session logs, matched them with
captures, and split fresh input into several categories:
- user prompts;
- developer instructions;
- repo bootstrap;
- tool call arguments;
- tool outputs;
- assistant loopback.
I counted cached input separately.
This was not production-grade observability and it was not a billing system. Log formats can change, some of the classification depends on the client, and the script needs maintenance. For my purpose that was fine. I wanted to understand the structure of the usage and compare different periods.
In total, I collected 5,420 turns over 120 days of usage.
What I found
The most interesting breakdown looked like this:
Source Fresh input
Tool outputs 67.4% Assistant loopback 17.9% Tool call arguments 8.3% User prompts 4.6% Developer instructions 1.43% Repo bootstrap 0.34%
I expected something different.
My own prompts accounted for only 4.6%. Even if I had somehow cut every prompt in half, it would not have changed the overall picture very much.
At the same time, 67.4% of fresh input came from tool outputs.
Once you see the result, it makes sense. During a normal session the agent constantly gets text back. It opens a file and gets text. It runs grep and gets results. It runs tests and gets another chunk of text. Sometimes it is small, sometimes it is very large.
Before I measured this, I mostly thought of those things as the agent’s internal work. Prompt size is easy to notice because I am the one writing the prompt. A search returning a few hundred lines does not feel like a separate expense in the same way.
But all of that text also enters the context, and some of it stays there for later turns.
Even an ordinary grep has a cost
This was especially noticeable during larger refactors and modularization work.
If the structure is stable and the agent already knows where the relevant code lives, things are fairly simple. But when files are moving between modules and dependencies are changing, a fresh agent quite reasonably starts with something like:
rg "Checkout"
It gets a few dozen results, then searches for a protocol:
rg "PaymentRouting"
Then the implementation. Then usages. Then it opens the router, the view model, and perhaps a neighboring module to figure out who actually owns the flow.
All of those are normal actions. I would explore an unfamiliar part of a codebase in roughly the same way. The difference is that for the agent, the search results also become part of the context.
I tried to remove at least some of this repeated exploration by keeping a small map of the important modules. Not documentation for the whole project, just a short file with pointers for the places where the agent repeatedly started the same searches from scratch.
For example:
## Checkout
Responsibility:
Checkout owns the flow from cart confirmation
to the final order result.
Entry points:
- CheckoutView.swift
- CheckoutViewModel.swift
- CheckoutRouter.swift
Navigation:
- CheckoutRouter.swift
Owns checkout navigation and creates child flows.
State:
- CheckoutViewModel.swift
Handles UI actions and checkout state updates.
Payment:
- RetryPaymentAction.swift
Retry entry point after a failed payment.
- PaymentGatewayClient.swift
Payment API only. Does not own navigation.
Boundaries:
- CheckoutRouter owns navigation.
- Payment module owns payment implementation.
- Cart owns price calculation.
If payment retry is broken:
1. Check RetryPaymentAction.
2. Check state update in CheckoutViewModel.
3. Check routing in CheckoutRouter.
4. Only then search the whole Checkout module.
The names here are just examples. The point is the structure.
A file like this should not explain every class to the agent. The code still has to be read. It simply answers a few questions before a broad search begins: where is the entry point, who owns navigation, where is the state, and where should I look first?
For complicated modules, this turned out to be useful. The agent still explored the code, but it did not have to begin every time with a search across the whole project.
At the same time, the map cannot grow forever. If you put everything that was ever useful into it, after a while you have another internal documentation system that has to be read, maintained, and kept in sync with the code. I kept only the pointers that repeatedly saved searches.
AGENTS.md turned out a little differently
Before the measurements, I was almost certain that AGENTS.md was
eating a noticeable amount of tokens.
The reasoning is obvious: it is loaded all the time, so its contents become part of the context of every session.
Matt Pocock, in his material on
AGENTS.md,
recommends treating it as a brief rather than documentation: keep it
short and declarative, and only reveal specialized information when it
is actually needed.
I generally agree with that, but in my data AGENTS.md was far from the
main problem:
developer instructions, 1.43%
repo bootstrap, 0.34%
Together, that was less than two percent of fresh input.
That does not mean AGENTS.md can be any size. For me, the result meant
something simpler: there was not much point in starting the optimization
there while tool outputs were sitting at 67.4%.
I still cleaned the file up later. Some instructions were only needed for particular kinds of tasks, for example specific modularization rules or a separate workflow. There was no reason to load them into every session.
I moved some of that context into skills.
Matt Pocock also has a separate piece on Writing for Agents and a collection called AI Skills for Real Engineers. Parts of that approach transfer quite well between Claude and Codex, even though the exact mechanisms are different.
My rule for AGENTS.md became fairly simple: not “can I delete another
ten lines?” but “does the agent really need this information in every
session?”
Sometimes a more detailed instruction is worth the extra tokens. If a
few lines explain ownership up front and save several searches across
the repository, shortening them just to make AGENTS.md smaller does
not make much sense.
Long sessions were more predictable
A session gradually accumulates history.
The agent searches for something, tests a hypothesis, gets an error, tries another option, reads several files. All of that was useful at the time. But the problem may already be solved while the history remains.
Research creates a lot of this extra context. You can spend quite a while checking several possible causes and eventually find the real one somewhere else. For the next stage, what usually matters is the final result: the cause is here, these are the relevant files, and this is the constraint that should not be broken.
The earlier wrong hypotheses are usually no longer useful to a new session.
For longer tasks, I had a context.md that I used to continue between
sessions. At first I made a fairly obvious mistake: I turned it into a
journal.
## Session 1
Checked payment flow.
Looked at CheckoutViewModel and PaymentService.
First assumption was wrong because...
Build failed with...
## Session 2
Moved implementation.
Found another issue in...
Tried...
After a while, the file started to look like notes from all previous work. As a result, a new session received most of the old context again, just in a different format.
So instead of appending to context.md, I started rebuilding it.
## Current state
Payment retry works, but checkout confirmation
is not refreshed after successful retry.
## Decisions
- Retry logic stays in Checkout.
- Payment module only performs payment operations.
- Navigation stays in CheckoutRouter.
## Important
Refreshing state directly from the view caused
duplicate requests. Do not repeat this approach.
## Relevant files
- CheckoutViewModel.swift
- CheckoutRouter.swift
- RetryPaymentAction.swift
## Next
Trace state update after successful RetryPaymentAction.
That is usually enough for a new session to continue without the full history.
I do not remove absolutely everything from the past. If the agent already followed an obvious but wrong path and there is a good chance a fresh agent will repeat it, a short note about that is useful. There is just no need to carry over the whole process that led to the conclusion.
Later I found almost the same idea in Matt Pocock’s /handoff
skill.
Matt Pocock uses a good phrase for it: portability, not compression. A handoff does not need to be the most complete compressed version of the previous session. Its job is to give another agent enough information to continue the work.
For Claude, the skill can be used almost directly. In Codex I had my own implementation, but the principle is the same.
When it is easier to start a new session
After that, I started using fresh sessions more often.
Previously, I did not want to lose context that had already been collected. In practice, once a separate stage of the work is finished, only a small part of that context is usually needed.
Suppose the work looks roughly like this:
research
→ root cause found
→ implementation
→ validation
Before implementation, I do not need every rg result, every file that
was opened, or every earlier assumption. Often something like this is
enough:
Root cause is in RetryPaymentAction.
State must be updated through CheckoutViewModel.
CheckoutRouter owns navigation.
Do not refresh directly from the view:
it causes duplicate requests.
Next: implement state refresh after successful retry.
Then I can open a fresh thread and continue.
I do not do this every few messages. Sometimes it is simply easier to continue the current session. But I no longer keep an old thread only because I do not want to lose all of the history that has accumulated in it.
What happened to usage
Over the observation period, the average fresh input per turn dropped noticeably.
In one of the earlier slices it was around 54.6K fresh tokens per turn. Later it was around 29.7K.
That is a difference of roughly 46%.
Cached input changed much less: about 652K → 583K per turn.
But these data do not let me separate the effect of each change. This was not an A/B test. The tasks changed, the sessions had different levels of complexity, and the amount of usage also varied.
So I cannot say exactly how much came from the module map, how much from fresh sessions, and how much from shorter handoffs.
I treat the 46% as an overall trend, not as proof that one particular technique reduced usage by that amount.
The breakdown itself was more useful to me. It showed where it made sense to look for the problem. With 67.4% of fresh input coming from tool outputs, experimenting with searches, file reads, and session history made more sense than trying to save a few more tokens in the prompt.
What about Claude?
The main numbers in this article come from Codex session logs. I am deliberately keeping that distinction: statistics from one agent should not automatically be applied to another.
But the general ideas transfer fairly well.
Claude Code now has /context, compaction, skills, and subagents.
Subagents, for example, make it possible to move a separate piece of
research out of the main session and return only the result.
For usage tracking, the same ccusage can read local Claude Code logs.
Another useful tool is Matt Pocock’s Claude Code status line. It shows the percentage of the context window already in use directly in the terminal.
The status line does not optimize anything by itself, but the state of the session is always visible:
context: 37%
or:
context: 82%
That makes it easier to decide whether to continue the current session, compact it, or prepare a handoff.
Matt Pocock’s /handoff
handles the second part: passing the current state to a fresh agent.
Specialized instructions can live in
skills instead of gradually turning
CLAUDE.md or AGENTS.md into documentation for the entire project.
I later reused some of these ideas with Codex as well. Not one-to-one at the level of hooks or specific commands, but as a general approach to organizing context.
Would I build this tracker today?
Probably not if the only goal was to see total usage.
For Codex and Claude Code, ccusage can already read local data, show usage, and export JSON for further analysis.
My script was useful because I wanted to answer more than:
How many tokens did I use?
I wanted to understand:
What is that usage actually made of?
Those are slightly different questions.
For the first one, an existing usage tracker is enough today. For the second, it can still be useful to look inside a particular session and discover that the large prompt that looked suspicious accounts for 4.6%, while most fresh input is coming from somewhere else entirely.
That was the most useful result of the whole experiment for me.
Before measuring anything, I would almost certainly have started by
shortening prompts and AGENTS.md.
After 5,420 turns, the result was:
- user prompts: 4.6%;
- developer instructions + bootstrap, less than 2%;
- tool outputs: 67.4%.
These are my numbers, not universal statistics for every coding agent. A different project or a different usage style can produce a very different distribution.
But since then, I have been much less worried about a long prompt when
the detail is actually useful. If a few extra paragraphs tell the agent
to start with CheckoutRouter.swift instead of doing a broad search
across the whole project, shortening that prompt just to save tokens
does not make much sense.
I pay much more attention now to the size of search results, file reads, and other tool outputs.