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

  1. Detect. auto classifies the input as a log, a test run, a file dump (source or JSON), or generic text. Override with --kind.
  2. 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.
  3. Gloss. If the press is still large and CTXTRIM_API_KEY is set, ctxtrim sends the press — not the original blob — to POST {CTXTRIM_BASE_URL}/chat/completions and asks for a plain-text brief under CTXTRIM_TARGET_CHARS. A gloss that comes back empty or longer than the press is thrown away. Results are cached under CTXTRIM_HOME.

A few behaviors worth knowing:

  • --offline never calls a model.
  • --gloss requires 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 (the default model endpoint)

Out of the box, ctxtrim sends glosses to AtmoRouter using zai/glm-5.3. AtmoRouter is an OpenAI- and Anthropic-compatible gateway with pay-per-token pricing, payment via QRIS and USDC, and cheap access to frontier models — which fits the "keep the summary step cheaper than the coding model" idea.

But you don't need it. ctxtrim only depends on POST /chat/completions with messages, model, temperature, and max_tokens, so any OpenAI-compatible provider works:

export CTXTRIM_BASE_URL=https://your-provider.example/v1 export CTXTRIM_API_KEY=your-key export CTXTRIM_MODEL=your-cheap-model 

And if you skip the key entirely, you still get the mechanical press with no network calls.

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.