SLOPSHOPPER

checkpoint

Lets a session compact its own context at a checkpoint, with its own instructions, then carry on

newguardtoaststatustooltimer
v0.1.0no licenseupdated 2026-10-08ryanlewis/claude-plugins/plugins/checkpoint
A shopper browsing a rack in a slop shop
README

checkpoint

A Claude Code mod that gives the model a checkpoint tool, so a session can compact its own context at a point it chooses, with summary instructions it writes itself, and then carry on.

How it works

  1. The model calls mcp__checkpoint__checkpoint with two fields:
  2. instructions: what the compaction summary must keep and what it may drop.
  3. resume: the first message after compaction, naming the next step.
  4. The model ends its turn.
  5. When the turn ends, the mod runs $.session.compact({ instructions }), then submits resume as a new prompt, so work continues from the summary.

An interrupted or failed turn keeps the checkpoint queued for the next turn that ends with an answer. Subagents cannot call the tool; it is for the main conversation.

The tool description tells the model when to checkpoint (a stretch of work finished and verified) and what to keep. In practice it works best when the prompt or skill driving the session says to use it, for example "checkpoint with the checkpoint tool after each phase".

Requirements

Claude Code with mods enabled on your account.

Install

/plugin install checkpoint@ryanlewis-plugins

To try a local checkout for one session:

claude --plugin-dir plugins/checkpoint

Tests

claude plugin validate plugins/checkpoint
claude plugin test plugins/checkpoint
Source 1 files
hooks/register.ts 70 lines
1import type { Register } from 'claude-code'
2
3const DESCRIPTION = `Compact your own context at a checkpoint, then carry on.
4
5Call it when a stretch of work is finished and verified and the next stretch does not need the detail that got you here: a phase or milestone landed, a subtask closed, a long investigation concluded, or before switching to unrelated work. Not mid-task, not while a step's outcome is unverified.
6
7Compaction runs once this turn ends, so end your turn straight after the call. Then the resume text arrives as a new message and you continue from the summary.
8
9instructions: what the summarizer must keep and what it may drop. Keep: the goal, decisions and why, verified facts with their evidence, things ruled out, open items and blockers, exact paths, ids and commands still needed, running background work. Drop: tool output already acted on, file dumps, superseded plans, dead ends beyond a one-line "ruled out".
10resume: the first message after compaction: the next concrete step, and any file to re-read first.`
11
12type Pending = { instructions: string; resume: string }
13
14export const register: Register = on => {
15  let pending: Pending | undefined
16
17  on('session.start', async ($, e, next) => {
18    await $.tool.register({
19      name: 'checkpoint',
20      description: DESCRIPTION,
21      inputSchema: {
22        type: 'object',
23        properties: {
24          instructions: { type: 'string', description: 'What the compaction summary must keep, and what it may drop.' },
25          resume: { type: 'string', description: 'The message that resumes work after compaction: the next step.' },
26        },
27        required: ['instructions', 'resume'],
28      },
29    })
30    return next(e)
31  })
32
33  on('tool.call', { tool: 'mcp__checkpoint__checkpoint' }, async ($, e, next) => {
34    if (e.agentId) return { deny: 'checkpoint is for the main conversation only.' }
35    const input = e as unknown as Partial<Pending>
36    if (!input.instructions?.trim() || !input.resume?.trim()) {
37      return { deny: 'checkpoint needs both instructions and resume.' }
38    }
39    pending = { instructions: input.instructions, resume: input.resume }
40    $.ui.status('checkpoint queued')
41    return { result: 'Checkpoint queued. Compaction runs when this turn ends: end your turn now, with no further tool calls.' }
42  }).catch(() => ({ deny: 'checkpoint: the mod failed; nothing was queued.' }))
43
44  on('turn.complete', async ($, e, next) => {
45    const result = await next(e)
46    // Only the main loop's answered turn; an interrupted or failed turn keeps it queued.
47    if (e.agentId || e.reason !== 'answer' || !pending) return result
48    const job = pending
49    pending = undefined
50    // Not inside this hook: the turn still ends through it, and compact rejects mid-turn.
51    $.clock.after(0, async () => {
52      try {
53        const r = await $.session.compact({ instructions: job.instructions })
54        if (r.skip) {
55          $.ui.status(undefined)
56          $.ui.toast(`checkpoint: compaction refused: ${r.skip}`)
57          return
58        }
59        $.ui.status(undefined)
60        await $.prompt.submit({ text: `Checkpoint reached and context compacted. Resume:\n\n${job.resume}` })
61      } catch (err) {
62        // Typically a new turn started first; keep it for the next turn end.
63        pending = job
64        $.ui.log(`checkpoint: ${String(err)}`, { to: 'debug' })
65      }
66    })
67    return result
68  })
69}
70