Save this Claude Code session and write a note for the next one. If you use herdr, /park also asks your other agent tabs to save, and can close them. /unpark…

/park saves your work in the current Claude Code tab and writes a short note for your next session. /unpark reads that note and picks up where you stopped.
herdr is optional. Both commands work without it. If you run Claude Code inside herdr, /park also asks every other idle agent tab to save first, and /park close closes the tabs that saved.
At the end of a day you stop in the middle of something. Tomorrow you have to remember what was done, what is still open, and what to do first.
Type /park and Claude writes that down in a note: what is done, what is open, the first step. Tomorrow, /unpark has Claude read the note and carry on. If you have five agent tabs open in herdr, /park asks each idle one to save before your note is written, so nothing is left half-saved.
Paste these two lines into a terminal (not into Claude):
claude plugin marketplace add varunmoka7/park
claude plugin install park@park
Needs Claude Code 2.1.287 or newer with mods enabled. herdr is not required. Install it (https://herdr.dev) only if you want /park to save your other agent tabs too.
macOS and Linux only for the full feature set. The note-finding step uses find. On Windows the commands work, but the session-start message and /unpark will not find your notes.
/park saves this tab and writes the next-session note. In herdr it first asks every other idle agent tab to save, and waits for them (up to 9 minutes each)./park close closes the tabs that saved, in herdr only. It refuses if any tab did not save or has started working again, and it never closes the tab you are in./unpark reads the newest note and follows it.The commands are registered in every interactive Claude Code session.
Notes go to the .park/ folder of the project, as .park/park-<topic>.md. You may want to add .park/ to your .gitignore.
.park folder in your project, to find the newest note..park/park-<topic>.md, written by Claude when you run /park. Nothing else.herdr binary only when you are inside herdr, to list tabs, send the save request and close tabs./park. It acts only on command.claude plugin uninstall park@park
You can delete the .park folder in a project at any time.
/park, run /park again before /park close.MIT. Copyright (c) 2026 Varun Moka.
hooks/register.ts 200 lines1import type { EngineInterface, Register } from 'claude-code'
2
3type Agent = {
4 pane_id: string
5 tab_id: string
6 agent: string
7 agent_status: string
8 name?: string
9 terminal_title_stripped?: string
10}
11
12// saving: the save prompt is out. busy: the tab was mid-task, so nothing was sent.
13type Outcome = 'saving' | 'saved' | 'blocked' | 'busy' | 'failed'
14type Tab = { agent: Agent; outcome: Outcome }
15
16// process.run allows ten minutes at most; herdr gives up half a minute before that.
17const SAVE_WAIT_MS = 540_000
18// A note older than this is not offered at session start.
19const FRESH_DAYS = 3
20
21// Where the next-session notes go, relative to the project folder.
22const NOTE_DIR = '.park'
23
24const SAVE =
25 'Save your work now. Finish or write down anything unsaved: what was done, what was not checked, dead ends, and anything a later session needs. Ask nothing, do not commit or push, then stop.'
26
27const parkPrompt = (tabList: string) =>
28 [
29 'Park this session.',
30 '1. Save your own work now: finish or write down anything unsaved. Ask nothing.',
31 `2. Write the start prompt for the next session to the ${NOTE_DIR}/ folder of this project as park-<topic>.md: what is done, what is open, the first step, and any agent tab below that did not save.`,
32 '3. Reply with the file path and one line per agent tab.',
33 tabList === '' ? 'There are no other agent tabs.' : `Agent tabs after /park:\n${tabList}`,
34 ].join('\n')
35
36// The tabs of the last /park, by pane. A reload empties it; /park close then asks for a new /park.
37let tabs = new Map<string, Tab>()
38// The tab this session runs in; /park close never closes it, even when another agent shares it.
39let ownTab: string | undefined
40let isParking = false
41let clearStatus: { cancel: () => void } | undefined
42
43const label = (a: Agent) => (a.name ?? a.terminal_title_stripped ?? a.pane_id).slice(0, 28)
44
45const lines = (list: Iterable<Tab>) =>
46 [...list].map(t => `${t.outcome.padEnd(7)} ${t.agent.agent.padEnd(7)} ${label(t.agent)}`).join('\n')
47
48// Without herdr (no session, or no pane id to tell this tab apart) there are no other tabs to act on.
49async function herdr($: EngineInterface, args: readonly string[], timeoutMs?: number) {
50 const session = await $.env.get('HERDR_SESSION')
51 if (session === undefined || (await $.env.get('HERDR_PANE_ID')) === undefined) return undefined
52
53 return $.process.run(['herdr', '--session', session, ...args], timeoutMs === undefined ? undefined : { timeoutMs })
54}
55
56// undefined means the list could not be read (no herdr, or herdr failed); [] means there are no other tabs.
57async function others($: EngineInterface): Promise<Agent[] | undefined> {
58 const ran = await herdr($, ['agent', 'list'])
59 if (ran === undefined || ran.exitCode !== 0) return undefined
60 const self = await $.env.get('HERDR_PANE_ID')
61 const agents: Agent[] = JSON.parse(ran.stdout).result.agents
62 ownTab = agents.find(a => a.pane_id === self)?.tab_id ?? ownTab
63
64 return agents.filter(a => a.pane_id !== self)
65}
66
67// A command hook may not submit a prompt itself (it holds the turn the prompt would wait on),
68// so the prompt goes out from a timer just after the hook has answered.
69function later($: EngineInterface, text: string) {
70 $.clock.after(250, () => void $.prompt.submit({ text, asUser: true }))
71}
72
73function show($: EngineInterface) {
74 $.ui.status(tabs.size === 0 ? undefined : `park: ${[...tabs.values()].map(t => `${label(t.agent)} ${t.outcome}`).join(', ')}`)
75}
76
77// Known limit: "saved" means the tab took the save prompt and went quiet. The pane is not read to
78// confirm the save itself; add a `herdr agent read` check if a false "saved" ever shows up.
79async function save($: EngineInterface, tab: Tab) {
80 try {
81 const ran = await herdr(
82 $,
83 ['agent', 'prompt', tab.agent.pane_id, SAVE, '--wait', '--timeout', String(SAVE_WAIT_MS)],
84 SAVE_WAIT_MS + 30_000,
85 )
86 const status = ran?.exitCode === 0 ? /"agent_status":"(\w+)"/.exec(ran.stdout)?.[1] : undefined
87 tab.outcome = status === 'idle' || status === 'done' ? 'saved' : status === 'blocked' ? 'blocked' : 'failed'
88 } catch {
89 tab.outcome = 'failed'
90 }
91 show($)
92}
93
94async function close($: EngineInterface) {
95 const all = [...tabs.values()]
96 if (all.length === 0) return { text: 'Nothing parked. Run /park first.' }
97 // A saved tab that was given new work since is no longer safe to close.
98 const current = await others($)
99 if (current === undefined) return { text: 'Not closing. Could not read the tab list from herdr.' }
100 // Any agent working in a tab keeps that tab open, including one that started after /park.
101 const workingTabs = new Set(current.filter(a => a.agent_status === 'working').map(a => a.tab_id))
102 const open = all.filter(t => t.outcome !== 'saved' || workingTabs.has(t.agent.tab_id))
103 if (open.length > 0) return { text: `Not closing. These tabs are not saved, or are working again:\n${lines(open)}` }
104
105 const ids = new Set(all.map(t => t.agent.tab_id))
106 ids.delete(ownTab ?? '')
107 const failed = new Set<string>()
108 for (const id of ids) {
109 const ran = await herdr($, ['tab', 'close', id])
110 if (ran?.exitCode !== 0) failed.add(id)
111 }
112 // Tabs that did not close stay listed, so /park close can be run again.
113 tabs = new Map([...tabs].filter(([, t]) => failed.has(t.agent.tab_id)))
114 show($)
115 const closed = ids.size - failed.size
116 const text = `Closed ${closed} saved tab${closed === 1 ? '' : 's'}.`
117
118 return { text: failed.size === 0 ? text : `${text} Could not close ${failed.size}; run /park close again.` }
119}
120
121// The newest note in this project's .park folder, no older than `days`.
122async function newestNote($: EngineInterface, days: number): Promise<string | undefined> {
123 const cwd = await $.session.cwd()
124 try {
125 const ran = await $.process.run([
126 'find', `${cwd}/${NOTE_DIR}`, '-maxdepth', '1', '-name', 'park-*.md', '-mtime', `-${days}`, '-exec', 'ls', '-t', '{}', '+',
127 ])
128
129 return ran.exitCode === 0 ? ran.stdout.split('\n')[0]?.split('/').pop() || undefined : undefined
130 } catch {
131 // no `find` on this machine (Windows): there is simply no note to offer
132 return undefined
133 }
134}
135
136export const register: Register = on => {
137 on('session.start', async ($, e, next) => {
138 if (e.isInteractive) {
139 await $.command.register({
140 name: 'park',
141 description: 'Save this tab and write a note for the next session. In herdr, also saves the other agent tabs; /park close closes them',
142 immediate: true,
143 })
144 await $.command.register({
145 name: 'unpark',
146 description: `Pick up the newest next-session note from ${NOTE_DIR}`,
147 immediate: true,
148 })
149 const note = await newestNote($, FRESH_DAYS)
150 if (note !== undefined) {
151 $.ui.toast(`Parked note: ${note}. Type /unpark to pick it up.`, { timeoutMs: 15_000 })
152 }
153 }
154
155 return next(e)
156 })
157
158 on('command.run', { command: 'park' }, async ($, e) => {
159 if (e.args.trim() === 'close') return close($)
160 if (isParking) return { text: `Already parking:\n${lines(tabs.values())}` }
161
162 clearStatus?.cancel()
163 isParking = true
164 let agents: Agent[]
165 try {
166 agents = (await others($)) ?? []
167 } catch (err) {
168 isParking = false
169 throw err
170 }
171 const first = (a: Agent): Outcome =>
172 a.agent_status === 'working' ? 'busy' : a.agent_status === 'blocked' ? 'blocked' : 'saving'
173 tabs = new Map(agents.map(a => [a.pane_id, { agent: a, outcome: first(a) }]))
174 show($)
175
176 const sent = [...tabs.values()].filter(t => t.outcome === 'saving')
177 void Promise.all(sent.map(t => save($, t))).then(() => {
178 isParking = false
179 later($, parkPrompt(lines(tabs.values())))
180 // the line has done its job once the note is going out; /park close still works from `tabs`
181 clearStatus = $.clock.after(60_000, () => $.ui.status(undefined))
182 })
183
184 return {
185 text:
186 agents.length === 0
187 ? 'No other agent tabs. Saving this tab and writing the next-session note.'
188 : `Parking ${agents.length} tab${agents.length === 1 ? '' : 's'}. This tab saves once they settle.\n${lines(tabs.values())}`,
189 }
190 })
191
192 on('command.run', { command: 'unpark' }, async $ => {
193 const note = await newestNote($, 3650)
194 if (note === undefined) return { text: `No park note in ${NOTE_DIR}.` }
195 later($, `Read ${NOTE_DIR}/${note} and follow it.`)
196
197 return { text: `Picking up ${note}.` }
198 })
199}
200