Keeps three lines in a band above the prompt: what Claude is doing right now, for what higher goal, and what it needs from you.

A Claude Code mod that keeps three lines in view above your prompt, so you always know where a session stands without scrolling or asking:
what am i doing right now: running the test suite after the rename.
for what higher goal: the search page shipped and checked on staging.
what is needed from you: nothing.
During long work it is easy to lose track of what Claude is doing, why, and whether it is waiting on you. This mod makes Claude end every reply with those three answers and pins the latest ones above the prompt, each line in its own coloured box. The lines are hidden from the transcript, so they are not repeated under every reply.
In a Claude Code terminal session, type:
/plugin install what-what-what --marketplace nickohold/what-what-what
Answer y to add the marketplace, then pick a scope (the user scope turns it on for every session). The mod is active from that moment; the band appears after Claude's next reply.
Works in Claude Code in the terminal and in the desktop app's Code tab.
CLAUDE.md. It rides with each prompt because a session that is already deep into a conversation ignores the same instruction when it only sits in the system prompt.claude -p and SDK sessions have no band, so they are not asked to add the lines to their output. Subagent replies are ignored.claude plugin validate .
claude plugin test .
The three labels live in one place, KEYS in hooks/footer.ts. The instruction Claude receives and the parser that reads its replies are both built from it.
MIT
hooks/register.tsx 79 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { FOOTER_INSTRUCTION, KEYS, parseFooter, stripFooter } from './footer'
5
6const footer = atom({ plugin: 'what-what-what', key: 'footer' } as const, null)
7
8export const register: Register = on => {
9 // Without this the band depends on each session being told separately to
10 // write the three lines. The instruction rides beside every prompt, unseen by
11 // the person, and not in the system prompt: in a long conversation whose
12 // earlier replies have no footer, Claude keeps to that pattern and passes over
13 // a system prompt section (seen 2026-10-06 on a session that loaded the mod
14 // mid-conversation), while an instruction next to the prompt is followed.
15 // Only where a band can be drawn: a session with no surface (`claude -p`, the
16 // SDK) is not asked to end its output with lines nobody would see.
17 on('prompt.submit', async ($, e, next) => {
18 const surfaces = await $.session.surfaces()
19
20 if (surfaces.length === 0) {
21 return next(e)
22 }
23
24 return next({ ...e, context: [...(e.context ?? []), FOOTER_INSTRUCTION] })
25 })
26
27 // Claude ends each message with the three lines; the band keeps the latest
28 // ones in view while the transcript scrolls on.
29 on('turn.complete', async ($, e, next) => {
30 // a subagent's turn is not Claude talking to the person
31 const parsed = e.agentId ? null : parseFooter(e.answer)
32
33 if (parsed) {
34 await update($, footer, () => parsed)
35 }
36
37 return next(e)
38 })
39
40 // The band carries the three lines, so the transcript draws the reply
41 // without them. The stored message keeps them (ctrl+o).
42 on('ui.render', { component: 'AssistantMessage' }, ($, e, next) =>
43 next({ ...e, props: { ...e.props, text: stripFooter(e.props.text) } }),
44 )
45
46 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
47 const lines = await read($, footer)
48
49 if (lines === null || e.props.hasSurvey) {
50 return next(e)
51 }
52
53 const { Box, Text } = $.ui.resolve(e)
54
55 // Each line in its own box, the border in its label's colour. A border
56 // takes a whole row, so stacked boxes would show two border rows between
57 // lines; each box after the first rises one row and draws its top border
58 // over the bottom border of the box above.
59 return (
60 <Box flexDirection="column">
61 {BOXES.map(({ key, color }, i) => (
62 <Box borderStyle="round" borderColor={color} paddingX={1} marginTop={i === 0 ? 0 : -1}>
63 <Text>
64 <Text bold color={color}>{KEYS[key]}:</Text> {lines[key]}
65 </Text>
66 </Box>
67 ))}
68 </Box>
69 )
70 })
71}
72
73/** The band's boxes in the order shown: which footer line each holds, in what colour. */
74const BOXES = [
75 { key: 'doing', color: 'claude' },
76 { key: 'goal', color: 'suggestion' },
77 { key: 'needed', color: 'warning' },
78] as const satisfies readonly { key: keyof typeof KEYS; color: string }[]
79hooks/footer.ts 89 lines1import type { Footer } from '../types'
2
3/** The footer's three keys, as Claude writes them, in the order they are shown. */
4export const KEYS = {
5 doing: 'what am i doing right now',
6 goal: 'for what higher goal',
7 needed: 'what is needed from you',
8} as const
9
10/**
11 * What tells Claude to write the footer, sent beside every prompt of a session
12 * the mod draws in: the band shows only what Claude writes, so a session that
13 * is never told shows nothing. The labels come from KEYS, the same ones
14 * parseFooter matches, so what is asked for is what is read.
15 */
16export const FOOTER_INSTRUCTION = [
17 'Standing instruction from the what-what-what mod, sent with every prompt and not written by the person. It applies to this reply even if your earlier replies in this conversation did not follow it.',
18 'End every message you write to the person with these three lines, as the very last lines of the message, each on its own line:',
19 '',
20 `${KEYS.doing}: <the step in progress right now, or "nothing" when you are idle or waiting>`,
21 `${KEYS.goal}: <the outcome the person is after; this line changes far less often than the first>`,
22 `${KEYS.needed}: <the one thing you need from the person, or "nothing">`,
23 '',
24 'Write the three labels exactly as shown: lowercase, word for word, never shortened, reworded or capitalised. Fill each in with one short plain sentence. This applies to every message, a one-word answer included.',
25 'A band above the prompt shows these lines to the person and the transcript hides them; it finds them by matching the labels literally, so a reworded label shows nothing.',
26 'A subagent reporting to another agent, not to the person, leaves the three lines out.',
27].join('\n')
28
29/** The text after `key:` on the LAST line of `answer` that starts with it. */
30function lineAfter(answer: string, key: string): string | null {
31 for (const written of answer.split('\n').toReversed()) {
32 // tolerate list markers and bold around the key: "- **what am i …:** text"
33 const line = written.replace(/^[\s>*_-]+/, '')
34
35 if (!line.toLowerCase().startsWith(key)) {
36 continue
37 }
38
39 const colon = line.indexOf(':', key.length)
40
41 if (colon === -1) {
42 continue
43 }
44
45 const text = line.slice(colon + 1).replace(/^[\s*_]+|[\s*_]+$/g, '')
46
47 if (text) {
48 return text
49 }
50 }
51
52 return null
53}
54
55/**
56 * The footer at the end of an answer, or null when any of the three lines is
57 * missing: a message without the full footer leaves the last one standing.
58 */
59export function parseFooter(answer: string): Footer | null {
60 const doing = lineAfter(answer, KEYS.doing)
61 const goal = lineAfter(answer, KEYS.goal)
62 const needed = lineAfter(answer, KEYS.needed)
63
64 return doing && goal && needed ? { doing, goal, needed } : null
65}
66
67/** True when `line` is one of the footer's three lines, markdown around the key tolerated. */
68function isFooterLine(line: string): boolean {
69 const bare = line.replace(/^[\s>*_-]+/, '').toLowerCase()
70
71 return Object.values(KEYS).some(key => bare.startsWith(key))
72}
73
74/**
75 * `text` without the footer that closes it: the band shows those lines, so the
76 * transcript does not repeat them. Only the lines at the very end go; a footer
77 * quoted mid-message stays, and a block that is nothing but footer is kept.
78 */
79export function stripFooter(text: string): string {
80 const lines = text.split('\n')
81 // one past the last line that is neither blank nor footer; 0 when there is none
82 const end = lines.findLastIndex(line => line.trim() !== '' && !isFooterLine(line)) + 1
83
84 const kept = lines.slice(0, end).join('\n')
85 const hadFooter = lines.slice(end).some(isFooterLine)
86
87 return hadFooter && kept.trim() ? kept : text
88}
89types/index.d.ts 9 lines1/** The three lines Claude ends each message with. */
2export type Footer = { doing: string; goal: string; needed: string }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'what-what-what': { footer: Footer | null }
7 }
8}
9