Pin one goal for the session: it sits above the prompt with a timer, and Claude is told to stay on it and flag unrelated work instead of drifting.

Pin one goal for the session. It sits above the prompt with a timer, and Claude is told to stay on it and flag unrelated work instead of drifting.
<img src="../../../assets/screens/hero.svg" alt="aim in a real Claude Code session" width="100%">
🎯 fix the login redirect bug · 12m [ Done ] [ Clear ]
╭──────────────────────────────────────────────────────────────╮
│ > also the footer looks off on mobile │
╰──────────────────────────────────────────────────────────────╯
● Fixed the redirect: the callback now keeps `?next=` through the
OAuth round-trip (auth/callback.ts:42).
Outside the current focus: the footer overlaps on narrow screens.
Want me to look at it after this?
Long sessions drift: one fix becomes a refactor, which becomes a dependency upgrade. aim gives the session one written goal that you can both see. It's pinned in the band above the prompt with the time spent so far, and it goes into Claude's system prompt, telling Claude to stay on the goal and mention side quests in one line instead of doing them.
/aim <goal> pins a goal. The band shows 🎯 <goal> · <elapsed> with Done and Clear buttons./aim done finishes the goal, shows a toast (Aim done in 34m 🎉) and records it. /aim history lists recent goals and how long each took./clear, /resume and restarts./aim alone shows the goal and its elapsed time. /aim clear drops it without recording it./plugin marketplace add Singh-AP/awesome-claude-mods
/plugin install aim@awesome-claude-mods
Requires Claude Code 2.1.287 or later.
| Command | What it does |
|---|---|
/aim <goal> | Pins the goal, replacing any current one |
/aim | Shows the goal and the time spent so far |
/aim done | Finishes it: toast, history entry, band cleared |
/aim clear | Drops it without recording it |
/aim history | The last 10 finished goals, newest first |
The command is /aim because /focus and /goal are built into Claude Code.
| Event | Why |
|---|---|
command.run on aim | Sets, finishes, clears and lists goals |
prompt.compose | Appends a session-scoped aim:goal section to the system prompt while a goal is set |
ui.render on AbovePrompt | Draws the band, followed by whatever await next(e) returns |
session.start, classic.SessionStart (clear/resume/fork) | Loads this project's goal from $.store into $.state |
$.clock.every | Redraws the band once a minute so the elapsed time stays current |
claude plugin test mods/productivity/aim # 11 tests
hooks/register.tsx 143 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AimDone, AimGoal } from '../types'
5import { clip, focusPrompt, formatDuration, HISTORY_LIMIT, parseFocus } from './format'
6
7const goal = atom({ plugin: 'aim', key: 'goal' } as const, null)
8
9const SECTION = 'aim:goal'
10const USAGE = 'Usage: /aim <goal> · /aim done · /aim clear · /aim history'
11
12async function goalKey($: EngineInterface): Promise<string> {
13 return `goal:${await $.session.root()}`
14}
15
16/** Copies this project's saved goal from $.store into $.state. */
17async function loadGoal($: EngineInterface): Promise<void> {
18 const saved = (await $.store.get(await goalKey($))) as AimGoal | undefined
19 await update($, goal, () => saved ?? null)
20}
21
22/** Writes the goal to $.state (which redraws the band) and keeps it in $.store. */
23async function saveGoal($: EngineInterface, next: AimGoal | null): Promise<void> {
24 await update($, goal, () => next)
25 const key = await goalKey($)
26 if (next === null) await $.store.delete(key)
27 else await $.store.set(key, next)
28}
29
30/** Ends the goal, records it, and says how long it took. */
31async function finishGoal($: EngineInterface): Promise<string> {
32 const current = await read($, goal)
33 if (current === null) return `No aim is set. ${USAGE}`
34 const now = await $.clock.now()
35 const took = formatDuration(now - current.startedAt)
36 const history = ((await $.store.get('history')) as AimDone[] | undefined) ?? []
37 const done: AimDone = { ...current, endedAt: now, root: await $.session.root() }
38 await $.store.set('history', [...history, done].slice(-HISTORY_LIMIT))
39 await saveGoal($, null)
40 $.ui.toast(`Aim done in ${took} 🎉`)
41 return `Done: "${current.text}" in ${took}.`
42}
43
44async function clearGoal($: EngineInterface): Promise<string> {
45 const current = await read($, goal)
46 await saveGoal($, null)
47 return current === null ? 'No aim was set.' : `Cleared the aim "${current.text}".`
48}
49
50async function listHistory($: EngineInterface): Promise<string> {
51 const list = ((await $.store.get('history')) as AimDone[] | undefined) ?? []
52 if (list.length === 0) return 'No finished goals yet. Finish one with /aim done.'
53 const lines = list
54 .slice(-10)
55 .reverse()
56 .map(one => {
57 const day = new Date(one.endedAt).toISOString().slice(0, 10)
58 return ` ${day} ${formatDuration(one.endedAt - one.startedAt).padStart(7)} ${clip(one.text, 70)}`
59 })
60 return `Recent goals (newest first):\n${lines.join('\n')}`
61}
62
63export const register: Register = on => {
64 on('session.start', async ($, e, next) => {
65 await loadGoal($)
66 try {
67 await $.command.register({
68 name: 'aim',
69 description: 'Pin one goal for this session (/aim <goal>), or: done, clear, history',
70 argumentHint: '<goal> | done | clear | history',
71 })
72 } catch (error) {
73 $.ui.log(`aim: could not add /aim: ${String(error)}`, { to: 'debug' })
74 }
75 // The band shows minutes, so a redraw a minute keeps it right.
76 if (e.isInteractive) $.clock.every(60_000, () => $.ui.invalidate('ui.render'))
77 return next(e)
78 })
79
80 // /clear, /resume and /branch reset $.state without a new session.start.
81 on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
82 await loadGoal($)
83 return next(e)
84 })
85
86 on('command.run', { command: 'aim' }, async ($, e) => {
87 const command = parseFocus(e.args)
88 switch (command.kind) {
89 case 'done':
90 return { text: await finishGoal($) }
91 case 'clear':
92 return { text: await clearGoal($) }
93 case 'history':
94 return { text: await listHistory($) }
95 case 'show': {
96 const current = await read($, goal)
97 if (current === null) return { text: `No aim is set. ${USAGE}` }
98 const elapsed = formatDuration((await $.clock.now()) - current.startedAt)
99 return { text: `Aim: "${current.text}" (${elapsed} so far). ${USAGE}` }
100 }
101 case 'set': {
102 const before = await read($, goal)
103 await saveGoal($, { text: command.goal, startedAt: await $.clock.now() })
104 const replaced = before === null ? '' : ` (replaces "${before.text}")`
105 return { text: `Aim set: "${command.goal}"${replaced}. Claude will stay on it and flag unrelated work.` }
106 }
107 }
108 })
109
110 // The goal rides in the system prompt, after the cached shared sections.
111 on('prompt.compose', async ($, e, next) => {
112 const composed = await next(e)
113 const current = await read($, goal)
114 if (current === null || e.traits.includes('bare')) return composed
115 const sections = composed.sections.filter(section => section.id !== SECTION)
116 return { sections: [...sections, { id: SECTION, text: focusPrompt(current.text), scope: 'session' as const }] }
117 })
118
119 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
120 const current = await read($, goal)
121 if (current === null || e.props.hasSurvey) return next(e)
122
123 const { Box, Button, Text } = $.ui.resolve(e)
124 const elapsed = formatDuration((await $.clock.now()) - current.startedAt)
125 const room = Math.max(12, e.props.bodyColumns - elapsed.length - 26)
126 // Keep what the mods after this one draw in the band.
127 const below = await next(e)
128
129 return (
130 <Box flexDirection="column">
131 <Box flexDirection="row" gap={1}>
132 <Text dimColor wrap="truncate-end">
133 🎯 {clip(current.text, room)} · {elapsed}
134 </Text>
135 <Button key="done" label="Done" onPress={() => finishGoal($)} />
136 <Button key="clear" label="Clear" onPress={() => clearGoal($)} />
137 </Box>
138 {below}
139 </Box>
140 )
141 })
142}
143hooks/format.ts 49 lines1// Pure helpers: no `$`, so tests import them directly.
2
3export const HISTORY_LIMIT = 50
4
5/** `<1m`, `12m`, `1h 05m`, `2d 3h`. */
6export function formatDuration(ms: number): string {
7 const minutes = Math.floor(Math.max(0, ms) / 60_000)
8 if (minutes < 1) return '<1m'
9 if (minutes < 60) return `${minutes}m`
10 const hours = Math.floor(minutes / 60)
11 if (hours < 24) return `${hours}h ${String(minutes % 60).padStart(2, '0')}m`
12 return `${Math.floor(hours / 24)}d ${hours % 24}h`
13}
14
15/** Cuts `text` to `room` cells, ending in an ellipsis when cut. */
16export function clip(text: string, room: number): string {
17 const flat = text.replace(/\s+/g, ' ').trim()
18 if (flat.length <= room) return flat
19 return `${flat.slice(0, Math.max(1, room - 1)).trimEnd()}…`
20}
21
22/** The system prompt section that keeps the model on the goal. */
23export function focusPrompt(goal: string): string {
24 return [
25 '# Current focus',
26 `The user has pinned one goal for this session: "${goal}"`,
27 'Keep your work on this goal. If you notice something unrelated that needs doing (a bug elsewhere, a refactor, a dependency upgrade), mention it in one line at the end of your reply instead of doing it, unless the user asks for it.',
28 'If a request seems to move away from the goal, do what the user asked, and say in one line that it is outside the current focus.',
29 ].join('\n')
30}
31
32export type FocusCommand =
33 | { kind: 'show' }
34 | { kind: 'done' }
35 | { kind: 'clear' }
36 | { kind: 'history' }
37 | { kind: 'set'; goal: string }
38
39/** Reads what followed `/focus`. */
40export function parseFocus(args: string): FocusCommand {
41 const text = args.replace(/\s+/g, ' ').trim()
42 const word = text.toLowerCase()
43 if (text === '') return { kind: 'show' }
44 if (word === 'done' || word === 'finish' || word === 'finished') return { kind: 'done' }
45 if (word === 'clear' || word === 'off' || word === 'reset') return { kind: 'clear' }
46 if (word === 'history' || word === 'log') return { kind: 'history' }
47 return { kind: 'set', goal: text.slice(0, 200) }
48}
49types/index.d.ts 12 lines1/** The goal the person is working on, and when they set it (ms since the epoch). */
2export type AimGoal = { text: string; startedAt: number }
3
4/** A finished goal, as `/aim history` lists it. */
5export type AimDone = { text: string; startedAt: number; endedAt: number; root: string }
6
7declare module 'claude-code' {
8 interface PluginState {
9 aim: { goal: AimGoal | null }
10 }
11}
12