My Claude usage kept climbing on small changes. Running grep was never the expensive part. Bringing its output into the conversation was. Part 1 of a series on moving routine work out of the model with a local task runner.

In September, I was adding a single Three.js element to my portfolio with Claude Code. It was a small change, but my usage kept climbing toward the limit.
After individual approvals, I was seeing jumps of roughly 10 to 15 percentage points on the usage indicator. That was an observation from working with the tool, not a controlled measurement. Still, it was enough to make me question the workflow.
I wanted to finish more work with the allowance I already had. Running out of usage while there were still changes to make and checks to run was becoming frustrating.
So I started looking at what happened around the actual edit. The agent searched for files, read source code, ran commands and brought their output into the conversation. Some of that information helped it make decisions. Some of it was routine bookkeeping, or much more detail than the next step needed.
Could I handle more of that locally, before asking the model to interpret it?
That question became /runtask.
The idea was straightforward: use a script for repeatable operations, and leave the model responsible for understanding the task, choosing an approach and changing the code.
Grep already runs locally. So do the compiler and the linter. Running those commands does not itself consume model tokens. Asking the model to coordinate them and interpret their output does. The opportunity was in handling more of that coordination locally and deciding how much of each result needed to reach the conversation.
For example, a script can record a lint baseline, run lint again and compare the results. The model can then investigate the new diagnostics instead of receiving the full output and reconstructing that comparison itself.
/runtask grew into a Claude Code skill backed by roughly 61 KB of
dependency-free Node code in two files, rt.mjs and core.mjs. The script
prepares context, runs checks, tracks changes and saves progress.
The work is split into three parts:
| Part | Responsibility |
|---|---|
| Main conversation | Coordinate the run and handle decisions that need my input |
| Local Node script | Search, prepare reports, compare results and maintain run state |
| Agents | Plan and implement the change, with optional review or search help |
The main conversation and the agents consume tokens. The Node code does not, although asking the model to call it and interpret a result does.
I kept the agent setup small: a Sonnet 5.5 worker handles both planning and implementation, a Sonnet 5.5 reviewer is optional, and a Haiku scout helps when local search fails. Reusing the worker means it can carry its understanding from the plan into the edit without another handoff.
I left Opus out of this workflow after watching my usage climb quickly with it in September. I wanted to see how much work I could get through with Sonnet and a more structured process around it. Sonnet sometimes needs more context or another turn, so the useful comparison is the effort needed to finish a task, including any corrections. Choosing a smaller model is only one part of that.
The worker starts with two files: a manifest describing the repository and a packet containing the task and selected code snippets.
The manifest is capped at 6,000 characters. It summarizes the stack, available checks and candidate files. The packet has an 18,000-character budget and includes source snippets, imports and direct dependents. Those are limits on the starting material, not on everything the worker reads during the task.
The script selects candidate files by matching the ticket against paths and content. A term in a filename gets a higher score than the same term inside a file. Explicit paths help when a ticket is too vague for that approach.
From there, the worker can request more information through focused local commands:
| Command | Returns |
|---|---|
RT find "<page text>" | Files by path, by translated i18n text and by strings in code |
RT symbol <name> | Where a symbol is declared |
RT refs <name> | Usages, up to 20 files, flagged when a file imports the symbol |
RT slice <file> --lines A:B | The requested line range, with long lines cut at 400 characters |
RT delta | What changed since the run started, to guide further inspection |
RT check <file> | Check results for the requested file |
My favorite is RT find, because it can search using the text someone sees on
the page. A ticket often mentions a button label or a heading. In a translated
application, the script can map that text to an i18n key and then to the code
using it.
The worker still explores the repository. The difference is that it starts with focused results and expands from there. Grep and Glob remain available as fallbacks, and the scout can help if those searches do not locate the relevant code.
The snippets explicitly say they are incomplete evidence. That matters: a short excerpt can omit the condition that explains how a component behaves. Saving context is useful only while the worker still has enough information to make a sound change.
Check output was one of the clearest places to reduce the amount of material passed to the model.
Before editing, the runner records a baseline: typecheck, lint, tests and optionally build. After the change, it compares diagnostics against that baseline and highlights newly reported errors. It also counts repeated diagnostics rather than treating each distinct message as a single item. That helps detect increases in the number of matching errors, although it cannot identify every new problem when old and new diagnostics look the same. Existing failures stay relevant, but they should not silently turn a small task into a cleanup of the whole project.
One recorded run gives an idea of the sizes involved:
| Artifact | Size |
|---|---|
| Raw baseline lint log | 58,451 bytes |
Verification summary in verify.md | 184 bytes |
These come from different stages of the same run and contain different levels of detail. They illustrate the gap between a raw log and a status report. They do not measure the reduction in token usage or the cost of completing the task.
The worker normally reads the summary. If that is not enough to understand a failure, it can open the detailed output. Prompt caching can make repeated input cheaper, so smaller reports alone cannot tell me how much of my Pro allowance I saved. They do show that routine check results can be communicated with far less text.
Verification ends with one of three results: PASS, FAIL or INCOMPLETE.
INCOMPLETE means the required verification could not be completed. A required
check might fail to start, or an existing test failure might prevent the runner
from establishing a clean verification result. Other checks may still have run
successfully and provided useful evidence.
The important distinction is between an optional check that was not requested and a required check that could not run. The report needs to show which checks were required, which passed and which were skipped or blocked. A skipped check is never evidence that the code passed it.
This is why the status matters. Reporting "no new errors" when the linter never
ran would hide the problem I need to investigate. INCOMPLETE makes that
missing verification explicit.
It also becomes important in Part 2.
The script also stores the run state on disk: what was scanned, what the plan
allows, which steps were recorded and what the checks found. The normal path
moves from scanned through ready, planned, building and verified to
finished. A run can also become blocked when it cannot continue.
The workflow uses script commands to validate and record those transitions. The worker is instructed to use those commands rather than edit the state file directly.
A hash snapshot records the starting state of repository files. That lets the runner detect changes made during the task even when the working tree already contains edits. The plan lists files to edit, create or delete, and saved originals provide a comparison point for review without requiring a commit.
If another file needs changing, the workflow requires the worker to stop and ask me to extend the approved scope. Verification compares the actual changes against that scope and flags mismatches between planned and actual actions.
Hooks handle formatting and checks after Edit/Write operations. The check hook focuses on new diagnostics relative to the baseline. Failures to execute a check also need to remain visible, rather than being mistaken for an empty list of errors.
Selected Git operations are blocked through hooks and permissions. These controls reduce how much of the process depends on the model remembering instructions, although each one only covers the actions it actually checks.
Persisted state is useful when a run stops, too. A later session can read the recorded progress and continue from it. It still needs context, but the record gives it somewhere concrete to start.
I also set limits on the worker's behavior, including at most two fix attempts and a short final report stating the outcome, checks performed and remaining limitations. I wanted failures to become visible without an open-ended sequence of retries consuming the rest of the allowance.
There is a risk in compressing all this activity into a few lines: a reassuring status can hide how little was actually verified.
In one run from October 21, typecheck and lint passed. Tests were skipped because no Vitest or Jest setup was detected, and the build was not run. Those details matter alongside the final status.
The worker also claimed that a foreground color already met a contrast ratio of at least 4.5:1 in both themes. The runner had not measured that ratio. Recording the claim as acceptance evidence did not make it a check.
That is a useful boundary for this approach. A script can record what ran and summarize the result. UI behavior, visual quality and requirements that need a browser still need relevant evidence. An optional reviewer reading the diff does not supply that evidence automatically.
The target is a report that is short enough to be useful and precise enough to show what remains unverified.
At this point, the approach had a clear rationale. Focus the initial context, summarize routine output, reuse one worker and keep progress outside the conversation. All of it was meant to leave more of my usage for the decisions and edits that moved the task forward.
So I took 12 earlier archived runs and counted how many had reached finished.
Zero.
That is where Part 2 begins.
*Built, measured and written by me, edited with Claude’s help.
Was this helpful?