Auto handoff at 250k tokens or 35% context: Claude writes a handoff, the context clears and the handoff reloads itself.

Stops long Claude sessions from rotting. At 35% context or 250k tokens (whichever comes first) Claude finishes its current step and writes a handoff. Then the context is cleared and the handoff submitted as the next prompt automatically, and the work continues in a fresh window. Nothing to type, copy or paste.
Inside Claude Code:
/plugin marketplace add hasuwini77/fresh-ctx
/plugin install fresh-ctx@fresh-ctx
Update later with /plugin marketplace update fresh-ctx. Restart Claude Code. From then on:
/clear and submits the handoff as the next prompt.Type fresh ctx any time to hand off early.
| Setting (env var) | Default | Meaning |
|---|---|---|
FRESH_CTX_PCT | 35 | Trigger point, % of window |
FRESH_CTX_TOKENS | 250000 | Trigger point in tokens; whichever of the two comes first wins |
FRESH_CTX_RENUDGE | 50000 | If Claude keeps going after a nudge, nudge again every this many tokens |
FRESH_CTX_MIN_GROWTH | 5 | A session that started from a handoff must grow this many points before handing off again (stops loops) |
FRESH_CTX_WINDOW | auto | Window size; read from the session, override only if needed |
FRESH_CTX_DIR | ~/.claude/fresh-ctx | Where handoffs live (one per project folder) |
Set these in ~/.claude/settings.json under "env".
Works in any permission mode: the mod approves only the fresh-ctx skill and its save script, so a handoff never waits on a prompt. Subagent tool calls are ignored.
Requires bash. Tested on Linux/WSL; macOS should work; native Windows is untested.
The chat app has no hooks and no /clear, so this version can't be automatic. Download fresh-ctx-chat.zip and upload it under Settings → Capabilities → Skills. Say fresh ctx (Claude also offers it when a chat gets long), copy the block it gives you, open a new chat, and paste it in.
/clear, and the handoff still reloads by itself..md.loaded in ~/.claude/fresh-ctx/.hooks/register.ts 52 lines1import type { Register } from 'claude-code'
2
3// The automatic half of fresh-ctx. When a main-thread turn ends:
4// - a handoff is waiting for this session → /clear, then submit the handoff as the next prompt;
5// - context is past the threshold with no handoff yet → ask Claude to write one (once per session).
6// Paths and env live in bash (hooks/mod.sh) so the mod and the shell hooks agree on them.
7const field = (input: unknown, key: string): string => {
8 const value = (input as Record<string, unknown> | null)?.[key]
9 return typeof value === 'string' ? value : ''
10}
11
12export const register: Register = on => {
13 const nudged = new Set<string>()
14
15 // The handoff must never wait on a permission prompt: allow the skill and its save script, nothing else.
16 on('tool.check', { tool: 'Skill' }, ($, e, next) =>
17 /(^|:)fresh-ctx$/.test(field(e.input, 'skill')) ? { decision: 'allow' } : next(e))
18 on('tool.check', { tool: 'Bash' }, ($, e, next) =>
19 field(e.input, 'command').startsWith(`bash "${$.plugin.root}/skills/fresh-ctx/save.sh"`) ? { decision: 'allow' } : next(e))
20
21 on('session.measure', async ($, e, next) => {
22 if (e.context.window) await $.process.run(['bash', `${$.plugin.root}/hooks/mod.sh`, 'window', String(e.context.window)])
23 return next(e)
24 })
25
26 on('turn.complete', async ($, e, next) => {
27 const done = await next(e)
28 if (e.agentId || e.isAborted) return done
29
30 const sid = await $.session.id()
31 const sh = (...args: string[]) => $.process.run(['bash', `${$.plugin.root}/hooks/mod.sh`, ...args])
32
33 void (async () => {
34 const handoff = (await sh('take', sid)).stdout.trim()
35 if (handoff) {
36 $.ui.toast('fresh-ctx: clearing context and reloading the handoff')
37 await $.command.run({ command: 'clear' })
38 await sh('mark', await $.session.id())
39 await $.prompt.submit({ text: `[fresh-ctx] Handoff from the previous context window. Continue from here; do not redo finished work.\n\n${handoff}` })
40 return
41 }
42 const { context } = await $.session.usage()
43 if (!context.tokens || nudged.has(sid)) return
44 if ((await sh('due', sid, String(context.tokens), String(context.window))).stdout.trim() !== 'yes') return
45 nudged.add(sid)
46 await $.prompt.submit({ text: `[fresh-ctx] Context is at ${context.percent}% (${context.tokens} tokens). Run the fresh-ctx skill now: write the handoff and stop.` })
47 })().catch(err => $.ui.toast(`fresh-ctx: ${String(err)}`))
48
49 return done
50 })
51}
52