SLOPSHOPPER

agent-shell

A pane that replays every Bash command any agent runs, as a terminal session with the command and its output.

panerowsguardcommandprompt
v0.1.3no licenseupdated 2026-10-17poldap/agent-shell
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-shell
│ ┃ Agent shell ✕ › fix the failing auth test and add an audit log call │ ┃ ╭──────────────────────────────────────────╮ │ ┃ │ ▸ Run the test suite main · failed │ ⏺ Read(src/auth.ts) │ ┃ ╰──────────────────────────────────────────╯ ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ ╭──────────────────────────────────────────╮ ⎿ Added 2 lines, removed 1 line │ ┃ │ ▸ Show working tree status main │ ⏺ Bash(bun test) │ ┃ ╰──────────────────────────────────────────╯ ⎿ 3 pass, 1 fail │ ┃ │ ┃ ╭──────────────────────────────────────────╮ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ▸ Clean build output and push main │ │ ┃ ╰──────────────────────────────────────────╯ ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ ╭──────────────────────────────────────────╮ › /agent-shell │ ┃ │ ▸ Show environment file main │ ⎿ agent-shell: Agent shell pane opened. │ ┃ ╰──────────────────────────────────────────╯ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agent shell
╭──────────────────────────────────────────────────────────╮ │ ▸ Run the test suite main · failed │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │ ▸ Show working tree status main │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │ ▸ Clean build output and push main │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │ ▸ Show environment file main │ ╰──────────────────────────────────────────────────────────╯
README

agent-shell

A Claude Code mod: a pane that shows every Bash command any agent runs (the main agent and subagents) as a terminal box. Each box has a short title, and opens to the command after a $ prompt with its full output below. Run /agent-shell to open the pane. The mod also asks the model to give every Bash call a very short description, which becomes the box title.

Install

/plugin install agent-shell --marketplace poldap/agent-shell

Answer y to add the marketplace, then pick a scope.

