Find your messages. Never miss what Claude is waiting on you for. Borders your own messages and outlines any part of Claude's reply that needs you to act…

Find your messages. Never miss what Claude is waiting on you for.

Terminal only. Your Move works in Claude Code in the terminal. It doesn't work in the Claude desktop app or on claude.ai.
A Claude Code plugin for long sessions. When you scroll back, everything Claude says looks the same, and the one line where it asked you to decide something is easy to miss. Your Move fixes two things:
Nothing else in the transcript changes. Background-task notices and messages from other agents keep their normal look, so the border only ever means "you said this", and the outline only ever means "this is on you".
From your shell:
claude plugin marketplace add davidtheproduct/your-move
claude plugin install your-move@davidtheproduct
Or from inside a Claude Code session:
/plugin install your-move --marketplace davidtheproduct/your-move
New sessions pick it up. It needs a recent Claude Code, since it is built with Claude Code's mods API (function hooks), which is in early access.
Open /config and look for the two Your Move rows:
| Setting | What it colours | Default |
|---|---|---|
| Your messages: border colour | The border around messages you type | #5b0a91 (deep violet) |
| Your move: outline colour | The outline around what needs you | #ff2020 (red) |
Use a hex colour such as #00aa88. Anything else falls back to the default.
Your move (bold or not) and covers that paragraph plus any list directly under it. A blank line followed by ordinary text ends it. The lead is always drawn in bold.claude -p), which draw no transcript.Only the drawing changes. Your stored conversation, and what Claude reads back from it, are untouched.
claude --plugin-dir . # run Claude Code with your working copy
claude plugin test . # run the tests
claude plugin validate --strict .claude-plugin/plugin.json
MIT, by davidtheproduct.
hooks/register.tsx 85 lines1import type { Register } from 'claude-code'
2
3import { split } from './actions'
4
5const DEFAULTS = { yourMessage: '#5b0a91', yourMove: '#ff2020' }
6
7// A colour the person set, if it is a hex colour; otherwise the default.
8const colour = (value: unknown, fallback: string) =>
9 typeof value === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim()) ? value.trim() : fallback
10
11// Rows that are the person typing (here, from the phone bridge, or an older
12// stored prompt with no origin stamp). Task notifications and other agents'
13// messages keep the normal look, so the border only ever means "I said this".
14const MINE = new Set(['composer', 'bridge', 'unclassified'])
15
16// Without this, Claude has no reason to write the marker the outline keys on.
17const MARKER = {
18 id: 'your-move:marker',
19 scope: 'session',
20 text: [
21 '# Flagging what needs the user',
22 'When part of a reply needs the user to act, decide, or answer something you are waiting on,',
23 'put it in its own section whose first line starts with `**Your move:**`, with any options as a',
24 'list directly beneath it. The user\'s interface outlines that section so it cannot be missed.',
25 'Use at most one such section per reply, near the end, and only when something truly needs',
26 'the user. Keep findings, reasoning and narration outside it.',
27 ].join('\n'),
28} as const
29
30export const register: Register = (on, options) => {
31 const yourMessage = colour(options.your_message_color, DEFAULTS.yourMessage)
32 const yourMove = colour(options.your_move_color, DEFAULTS.yourMove)
33
34 on('prompt.compose', async ($, e, next) => {
35 const result = await next(e)
36
37 // Headless runs draw no transcript, so the instruction would be wasted.
38 if (e.surfaces.length === 0) {
39 return result
40 }
41
42 return { ...result, sections: [...result.sections, MARKER] }
43 })
44
45 on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
46 const drawn = await next(e)
47 const isMine = MINE.has(e.props.origin.kind) && !e.props.task && !e.props.from
48
49 if (!isMine) {
50 return drawn
51 }
52
53 const { Box } = $.ui.resolve(e)
54
55 return (
56 <Box borderStyle="bold" borderColor={yourMessage} paddingX={1} marginTop={1} flexDirection="column">
57 {drawn}
58 </Box>
59 )
60 })
61
62 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
63 const parts = split(e.props.text)
64
65 if (!parts.some(p => p.isAction)) {
66 return next(e)
67 }
68
69 const { Box, Markdown } = $.ui.resolve(e)
70 const drawn = await Promise.all(
71 parts.map((p, i) =>
72 p.isAction
73 ? (
74 <Box borderStyle="bold" borderColor={yourMove} paddingX={1} marginY={1} flexDirection="column">
75 <Markdown text={p.text} />
76 </Box>
77 )
78 : next({ ...e, props: { ...e.props, text: p.text, isFirstOfReply: e.props.isFirstOfReply && i === 0 } }),
79 ),
80 )
81
82 return <Box flexDirection="column">{drawn}</Box>
83 })
84}
85hooks/actions.ts 42 lines1// Splits an assistant reply into plain and action parts. An action part starts
2// at a line opening with "Your move" (bold or not) or "needs input:" and runs
3// to the first blank line followed by something other than a list item or an
4// indented continuation, so a lead line plus its bullets stay together.
5export type Part = { text: string; isAction: boolean }
6
7const START = /^\s*(\*\*)?(your move\b|needs input:)/i
8const CONTINUES = /^(\s+\S|\s*([-*+]|\d+[.)])\s)/
9const PLAIN_LEAD = /^(\s*)your move\s*:?/i
10
11export const split = (text: string): Part[] => {
12 const lines = text.split('\n')
13 const parts: Part[] = []
14 let buf: string[] = []
15 let inAction = false
16
17 const flush = () => {
18 const joined = buf.join('\n').replace(/^\n+|\n+$/g, '')
19 if (joined) parts.push({ text: joined, isAction: inAction })
20 buf = []
21 }
22
23 lines.forEach((line, i) => {
24 if (!inAction && START.test(line)) {
25 flush()
26 inAction = true
27 // Always draw the lead in bold, even when Claude wrote it plain.
28 line = line.replace(PLAIN_LEAD, '$1**Your move:**')
29 } else if (inAction && line.trim() === '') {
30 const nextLine = lines.slice(i + 1).find(l => l.trim() !== '')
31 if (nextLine === undefined || !CONTINUES.test(nextLine) || START.test(nextLine)) {
32 flush()
33 inAction = false
34 }
35 }
36 buf.push(line)
37 })
38 flush()
39
40 return parts
41}
42