SLOPSHOPPER

micro-compaction

Adds `/compact micro`: elides Read results and strips thinking, keeping the conversation structure

new
★ 2v0.1.2Unlicenseupdated 2026-10-08ruihe774/cc-micro-compaction
A shopper browsing a rack in a slop shop
README

micro-compaction

A Claude Mod that adds micro-compaction as /compact micro. Instead of asking a model to summarize your whole conversation, it trims the two bulkiest and cheapest-to-recover parts of the transcript and leaves everything else exactly as it was.

What it does

When you run /compact micro, the plugin rewrites the conversation transcript locally:

  • File reads are elided. Each successful Read tool result is replaced with a short placeholder such as [file contents elided by compaction: <path>, N lines. Read it again if needed.] (or lines A-B for partial reads). Images and PDFs get one too ([image elided by compaction: <path>, WxH. …], [PDF elided …], [PDF pages elided …, pages N]), since their image and document blocks are usually the largest results in a conversation. Claude can read the file again on its own whenever it needs the contents.
  • Thinking is dropped. Thinking-only messages are removed from the transcript.
  • Everything else is kept. Your prompts, Claude's replies, tool calls, and non-Read tool output stay unchanged, so the structure and wording of the conversation survive.

Some results are deliberately left alone: errored reads, text results already shorter than their placeholder ("unchanged since last read" stubs, tiny files), and earlier placeholders. Running /compact micro repeatedly is safe, because elision is idempotent.

When it runs

  • Only for a manual /compact micro.
  • Any other /compact, with or without instructions (/compact keep the plan), is passed through unchanged to Claude Code's normal model-written summary.
  • Automatic compaction (when the context window fills up) and other compaction triggers also use the normal summary, since that may be needed to fit the window.

Installation

Requires Claude Code v2.1.287 or later.

Install it from the official Anthropic plugin directory. In case you haven't added this marketplace yet, add it first, and refresh it to get the latest listing:

claude plugin marketplace add anthropic-plugin-directory
claude plugin marketplace update anthropic-plugin-directory
claude plugin install micro-compaction@anthropic-plugin-directory

If you prefer using the TUI, inside a session, use /plugin marketplace add, /plugin marketplace update and /plugin install with the same arguments.

To update to a newer release later:

claude plugin marketplace update anthropic-plugin-directory
claude plugin update micro-compaction@anthropic-plugin-directory

To try a local checkout while developing, load it directly instead:

claude --plugin-dir /path/to/micro-compaction

Then run /compact micro. Plain /compact keeps working as usual.

Best practice

  • Lower bashOutputMaxChars in your Claude Code config. Micro-compaction leaves Bash output alone, but long output is spilled to a file that Claude then reads with Read, and that read can be elided. The default threshold is 30000 characters; a lower one (for example 10000) spills more output to files, so more of it can be stripped.
  • Watch prompt cache warmness. Micro-compaction rewrites earlier messages, which invalidates the prompt cache. Running it often while the cache is still warm is not cost-efficient, so it pays off most when the cache has gone cold anyway or the elided content is large.
  • Use full compaction when the context is mostly unrelated information. If a large part of the conversation no longer matters and you don't need to preserve its structure, plain /compact (the default) is a better fit, as is /clear. Micro-compaction keeps everything except file reads and thinking, so it can't discard irrelevant discussion.
  • Compact at natural breakpoints. The best moment is after an exploration or reading phase, when many files have been read but the findings are already in the conversation, and before the next phase begins.
  • Prefer the Read tool for file contents. Only Read results are elided. Output from cat in Bash, or from other tools such as MCP tools, web fetches and searches, is kept as is.
  • Expect re-reads if you keep working on the same files. Claude re-reads an elided file when it needs it again, which adds back the tokens you saved. Micro-compaction helps most when the files you read are no longer needed.
  • It shines in image and PDF heavy sessions. Image and PDF results are usually the largest blocks in a transcript, and they are always elided.
  • Fall back to full compaction if micro isn't enough. Prompts, replies, tool calls and non-Read output are kept, so a long conversation can still fill the window. Run /compact for a summary, or rely on automatic compaction, which always uses the normal summary.

What the hook does

The plugin registers exactly one hook, on the session.compact event, with the filter trigger: 'manual'. session.compact is also the name of the call that Claude Code makes to compact a conversation, so this hook sees the conversation's message list for every manual /compact. It changes the call only when the instructions are exactly micro (ignoring surrounding whitespace and case): then it returns a rewritten list (file reads elided, thinking dropped) in place of core's model-written summary. For any other /compact, with or without instructions, it calls next with the call unchanged, so Claude Code compacts as usual. Other triggers (automatic, plugin, precompute) never reach the hook because of its trigger: 'manual' filter. The plugin never makes the session.compact call itself, and it hooks no other event.

What it runs, sends, and fetches

  • It runs a single session.compact hook, written in TypeScript (hooks/register.ts and hooks/elide.ts), inside Claude Code.
  • It makes no model calls, no network requests, and no shell commands.
  • It does not read or write any files itself, and it uses no credentials, environment variables, or MCP servers.
  • It does not collect, store, or transmit any data. It only edits the in-memory message list that Claude Code hands to the hook, and it writes one debug-level log line with the count of elided reads and dropped thinking blocks.
  • It has no package dependencies and no install step.