Source 2 files
hooks/register.tsx 233 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { ShellEntry } from '../types'
5
6const PANE = 'agent-shell'
7const entries = atom({ plugin: 'agent-shell', key: 'entries' } as const, [])
8
9const MAX_ENTRIES = 500
10
11// A title when the call gave no description: the first real command, minus leading VAR=value prefixes.
12const ASSIGNMENT = /^[A-Za-z_]\w*=("[^"]*"|'[^']*'|\S*)\s*(;|&&)?\s*/
13const titleOf = (command: string, description?: string) => {
14  const given = description?.trim()
15  if (given) return given
16  for (const line of command.split('\n')) {
17    let rest = line.trim()
18    while (ASSIGNMENT.test(rest)) rest = rest.replace(ASSIGNMENT, '')
19    if (rest !== '' && !rest.startsWith('#') && !/^cd\s/.test(rest)) return rest.slice(0, 60)
20  }
21  return command.trim().slice(0, 60)
22}
23
24// Entries saved before `isDescribed` existed: a title that is a piece of the command was derived, not given.
25const shownTitle = (one: ShellEntry) =>
26  (one.isDescribed ?? (one.title !== undefined && !one.command.includes(one.title)))
27    ? one.title
28    : titleOf(one.command)
29
30type Token = { text: string; color?: string; dim?: boolean }
31
32const COLORS = {
33  command: '#61afef',
34  flag: '#d19a66',
35  string: '#98c379',
36  variable: '#c678dd',
37  operator: '#56b6c2',
38  number: '#e5c07b',
39}
40
41// A small bash highlighter: commands, flags, strings, variables, operators, comments.
42const highlight = (source: string): Token[] => {
43  const tokens: Token[] = []
44  const re =
45    /(#.*)|('[^']*'|"(?:[^"\\]|\\.)*")|(\$\{[^}]*\}|\$\(|\$\w+|\$[?!@*#])|(\|\||&&|[|;&<>]+)|(\s+)|([^\s|;&<>'"$#]+)|[\s\S]/g
46  let isCommandNext = true
47  for (const m of source.matchAll(re)) {
48    const [text, comment, str, variable, operator, space, word] = m
49    if (comment) tokens.push({ text, dim: true })
50    else if (str) tokens.push({ text, color: COLORS.string })
51    else if (variable) tokens.push({ text, color: COLORS.variable })
52    else if (operator) {
53      tokens.push({ text, color: COLORS.operator })
54      isCommandNext = true
55    } else if (space) {
56      tokens.push({ text })
57      if (space.includes('\n')) isCommandNext = true
58    } else if (word) {
59      if (isCommandNext && !/^\w+=/.test(word)) {
60        tokens.push({ text, color: COLORS.command })
61        isCommandNext = false
62      } else if (word.startsWith('-') && word.length > 1) tokens.push({ text, color: COLORS.flag })
63      else if (/^\d+$/.test(word)) tokens.push({ text, color: COLORS.number })
64      else tokens.push({ text })
65    } else tokens.push({ text })
66  }
67  return tokens
68}
69
70export const register: Register = on => {
71  on('session.start', async ($, e, next) => {
72    await $.command.register({
73      name: 'agent-shell',
74      description: 'Show the shell commands every agent runs, as terminal boxes',
75    })
76    void $.ui.open({ id: PANE, title: 'Agent shell' })
77
78    return next(e)
79  })
80
81  on('command.run', { command: 'agent-shell' }, async $ => {
82    await $.ui.open({ id: PANE, title: 'Agent shell' })
83
84    return { text: 'Agent shell pane opened.' }
85  })
86
87  // Every model call gets the instruction, subagents' included: the box title is the description.
88  on('prompt.compose', async ($, e, next) => {
89    const { sections } = await next(e)
90
91    return {
92      sections: [
93        ...sections,
94        {
95          id: 'agent-shell:titles',
96          scope: 'session' as const,
97          text: 'Every Bash call must set the `description` parameter to a very short title (2-6 words, imperative, e.g. "List dragon folder", "Run host tests") saying what the command does. It is shown as the heading of the command in the user\'s Agent shell pane. Never leave it out, however simple the command.',
98        },
99      ],
100    }
101  })
102
103  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
104    let agent = 'main'
105    if (e.agentId) {
106      const found = (await $.agent.list()).find(a => a.id === e.agentId)
107      agent = found?.name ?? found?.type ?? e.agentId.slice(0, 8)
108    }
109    const entry: ShellEntry = {
110      id: e.tool_use_id,
111      agent,
112      title: titleOf(e.command, e.description),
113      isDescribed: Boolean(e.description?.trim()),
114      command: e.command,
115      output: '',
116      isDone: false,
117      isError: false,
118      isOpen: false,
119    }
120    await update($, entries, list => [...list, entry].slice(-MAX_ENTRIES))
121
122    const ran = await next(e)
123
124    const output =
125      ran.deny !== undefined
126        ? `denied: ${ran.deny}`
127        : ran.result && typeof ran.result === 'object' && 'stdout' in ran.result
128          ? [ran.result.stdout, ran.result.stderr].filter(Boolean).join('\n')
129          : (ran.text ?? '')
130    await update($, entries, list =>
131      list.map(one =>
132        one.id === entry.id
133          ? {
134              ...one,
135              output: output.replace(/\s+$/, ''),
136              isDone: true,
137              isError: ran.deny !== undefined || ran.isError === true,
138            }
139          : one,
140      ),
141    )
142
143    return ran
144  }).catch(($, e, next) => next(e))
145
146  // Under each Bash row in the conversation: a button that opens that command's box.
147  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
148    const row = await next(e)
149    if (e.tool !== 'Bash') return row
150    const { Box, Button } = $.ui.resolve(e)
151
152    return (
153      <Box flexDirection="column">
154        {row}
155        <Button
156          key={`open-${e.tool_use_id}`}
157          dimColor
158          onPress={async () => {
159            await update($, entries, list =>
160              list.map(one => (one.id === e.tool_use_id ? { ...one, isOpen: true } : one)),
161            )
162            await $.ui.open({ id: PANE, title: 'Agent shell' })
163          }}
164        >
165          ▸ open in Agent shell
166        </Button>
167      </Box>
168    )
169  }).catch(($, e, next) => next(e))
170
171  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
172    const { Box, Text, Button, Code } = $.ui.resolve(e)
173    const list = await read($, entries)
174
175    const toggle = (id: string) =>
176      update($, entries, all =>
177        all.map(one => (one.id === id ? { ...one, isOpen: !one.isOpen } : one)),
178      )
179
180    return (
181      <Box flexDirection="column" gap={1}>
182        {list.length === 0 && <Text dimColor>No shell commands yet.</Text>}
183        {list.map(one => (
184          <Box
185            key={`box-${one.id}`}
186            flexDirection="column"
187            borderStyle="round"
188            borderColor={one.isError ? 'red' : undefined}
189            paddingX={1}
190          >
191            <Box flexDirection="row" gap={1}>
192              <Button
193                key={`toggle-${one.id}`}
194                plain
195                label={`${one.isOpen ? '▾' : '▸'} ${shownTitle(one)}`}
196                onPress={() => toggle(one.id)}
197              />
198              <Text dimColor>
199                {one.agent}
200                {one.isDone ? (one.isError ? ' · failed' : '') : ' · running…'}
201              </Text>
202            </Box>
203            {one.isOpen && (
204              <Box backgroundColor="#111111" paddingX={1} marginTop={1} flexDirection="column">
205                <Box flexDirection="row" justifyContent="space-between" gap={2}>
206                  <Text>
207                    <Text color="green" bold>
208                      ${' '}
209                    </Text>
210                    {highlight(one.command).map(t => (
211                      <Text color={t.color} dimColor={t.dim}>
212                        {t.text}
213                      </Text>
214                    ))}
215                  </Text>
216                  <Button
217                    key={`copy-${one.id}`}
218                    plain
219                    dimColor
220                    label="copy"
221                    onPress={press => $.ui.copy({ text: one.command, surface: press.surface })}
222                  />
223                </Box>
224                {one.output !== '' && <Code source={one.output} />}
225              </Box>
226            )}
227          </Box>
228        ))}
229      </Box>
230    )
231  })
232}
233
types/index.d.ts 18 lines
1export type ShellEntry = {
2  id: string
3  agent: string
4  title: string
5  command: string
6  output: string
7  isDone: boolean
8  isError: boolean
9  isOpen: boolean
10  isDescribed?: boolean
11}
12
13declare module 'claude-code' {
14  interface PluginState {
15    'agent-shell': { entries: ShellEntry[] }
16  }
17}
18