SLOPSHOPPER

agent-standup-band

A line above your Claude Code prompt when another Claude Code session on this computer needs you (an approval, an answer), and a toast when one finishes its…

newbandtoastprocesstimer
★ 323v1.41.1MITupdated 2026-10-21Atoll92/agent-standup/plugin-band
A shopper browsing a rack in a slop shop
README

Agent Standup band

Agent Standup

A line above your Claude Code prompt when another Claude Code session on this computer needs you:

✋ web-shop (VS Code) needs an approval Agent Standup · /agent-standup:open shows them all Hide

and a short toast when one starts waiting for you, or finishes its turn. You stay in the session you are working in, and still know when another one is stuck on a question or an approval.

It is a mod: a plugin of Claude Code's function hooks. It draws in the terminal and in the Claude desktop app's Code tab. In VS Code, the Agent Standup extension shows the same in its status bar.

Install

In Claude Code 2.1.294 or later:

/plugin install agent-standup-band --marketplace Atoll92/agent-standup

With an earlier version it loads and does nothing: those versions do not run mods (and before 2.1.221 in the terminal, 2.1.269 elsewhere, they do not write the status it reads either).

To see every session as a live team call, install the main plugin too: /plugin install agent-standup --marketplace Atoll92/agent-standup, then /agent-standup:open.

What it reads and runs

  • Every 4 seconds, the status file Claude Code keeps for each open session, in ~/.claude/sessions/<pid>.json (or under CLAUDE_CONFIG_DIR): which session, its folder, and whether it is busy, idle or waiting for you. Not the conversations. A session started by a program with claude -p is left out: its questions go to that program. An Agent SDK app (Zed, a home-made app) is kept, since it may show its menus to a person.
  • ps, to check that a session said to be waiting still runs: a session that crashed leaves its file behind.

It writes nothing and sends nothing anywhere.

More

Source and issues: https://github.com/Atoll92/agent-standup. Unofficial: not affiliated with or endorsed by Anthropic. MIT licensed.