Development

The core logic is the pure function microCompact(messages) in hooks/elide.ts, covered by unit tests in tests/elide.test.ts.

claude plugin validate .
claude plugin test
Source 2 files
hooks/register.ts 13 lines
1import { microCompact } from './elide.ts'
2
3export function register(on: any) {
4  // Only the person's `/compact micro`; any other /compact (with or without instructions)
5  // and auto, plugin, precompute go to core's summary untouched
6  on('session.compact', { trigger: 'manual' }, async ($: any, e: any, next: any) => {
7    if (e.instructions?.trim().toLowerCase() !== 'micro') return next(e)
8    const { messages, stats } = microCompact(e.messages)
9    $.ui.log(`Elided ${stats.elided} file reads with ${stats.chars} chars`)
10    return { messages }
11  })
12}
13
hooks/elide.ts 108 lines
1import type { SessionMessage as Msg } from 'claude-code'
2
3export type MicroCompactStats = {
4  elided: number
5  /** Characters of Read result text replaced by placeholders */
6  chars: number
7  thinkingDropped: number
8}
9
10const MARKER = ' elided by compaction: '
11const AGAIN = '. Read it again if needed.]'
12// Any of our placeholders, so a second pass leaves them be
13const PLACEHOLDER = /^\[[^\]\n]* elided by compaction: /
14
15// Read numbers its output as `   12\tline`; the first and last numbers give the range read
16const LINE_NO = /^\s*(\d+)\t/
17
18const pathOf = (input: Record<string, unknown>) => (typeof input.file_path === 'string' ? input.file_path : 'unknown file')
19
20function describeText(input: Record<string, unknown>, text: string): string {
21  let first: number | null = null
22  let last: number | null = null
23  for (const line of text.split('\n')) {
24    const m = LINE_NO.exec(line)
25    if (!m) continue
26    const n = Number(m[1])
27    first ??= n
28    last = n
29  }
30  const range = first === null ? '' : first === 1 ? `, ${last} lines` : `, lines ${first}-${last}`
31  return `[file contents${MARKER}${pathOf(input)}${range}${AGAIN}`
32}
33
34/**
35 * The placeholder for a Read whose bulk is an image or document block rather than text
36 * (`result.type` image, pdf, or parts: PDF pages rendered as images), or null for any other.
37 * Their `text` is empty or a one-line note, so length says nothing about their size.
38 */
39function describeMedia(input: Record<string, unknown>, result: unknown): string | null {
40  if (!result || typeof result !== 'object') return null
41  const r = result as { type?: unknown; file?: any; firstPage?: unknown }
42  const path = pathOf(input)
43  switch (r.type) {
44    case 'image': {
45      const d = r.file?.dimensions
46      const size = d?.originalWidth && d?.originalHeight ? `, ${d.originalWidth}x${d.originalHeight}` : ''
47      return `[image${MARKER}${path}${size}${AGAIN}`
48    }
49    case 'pdf':
50      return `[PDF${MARKER}${path}${AGAIN}`
51    case 'parts': {
52      const first = typeof r.firstPage === 'number' ? r.firstPage : null
53      const count = typeof r.file?.count === 'number' ? r.file.count : null
54      const pages =
55        typeof input.pages === 'string' ? input.pages : first !== null && count !== null ? `${first}-${first + count - 1}` : null
56      return `[PDF pages${MARKER}${path}${pages ? `, pages ${pages}` : ''}${AGAIN}`
57    }
58    default:
59      return null
60  }
61}
62
63// Thinking blocks come as assistant messages of their own (one transcript entry per block),
64// which read as no text and no tool uses
65function isThinkingOnly(m: Msg): boolean {
66  return m.role === 'assistant' && !m.text && m.toolUses.length === 0
67}
68
69/**
70 * Elides the results of Read calls and drops thinking. Every other message keeps its
71 * handle, so the engine keeps it whole; a user message holding an elided result is rebuilt.
72 */
73export function microCompact(messages: readonly Msg[]): { messages: Msg[]; stats: MicroCompactStats } {
74  const reads = new Map<string, Record<string, unknown>>()
75  for (const m of messages) for (const u of m.toolUses) if (u.tool === 'Read') reads.set(u.tool_use_id, u.input)
76
77  const stats: MicroCompactStats = { elided: 0, chars: 0, thinkingDropped: 0 }
78  const out: Msg[] = []
79  for (const m of messages) {
80    if (isThinkingOnly(m)) {
81      stats.thinkingDropped++
82      continue
83    }
84    let changed = false
85    const toolResults = m.toolResults?.map(r => {
86      const input = reads.get(r.tool_use_id)
87      if (!input || r.isError || PLACEHOLDER.test(r.text)) return r
88      let text = describeMedia(input, r.result)
89      if (text === null) {
90        text = describeText(input, r.text)
91        // Already short (an "unchanged since last read" stub, a tiny file)
92        if (r.text.length <= text.length) return r
93      }
94      changed = true
95      stats.elided++
96      stats.chars += r.text.length
97      return { tool_use_id: r.tool_use_id, isError: r.isError, text }
98    })
99    if (!changed) {
100      out.push(m)
101      continue
102    }
103    const { handle: _, ...rest } = m
104    out.push({ ...rest, toolResults })
105  }
106  return { messages: out, stats }
107}
108