Gives Claude a continueInNewSession(prompt) tool: /clear, then start the fresh session with that prompt.

A Claude Code mod that gives the agent a tool:
mcp__continue-session__continueInNewSession(prompt: string)
When called, the mod waits for the current turn to end, runs /clear, and submits prompt as the first user message of the fresh session (as your own words, asUser).
Requires Claude Code v2.1.287 or later (claude --version).
A mod is code that runs inside Claude Code with your permissions; it is not sandboxed. This one:
/clear) when the agent calls the tool, andmaxChain (below) bounds how long that goes on.It reads and writes only its own plugin store, starts no processes and makes no network requests. To check that yourself before installing, clone this repository and run:
claude plugin validate plugins/continue-session
The hooks: and calls: lines list every event it handles and everything it asks Claude Code to do.
/plugin marketplace add CodelianceAI/Codeliance-Skills /plugin install continue-session@codeliance-skills
Take on the plan in openspec/changes/<name>. When it's done, call continueInNewSession with a prompt that tells the next session to pick the next best openspec change and do the same, including this same instruction. Stop when there is no clear next step.
maxChain (default 10): handoffs allowed in a row without a message from you. Any prompt you type resets it. Set it with /config or pluginConfigs in settings./clear)./clear succeeds but the submit doesn't, /continue-resend re-sends the saved prompt.Load the working copy instead of the installed one:
claude --plugin-dir /path/to/continue-session
or permanently, in ~/.claude/settings.json:
{ "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/continue-session" } }
Then:
claude plugin validate . claude plugin test .
hooks/register.ts 148 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// continueInNewSession(prompt): the agent hands its follow-up work to a brand-new
4// context. The tool only queues the handoff; once the current turn has ended the
5// mod runs /clear and submits `prompt` as the fresh session's first user message.
6
7const TOOL = 'continueInNewSession'
8const FULL = 'mcp__continue-session__continueInNewSession'
9const tag = '[continue-session]'
10const CHAIN_KEY = 'chain' // handoffs in a row since the person last typed
11const PENDING_KEY = 'pending' // prompt saved before /clear, removed once submitted
12const DEFAULT_MAX_CHAIN = 10 // keep in step with userConfig.maxChain.default in plugin.json
13
14const DESCRIPTION = [
15 'End this session and continue the work in a brand-new session with an empty context.',
16 'After your current turn ends, the conversation is cleared (/clear) and `prompt` is sent',
17 'as the first user message of the new session. Nothing else from this session carries over,',
18 'so `prompt` must be self-contained: point at the files, plans or specs to read.',
19 'Use it when the current unit of work is finished and a follow-up should start fresh.',
20 'Call it as your LAST action, then end your turn immediately without further tool calls.',
21].join(' ')
22
23// Module-level state: reset on reload, survives /clear (same process).
24let queued: string | undefined
25
26async function chain($: EngineInterface) {
27 return Number((await $.store.get(CHAIN_KEY)) ?? 0)
28}
29
30async function registerTool($: EngineInterface) {
31 await $.tool.register({
32 name: TOOL,
33 description: DESCRIPTION,
34 inputSchema: {
35 type: 'object',
36 properties: {
37 prompt: {
38 type: 'string',
39 description: 'The complete first message for the new session.',
40 },
41 },
42 required: ['prompt'],
43 additionalProperties: false,
44 },
45 })
46}
47
48async function handoff($: EngineInterface, prompt: string) {
49 await $.store.set(PENDING_KEY, prompt)
50 try {
51 // Queued by the engine until the session is idle, i.e. after the current turn.
52 await $.command.run({ command: 'clear' })
53 } catch (err) {
54 await $.store.delete(PENDING_KEY)
55 $.ui.log(`${tag} /clear failed, staying in this session: ${String(err)}`)
56 $.ui.toast(`${tag} handoff failed (/clear)`)
57 return
58 }
59 try {
60 const r = await $.prompt.submit({ text: prompt, asUser: true })
61 if ((r as { drop?: string }).drop !== undefined) throw new Error(`dropped: ${(r as { drop?: string }).drop}`)
62 await $.store.delete(PENDING_KEY)
63 } catch (err) {
64 $.ui.log(`${tag} submit failed after /clear: ${String(err)}. Run /continue-resend to retry.`)
65 $.ui.toast(`${tag} handoff prompt not sent; /continue-resend`)
66 }
67}
68
69export const register: Register = (on, options) => {
70 queued = undefined
71 const maxChain = Number((options as { maxChain?: number }).maxChain ?? DEFAULT_MAX_CHAIN)
72
73 on('session.start', async ($, e, next) => {
74 await registerTool($)
75 await $.command.register({
76 name: 'continue-resend',
77 description: 'Re-send a continueInNewSession prompt that was not delivered',
78 })
79 return next(e)
80 })
81
82 // session.start does not fire after /clear; re-registering replaces the tool,
83 // so this is harmless if the registration already survived.
84 on('classic.SessionStart', async ($, e, next) => {
85 const out = await next(e)
86 if (e.source === 'clear') {
87 try { await registerTool($) } catch (err) { $.ui.log(`${tag} re-register after /clear failed: ${String(err)}`) }
88 }
89 return out
90 })
91
92 on('tool.call', { tool: 'mcp__continue-session__continueInNewSession' }, async ($, e) => {
93 if (e.agentId !== undefined) {
94 return { deny: 'Only the main session can hand off; subagents should return their result instead.' }
95 }
96 const prompt = typeof e.prompt === 'string' ? e.prompt.trim() : ''
97 if (!prompt) return { deny: '`prompt` must be a non-empty, self-contained message.' }
98 if (queued !== undefined) return { deny: 'A handoff is already queued. End your turn now.' }
99
100 const n = await chain($)
101 if (n >= maxChain) {
102 $.ui.toast(`${tag} chain limit (${maxChain}) reached; not handing off`)
103 return { deny: `Handoff limit reached (${maxChain} in a row without the user). Stop and summarise for the user instead.` }
104 }
105 await $.store.set(CHAIN_KEY, n + 1)
106
107 // Run outside this hook: $.command.run rejects inside a hook the turn waits on.
108 $.clock.after(0, () => {
109 void handoff($, prompt).finally(() => { queued = undefined })
110 })
111 // Set only once the timer that clears it exists, so a failure above can't leave every tool denied.
112 queued = prompt
113 $.ui.log(`${tag} handoff ${n + 1}/${maxChain} queued; /clear runs when this turn ends`)
114 return { result: 'Handoff queued. The session will be cleared and the new session started with your prompt as soon as this turn ends. Do not call any more tools; end your turn now.' }
115 }).catch(() => (
116 // Fail closed: without this a hook that threw (say, the store) is skipped and the chain limit with it.
117 { deny: 'The handoff could not be queued (internal error). Stay in this session and tell the user.' }
118 ))
119
120 // Anything the agent tries after queuing a handoff would be wiped by /clear anyway.
121 // No .catch here or on prompt.submit below: skipped on failure is right, neither should block the person's work.
122 on('tool.call', async ($, e, next) => {
123 if (queued !== undefined && e.agentId === undefined && e.tool !== FULL) {
124 return { deny: 'A handoff to a new session is queued; this session is about to be cleared. End your turn now.' }
125 }
126 return next(e)
127 })
128
129 // A message the person typed (not our own asUser submit) resets the chain.
130 on('prompt.submit', async ($, e, next) => {
131 if (e.origin.kind === 'composer' || e.origin.kind === 'bridge') {
132 if (!e.text.trimStart().startsWith('/')) await $.store.set(CHAIN_KEY, 0)
133 }
134 return next(e)
135 })
136
137 on('command.run', { command: 'continue-resend' }, async $ => {
138 const pending = await $.store.get(PENDING_KEY)
139 if (typeof pending !== 'string') return { text: `${tag} nothing to re-send` }
140 $.clock.after(0, () => {
141 void $.prompt.submit({ text: pending, asUser: true })
142 .then(() => $.store.delete(PENDING_KEY))
143 .catch(err => $.ui.log(`${tag} re-send failed: ${String(err)}`))
144 })
145 return { text: `${tag} re-sending the pending handoff prompt` }
146 })
147}
148