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

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.
mcp__checkpoint__checkpoint with two fields:instructions: what the compaction summary must keep and what it may drop.resume: the first message after compaction, naming the next step.$.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".
Claude Code with mods enabled on your account.
/plugin install checkpoint@ryanlewis-plugins
To try a local checkout for one session:
claude --plugin-dir plugins/checkpoint
claude plugin validate plugins/checkpoint
claude plugin test plugins/checkpointhooks/register.ts 70 lines1import 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