Principles
Every token an agent reads is billed input. CtxCtl exists because the two biggest context polluters — whole-file dumps and raw command logs — are both avoidable. This page explains the design decisions behind the tool, what the compression actually preserves, and where the savings naturally level off.
Context is billed, so size is a budget
Section titled “Context is billed, so size is a budget”A coding agent session typically spends most of its tokens re-reading things: the entire file to find one function, the full build log to find one error. In the scripted-agent benchmark behind the Benchmark page, the base arm burned an average of 26,284 uncached input tokens per session just using built-in read + bash tools — even with an already-aggressive 81.7% provider prefix-cache hit rate.
The economics are unforgiving:
- Input tokens are billed on every turn they appear in, not once.
- Large contexts also degrade answer quality — more haystack around the needle.
- Provider prompt caches discount repeated prefixes, but only if the prefix stays stable.
CtxCtl attacks the first two directly and engineers for the third.
Slices, not summaries
Section titled “Slices, not summaries”The file-reading half of ctxctl is a tree-sitter symbol engine with a strict rule: locate with the AST, slice from the original source bytes.
The pipeline is deliberately simple and stateless:
- Parse the file with tree-sitter into an AST (parse-and-discard; nothing is cached).
- Extract symbols — names, kinds, line ranges, normalized signatures — into an outline.
- Resolve the requested symbol to its byte range.
- Return the verbatim source bytes for that range.
Nothing is reworded, paraphrased, or summarized. A slice keeps comments, formatting, and indentation exactly as written, so it remains valid source: your agent can quote it back, re-parse it, or diff it against later edits without any translation step in between.
ctxctl outline src/server.rsctxctl symbol src/server.rs --name handle_request# src/server.rs [12 symbols, ~2.1 KB -> ~410 tokens, saved ~80%] fn handle_request L:42-58 pub async fn handle_request(&self, id: u64) struct RequestHandler L:12-40 pub struct RequestHandler { const MAX_RETRIES L:60 const MAX_RETRIES: u32 = 3;# handle_request src/server.rs:42-58 (58 tokens, saved ~85%)pub async fn handle_request(&self, id: u64) -> Result<String, Error> { let row = self.db.get(id).await?; Ok(row.to_string())}The outline gives the map (~80% smaller than the file here); the slice gives only the territory
you asked for. When even a full body is too much, --compact returns an AST-pruned view —
signature plus a fold marker like // ... [N lines omitted] — and --signature returns the
declaration line alone. See Explore unfamiliar code for the workflow.
Exec compression pipeline
Section titled “Exec compression pipeline”Command output is the other firehose. ctxctl exec wraps a command and compresses its output
through a fixed pipeline:
- Keep key lines matching the configured patterns — by default
error,warning,failed,panic,fatal, case-insensitive. Diagnostic location lines (--> src/foo.rs:12:34, rustc/cargo style) are kept implicitly, so a kept error header retains itsfile:linecontext. - Keep head and tail — the first and last lines (default 5 each) preserve the command echo and the final verdict/summary.
- Collapse the middle into a single
... [N lines omitted]marker, but only past the collapse threshold (20 lines by default). - Warn when compression fails. If folding ran yet saved at most 10% — typically an
over-broad
--keeppattern — a deterministic warning line is appended.
ctxctl exec "cargo build"$ cargo builderror[E0308]: mismatched types --> src/main.rs:12... [34 lines omitted]warning: unused variable: `x` --> src/server.rs:88 = note: 2 warnings emittedSaved ~70% (1,240 -> 372 tokens)Compression streams incrementally as the child process produces output, so memory stays bounded by the head/tail windows plus kept matches — commands emitting gigabytes cannot exhaust it. The wrapped command’s exit code passes through unchanged. Details and tuning live in Compress command output.
Byte stability feeds prompt caching
Section titled “Byte stability feeds prompt caching”Saving tokens once is table stakes. CtxCtl’s core contract is that output is byte-stable: the body never contains timestamps, counters, random values, machine-specific paths, or PIDs. Same input + same config → byte-identical output.
That property compounds with provider prompt caching — roughly ~90% discounts on cache hits at Anthropic and ~50% at OpenAI. A deterministic tool means identical runs produce identical prefixes, so yesterday’s cached tokens stay cached today. Volatile output (timestamps, progress counters) would invalidate the cache and silently give the savings back.
Even the savings metric obeys this: saved N% is a deterministic function computed with the
cl100k_base BPE tokenizer — never an external measurement.
Stateless by design
Section titled “Stateless by design”There is no index, no cache, no daemon, no session state. Every invocation parses and discards. This costs some redundant parsing on repeat reads, and buys:
- Zero setup and zero staleness — a file edited mid-session is parsed fresh next call.
- Nothing to corrupt, migrate, or clean up across worktrees and branches.
- Config files hold only default-behavior preferences, resolved identically everywhere.
When ctxctl helps least
Section titled “When ctxctl helps least”Honesty about limits matters more than a flattering headline number. Savings scale inversely with how well your provider already caches:
- On models whose providers cache aggressively — deepseek-v4-flash cut session cost −33% versus built-in tools in the benchmark — the relative cost gain shrinks, because the baseline is already discounted, even though uncached (fully billed) input still drops.
- On weak-caching models the headline win is larger: solar-pro4 saved 47% session cost.
In both cases ctxctl reduces what gets billed as new input. How much of that reduction converts into dollars depends on your provider’s cache quality — see the caveats section of Benchmark. Two further honest limits:
- Tool-heavy sessions took roughly 2× wall-clock time in the benchmark; you trade seconds for dollars and cache pressure.
- Tiny inputs invert the math: on a small file the JSON envelope can exceed the file itself, so
saved%may legitimately be 0.
Where to go next
Section titled “Where to go next”- Explore unfamiliar code — the outline → slice workflow.
- Compress command output — tuning
execfor builds, tests, and CI logs. - Wire up coding agents — MCP tools or CLI + skill.
- Commands — the full command surface and flags.