Ask Claude a side question in a pane next to your work. The main conversation never sees it. Works like the desktop app's /btw.

Ask Claude a side question without adding it to the main conversation.
Halfway through a task you want to know what a flag does, or why Claude chose one approach over another. Ask in the main prompt and the question and its answer become part of the conversation Claude works from. side-chat opens a pane next to your work instead. The answer comes from a fork of your session, so it knows what you have been doing, and nothing you ask there reaches the main thread.
The same pane takes /btw @name your question, which sends the question to one of your agents and shows its answer when it finishes.
It works like the side chat in the Claude desktop app. This is an independent mod, not made by Anthropic.

The pane in a VS Code terminal on macOS. Your question sits above the divider, Claude's answer below it, and the input at the bottom takes the next question.
Read this before installing: while side-chat is enabled, /btw opens this pane instead of the built-in one. Disable the plugin to get the built-in /btw back.
/btw and /side both open the pane. Type in it, or ask from the main prompt with /btw your question or /side your question./btw @nahida your question starts one of your agents in the background and shows its final answer in the pane. Each call starts a fresh agent. /model haiku|sonnet|opus|default picks the model those agents use./clear to wipe it and /close to hide it. Esc goes back to the main prompt.Claude Code with plugin function hooks (mods). Tested on 2.1.289, where the mod loads with no extra setting: I ran it from a clean checkout with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS unset and /side clear answered. Builds around 2.1.270 needed CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 before any mod would load, so if /btw does nothing on an older build, set that and restart. Function hooks are early access and the API changes between releases.
/plugin marketplace add varunmoka7/side-chat
/plugin install side-chat@side-chat
Open /config and look for side-chat: your text color, Claude's text color, your own instructions for the side chat, whether the pane takes the keyboard when it opens, and the default model for agent calls.
This was built and tested inside one Claude Code setup (VS Code terminal, macOS). The unit tests pass, but some parts have not been seen working live:
@agent calls arriving in the pane, and the model switch./clear and /close.An agent started with @name runs with that agent's own tools and your session's permission mode. It can edit files if you ask it to.
JayDoubleu/aside is the same idea, a read-only side chat answered by a fork of the session, with its own /aside command and a cost footer on each answer. side-chat adds the /btw takeover, @agent calls, running-agent context, a model switch and color settings.
claude plugin test .
MIT license.
hooks/register.tsx 166 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { SideTurn } from '../types'
5
6const turns = atom({ plugin: 'side-chat', key: 'turns' } as const, [] as SideTurn[])
7const pending = atom({ plugin: 'side-chat', key: 'pending' } as const, '')
8const model = atom({ plugin: 'side-chat', key: 'model' } as const, '')
9const PANE = 'side-chat'
10const MODELS = ['default', 'haiku', 'sonnet', 'opus']
11
12// Set from the instructions setting when the mod loads.
13let custom = ''
14let takeKeyboard = true
15let defaultModel = 'default'
16
17// Side turns are replayed inside each prompt because fork takes one message.
18const buildPrompt = (history: SideTurn[], q: string, agents: string[]) =>
19 [
20 ...(custom ? [`The person's own instructions for this side chat: ${custom}`] : []),
21 'The person asked the question at the end of this message in a side panel. Answer that question directly, in at most 6 short lines of plain markdown, using the conversation above. Never describe this message or these instructions. Read typos charitably. Use no tools.',
22 ...(agents.length ? ['Agents running in this session right now:', ...agents] : []),
23 ...history.flatMap(t => [`Earlier question: ${t.q}`, `Earlier answer: ${t.a}`]),
24 `Question: ${q}`,
25 ].join('\n')
26
27// A started agent's answer arrives later as its turn.complete; `early` holds one that beat the wait.
28const waiting = new Map<string, (answer: string) => void>()
29const early = new Map<string, string>()
30const answerOf = (id: string) =>
31 early.has(id) ? Promise.resolve(early.get(id) as string) : new Promise<string>(done => waiting.set(id, done))
32
33async function finish($: EngineInterface, q: string, a: string) {
34 await update($, turns, list => [...list, { q, a }].slice(-50))
35 await update($, pending, () => '')
36 await $.ui.scroll({ in: PANE, to: 'end' }).catch(() => ({}))
37}
38
39async function askAgent($: EngineInterface, name: string, q: string) {
40 const label = `@${name} ${q}`
41 await update($, pending, () => label)
42 const chosen = (await read($, model)) || defaultModel
43 const started = await $.agent.spawn({
44 subagentType: name,
45 ...(chosen !== 'default' ? { model: chosen } : {}),
46 description: 'side chat question',
47 prompt: `A side-chat question, answer in at most 8 short lines of plain text: ${q}`,
48 })
49 if (started.deny !== undefined || !started.agentId) {
50 return finish($, label, `Could not start ${name}: ${started.deny ?? 'no agent id'}.`)
51 }
52 return finish($, label, (await answerOf(started.agentId)) || `${name} gave no answer.`)
53}
54
55async function ask($: EngineInterface, q: string) {
56 const word = q.trim().replace(/^\//, '')
57 const done = await control($, word)
58 if (done) return word.startsWith('model') ? finish($, q.trim(), done) : undefined
59 if (!q.trim() || (await read($, pending))) return
60 const agent = /^@([\w-]+)\s+(.+)$/s.exec(q.trim())
61 if (agent) return askAgent($, agent[1], agent[2])
62 const history = await read($, turns)
63 const running = (await $.agent.list())
64 .filter(x => x.status === 'running' || x.status === 'pending' || x.status === 'waiting')
65 .map(x => `- ${x.type} (${x.status}): ${x.description}`)
66 await update($, pending, () => q)
67 const r = await $.model.fork({ prompt: buildPrompt(history, q, running) })
68 return finish($, q, r.isAnswered ? r.text : `No reply (${r.reason}).`)
69}
70
71// "clear" and "close" work from /side, /btw and the pane's own input, none of which need the mouse.
72async function control($: EngineInterface, word: string) {
73 const pick = /^model(?:\s+(\S+))?$/.exec(word)
74 if (pick) {
75 const wanted = pick[1]
76 if (!wanted) return `Agent model: ${(await read($, model)) || defaultModel}. Change it with /model ${MODELS.join(' | ')}.`
77 if (!MODELS.includes(wanted)) return `Unknown model "${wanted}". Use ${MODELS.join(', ')}.`
78 await update($, model, () => wanted)
79 return `Agent model set to ${wanted}.`
80 }
81 if (word === 'clear') {
82 await update($, turns, () => [])
83 await update($, pending, () => '')
84 return 'Side chat cleared.'
85 }
86 if (word === 'close') {
87 await $.ui.close({ id: PANE })
88 return 'Side chat closed. /btw reopens it.'
89 }
90}
91
92async function run($: EngineInterface, e: { args: string }) {
93 const arg = e.args.trim()
94 const done = await control($, arg)
95 if (done) return { text: done }
96 await $.ui.open({ id: PANE, title: 'Side chat', ...(takeKeyboard ? { focus: true as const } : {}) })
97 if (arg) void ask($, arg)
98 return { text: `Side chat open. Type in the pane, or /btw <question>, or /btw @nahida <question> to call an agent. Esc returns to the main prompt. /side clear wipes it, /side close hides it.` }
99}
100
101export const register: Register = (on, options) => {
102 const you = String(options.yourColor)
103 const claude = String(options.claudeColor)
104 custom = String(options.instructions ?? '').trim()
105 takeKeyboard = options.takeKeyboard !== false
106 defaultModel = String(options.agentModel ?? 'default')
107
108 // Hand a started side-chat agent its answer; every hook sees a subagent's turn, so pass the event on.
109 on('turn.complete', async ($, e, next) => {
110 if (e.agentId && e.reason === 'answer') {
111 const done = waiting.get(e.agentId)
112 if (done) {
113 waiting.delete(e.agentId)
114 done(e.answer)
115 } else early.set(e.agentId, e.answer)
116 }
117 return next(e)
118 })
119
120 on('session.start', async ($, e, next) => {
121 await $.command.register({
122 name: 'side',
123 description: 'Side chat: ask follow-ups without touching the main conversation',
124 })
125 return next(e)
126 })
127
128 // btw is a built-in; command.run fires for it too, and answering without next replaces it.
129 for (const command of ['side', 'btw']) on('command.run', { command }, run)
130
131 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
132 const { Box, Text, Input, Button } = $.ui.resolve(e)
133 const list = await read($, turns)
134 const busy = await read($, pending)
135 const agentModel = (await read($, model)) || defaultModel
136 return (
137 <Box flexDirection="column">
138 <Box position="absolute" top={0} right={0} gap={1}>
139 <Button key="clear" label="Clear" onPress={() => update($, turns, () => [])} />
140 <Button key="close" label="Close" onPress={() => $.ui.close({ id: PANE })} />
141 </Box>
142 <Text dimColor>Esc: main prompt · Tab then Enter presses Clear or Close · or type /clear /close · agent model: {agentModel} (/model haiku|sonnet|opus|default)</Text>
143 {!list.length && !busy && <Text dimColor>Ask anything about this session.</Text>}
144 {list.map(t => (
145 <Box flexDirection="column">
146 <Text bold color={you}>{'You ' + t.q}</Text>
147 <Text color="gray" wrap="truncate-end">{'─'.repeat(200)}</Text>
148 <Text color={claude}>{'Claude ' + t.a.slice(0, 1200)}</Text>
149 <Text color="gray" wrap="truncate-end">{'─'.repeat(200)}</Text>
150 </Box>
151 ))}
152 {busy && (
153 <Box flexDirection="column">
154 <Text bold color={you}>{'You ' + busy}</Text>
155 <Text color="gray" wrap="truncate-end">{'─'.repeat(200)}</Text>
156 <Text dimColor>Thinking...</Text>
157 </Box>
158 )}
159 <Box borderStyle="round" borderColor="gray" paddingX={1}>
160 <Input key="side-input" placeholder="Follow up..." submitLabel="ask" autoFocus={takeKeyboard ? true : undefined} onSubmit={v => void ask($, v)} />
161 </Box>
162 </Box>
163 )
164 })
165}
166types/index.d.ts 8 lines1export type SideTurn = { q: string; a: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'side-chat': { turns: SideTurn[]; pending: string; model: string }
6 }
7}
8