Repo: ๐ github.com/bayuh85/ctxtrim (MIT, Node.js 20+)
The problem
Coding agents like Claude Code, Cursor, and Cline are great until the context window fills up with junk. A test run dumps hundreds of lines. A server log repeats the same startup message over and over. The agent cats a large JSON file to check one field. All of that lands in context, gets re-read on every turn, and pushes out the stuff that actually matters โ like what you decided twenty minutes ago.
The usual fixes are manual: | tail -50, grep ERROR, or telling the agent "please don't read the whole file." That works until it doesn't.
What ctxtrim does
ctxtrim shrinks what a coding agent reads:
- Press: large logs, test runs, and file dumps are trimmed mechanically โ no network, no key needed.
-
Gloss (optional): if the trimmed result is still big, a cheap OpenAI-compatible model summarizes it (default:
zai/glm-5.3). - Session notes: decisions and findings go into a compact session ledger instead of a growing transcript.
It ships as both a command-line tool and an MCP server, and both expose the same operations.
Quickstart
Requires Node.js 20 or newer.
git clone https://github.com/bayuh85/ctxtrim.git
cd ctxtrim
npm install
npm run build
The mechanical press works with no key at all. If you want model summaries, set a key:
cp .env.example .env
# CTXTRIM_API_KEY=your-key
Now pipe something noisy through it:
export CTXTRIM_API_KEY=your-key
# optional; these are already the defaults
export CTXTRIM_BASE_URL=https://atmorouter.dev/v1
export CTXTRIM_MODEL=zai/glm-5.3
npm test 2>&1 | node dist/cli.js shrink --kind test --session demo
node dist/cli.js note add --session demo --tag decision "Retry refresh with jitter."
node dist/cli.js recall --session demo
The brief goes to stdout (so you can keep piping), and a one-line receipt goes to stderr. The README's example: pressing a 300-line log containing three connection errors and one warning reports:
ctxtrim ยท log ยท press ยท 12578 โ 1299 bytes ยท kept 10%
The brief keeps the repeated startup line collapsed (ready ร3), the errors, the warning, and the head and tail. The quiet middle becomes a single omission line.
The receipt tells you which path was taken:
-
passthroughโ input was already small -
pressโ mechanical trim -
glossโ model summary -
cacheโ a gloss reused for the same text and model
Setting it up with your agent (MCP)
Build first (npm run build), then point your client at node /absolute/path/to/ctxtrim/dist/cli.js serve.
Claude Code
Project .mcp.json (or your user config):
{
"mcpServers": {
"ctxtrim": {
"command": "node",
"args": ["/absolute/path/to/ctxtrim/dist/cli.js", "serve"],
"env": {
"CTXTRIM_API_KEY": "your-key",
"CTXTRIM_BASE_URL": "https://atmorouter.dev/v1",
"CTXTRIM_MODEL": "zai/glm-5.3"
}
}
}
}
Or from a shell:
claude mcp add ctxtrim --env CTXTRIM_API_KEY=your-key -- node /absolute/path/to/ctxtrim/dist/cli.js serve
Cursor
Put the same mcpServers.ctxtrim block in .cursor/mcp.json in your project, or add it via Cursor Settings โ MCP.
Cline
Add the same entry to Cline's MCP settings file:
- Linux:
~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json - macOS:
~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json - Windows:
%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
(VS Code Insiders and other forks use their own User/globalStorage directory.) Cline picks up the file on save.
The tools your agent gets
| Tool | Use it when |
|---|---|
shrink |
A tool result is too large to keep. mode is auto (default), gloss, or offline. |
note_add |
You decided something, found a cause, or have a next step. |
note_list |
You only want the current notes. |
note_clear |
The session is finished. |
recall |
You need the stats, the rolled-up digest, and the notes. |
After restarting the MCP client, the practical habit is: ask the agent to call shrink on bulky tool output, and recall at the start of a long task. You can bake that into your CLAUDE.md, Cursor rules, or Cline custom instructions.
How a shrink works
-
Detect.
autoclassifies the input as a log, a test run, a file dump (source or JSON), or generic text. Override with--kind. - Press. Duplicate lines collapse. Logs keep errors, warnings, and the head and tail. Test output keeps failures, nearby frames, and the summary. Source code keeps an outline of imports and declarations. JSON becomes a short description of its shape. Anything still huge is cut to a character cap.
-
Gloss. If the press is still large and
CTXTRIM_API_KEYis set, ctxtrim sends the press โ not the original blob โ toPOST {CTXTRIM_BASE_URL}/chat/completionsand asks for a plain-text brief underCTXTRIM_TARGET_CHARS. A gloss that comes back empty or longer than the press is thrown away. Results are cached underCTXTRIM_HOME.
A few behaviors worth knowing:
-
--offlinenever calls a model. -
--glossrequires a key and exits with status 2 if it's missing. - Provider errors fall back to the press, unless you explicitly asked for
--gloss. - Binary input isn't expanded; you get the original size and a one-line stub.
The idea behind the split is simple: the mechanical press is free and deterministic, and the cheap model only ever sees already-trimmed text, so the summary step stays much cheaper than having your main coding model read the raw output.
The session ledger
Notes are stored per session as JSON under $CTXTRIM_HOME/sessions/ (default ~/.ctxtrim), one compact line per note, with optional tags like decision, blocker, or a filename stem. When active notes exceed CTXTRIM_NOTE_BUDGET, the oldest lines roll up into a digest so the session stays small enough to paste back into a prompt.
recall prints shrink totals (bytes in, bytes out, how much was kept), the digest, and current notes. Note that the shrunk brief itself isn't copied into the ledger โ if you want the agent to remember a conclusion, write a note.
Tuning
Everything is an environment variable. The main knobs:
| Variable | Default | Role |
|---|---|---|
CTXTRIM_PASSTHROUGH_CHARS |
2000 |
Return text as-is at or below this size |
CTXTRIM_GLOSS_THRESHOLD |
3500 |
Gloss when the press is still at least this long |
CTXTRIM_TARGET_CHARS |
1800 |
Budget given to the model |
CTXTRIM_PRESS_CHARS |
12000 |
Cap on the press (what the model sees) |
CTXTRIM_NOTE_BUDGET |
4000 |
Active note budget before rollup |
Use it as a library
import { loadConfig, shrink, Ledger } from "ctxtrim";
const result = await shrink(logText, { env: process.env, kind: "log" });
const ledger = new Ledger(loadConfig().home);
await ledger.add("feature-auth", "Refresh flake is a clock skew, not a bad signature.");
Credit
The concept is inspired by context-mode by mksglu. ctxtrim is a separate project and is not affiliated with it. If you're exploring this space, check out context-mode too.
About AtmoRouter: a cheap AI API gateway and OpenRouter alternative
Out of the box, ctxtrim sends glosses to AtmoRouter using zai/glm-5.3, which fits the "keep the summary step cheaper than the coding model" idea. Any OpenAI-compatible endpoint works too. AtmoRouter is a cheap AI API gateway that speaks both the OpenAI Chat Completions API and the Anthropic Messages API, so one key reaches 100+ models from Cursor, Cline, Claude Code, or any compatible client. It is pay-per-token with no subscription, accepts QRIS and USDC, and prices sit well below official rates: AtmoRouter advertises an average of 83.7% below official prices, which makes it an affordable LLM API and a practical OpenRouter alternative for agent workloads.
Try it
If your agent keeps drowning in test output and logs, give it a try:
๐ github.com/bayuh85/ctxtrim
Feedback on the press heuristics is especially welcome โ if there's a log or test format it handles badly, an issue with a sample is the most useful thing you can send.
Top comments (0)