Source 1 files
hooks/register.tsx 122 lines
1// Agent Standup's band for Claude Code (a mod: Claude Code's function hooks). In the session you are in, a line above the prompt says when another Claude Code session
2// on this computer needs you (an approval, an answer), and a toast says when one needs you or has finished its turn. It reads only the status file each session keeps
3// in <config dir>/sessions/<pid>.json (written by Claude Code 2.1.221 and later in the terminal, 2.1.269 elsewhere; mods run from 2.1.294) and asks `ps` whether a waiting session's process still runs. Nothing is sent anywhere.
4// The whole call, with every session as a character: /agent-standup:open (the agent-standup plugin).
5import type { EngineInterface, Register } from 'claude-code'
6
7// What the band shows, kept in the module and not in $.state: $.state needs a "types" contract in plugin.json, which Claude Code before 2.1.29x refuses in a manifest.
8// A reload of the module starts them over, and the next look (every 4 s) fills them again.
9type Need = { id: string; where: string; what: string }   // the session that waits; its folder, name and app; "an approval", "an answer"
10let needs: readonly Need[] = []
11let hidden = ''   // the waits you put away with "Hide", until another one comes
12
13type Status = { pid?: unknown; sessionId?: unknown; status?: unknown; waitingFor?: unknown; cwd?: unknown; name?: unknown; nameSource?: unknown; entrypoint?: unknown }
14
15// "web-shop · release (VS Code)": the folder, the name you gave the session (not the one Claude Code made up), the app it runs in
16export const whereOf = (s: Status): string => {
17  const cwd = typeof s.cwd === 'string' ? s.cwd : ''
18  const folder = cwd.split(/[\\/]/).filter(Boolean).pop() || 'A session'
19  const named = typeof s.name === 'string' && s.name && s.nameSource !== 'derived' ? ' · ' + s.name : ''
20  const app = s.entrypoint === 'claude-vscode' ? ' (VS Code)' : s.entrypoint === 'claude-desktop' ? ' (desktop app)' : ''
21  return folder + named + app
22}
23export const whatOf = (w: unknown): string => (w === 'permission prompt' ? 'an approval' : w === 'input needed' ? 'an answer' : 'you')
24// waiting, and not on a dialog the person opened there themselves (/model, /config…): those say "dialog open"
25const waits = (s: Status): boolean => s.status === 'waiting' && s.waitingFor !== 'dialog open'
26// a session a program started with claude -p (entrypoint sdk-cli): its questions go to that program, and its turns are not yours. An Agent SDK host (sdk-ts, sdk-py: Zed, a home-made app)
27// may show its menus to a person, so those are kept (as liveStatus in Agent Standup's server)
28const byProgram = (s: Status): boolean => s.entrypoint === 'sdk-cli'
29
30// The processes that still run, of those asked about. A session that crashed leaves its file behind, saying what it was doing then.
31const alive = async ($: EngineInterface, pids: readonly number[]): Promise<Set<number>> => {
32  if (!pids.length) return new Set()
33  try {
34    const { stdout } = await $.process.run(['ps', '-o', 'pid=', '-p', pids.join(',')], { timeoutMs: 5000 })
35    return new Set(stdout.split(/\s+/).filter(Boolean).map(Number))   // ps exits 1 when none of them runs: nothing is listed then
36  } catch {
37    return new Set(pids)   // no ps (Windows): every file is taken at its word
38  }
39}
40
41export const register: Register = on => {
42  on('session.start', async ($, e, next) => {
43    const started = await next(e)
44    const self = await $.session.id()
45    const config = (await $.env.get('CLAUDE_CONFIG_DIR')) || ((await $.env.get('HOME')) || (await $.env.get('USERPROFILE')) || '') + '/.claude'
46    const dir = config.replace(/[\\/]+$/, '') + '/sessions'
47    let before = new Map<string, string>()   // session id → what it was doing at the last look; nothing is announced at the first look
48    let first = true
49
50    const look = async () => {
51      let names: string[] = []
52      try {
53        names = (await $.fs.list(dir)).filter(f => f.kind === 'file' && /^\d+\.json$/.test(f.name)).map(f => f.name)
54      } catch {
55        names = []   // no such folder: a Claude Code that writes no status, or none has run yet
56      }
57      const rows: Status[] = []
58      for (const n of names) {
59        try {
60          const s = JSON.parse(await $.fs.read(dir + '/' + n)) as Status
61          if (s && typeof s.sessionId === 'string' && s.sessionId !== self && !byProgram(s)) rows.push(s)
62        } catch {
63          // a file being written, or not one of Claude Code's: skipped
64        }
65      }
66      const pids = rows.filter(waits).map(s => Number(s.pid)).filter(p => Number.isInteger(p) && p > 0)
67      const live = await alive($, pids)
68      const list: Need[] = rows.filter(s => waits(s) && live.has(Number(s.pid))).map(s => ({ id: String(s.sessionId), where: whereOf(s), what: whatOf(s.waitingFor) }))
69      if (JSON.stringify(needs) !== JSON.stringify(list)) {
70        needs = list
71        $.ui.invalidate('ui.render')
72      }
73
74      const now = new Map(rows.map(s => [String(s.sessionId), waits(s) ? 'waiting' : String(s.status)]))
75      if (!first) {
76        for (const s of rows) {
77          const was = before.get(String(s.sessionId)), is = now.get(String(s.sessionId))
78          if (is === 'waiting' && was !== 'waiting' && live.has(Number(s.pid))) $.ui.toast(`✋ ${whereOf(s)} needs ${whatOf(s.waitingFor)}`)
79          else if (is === 'idle' && was === 'busy') $.ui.toast(`✓ ${whereOf(s)} has finished: your turn there`)
80        }
81      }
82      before = now
83      first = false
84    }
85
86    await look().catch(() => undefined)
87    $.clock.every(4000, () => {
88      look().catch(() => undefined)
89    })
90    return started
91  })
92
93  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
94    const list = needs
95    const sig = list.map(n => n.id + ':' + n.what).join(',')
96    if (e.props.hasSurvey || !list.length || hidden === sig) return next(e)
97    const { Box, Button, Text } = $.ui.resolve(e)
98    return (
99      <Box flexDirection="column">
100        {list.slice(0, 3).map(n => (
101          <Text key={n.id} wrap="truncate-end">
102            ✋ {n.where} <Text dimColor>needs {n.what}</Text>
103          </Text>
104        ))}
105        {list.length > 3 ? <Text dimColor>and {list.length - 3} more</Text> : null}
106        <Box flexDirection="row" gap={1}>
107          <Text dimColor>Agent Standup · /agent-standup:open shows them all</Text>
108          <Button
109            key="hide"
110            label="Hide"
111            plain
112            onPress={() => {
113              hidden = sig
114              $.ui.invalidate('ui.render')
115            }}
116          />
117        </Box>
118      </Box>
119    )
120  })
121}
122