SLOPSHOPPER

park

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…

newcommandtoaststatusprocesstimer
v0.1.2MITupdated 2026-10-13varunmoka7/park
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · park
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ park │ ⏺ Read(src/auth.ts) │ Parked note: package.json. Type /unpark to │ ⎿ Read 6 lines │ pick it up. │ ⏺ Update(src/auth.ts) ╰────────────────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /park ⎿ park: No other agent tabs. Saving this tab and writing the next-session note. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

park

/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.

Why you might want it

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.

Install

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.

How to use

  • /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.
  • At session start, a short message tells you if a note from the last 3 days exists.

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.

What it touches

  • Network: none.
  • Files read: the .park folder in your project, to find the newest note.
  • Files written: the note, .park/park-<topic>.md, written by Claude when you run /park. Nothing else.
  • Other programs: it runs the herdr binary only when you are inside herdr, to list tabs, send the save request and close tabs.
  • Model cost: one turn for each tab you park, plus one turn for this tab to write the note.
  • Off or on: nothing runs until you type /park. It acts only on command.

Turn it off / uninstall

claude plugin uninstall park@park

You can delete the .park folder in a project at any time.

Known limits

  • "Saved" means the tab took the save request and went quiet. The mod does not read the tab to confirm that it really saved.
  • The list of parked tabs is kept in memory. If you reload Claude Code after /park, run /park again before /park close.
  • Busy tabs are skipped, not interrupted. They are listed in the note.

License

MIT. Copyright (c) 2026 Varun Moka.

Source 1 files
hooks/register.ts 200 lines
1import 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