/pin 으로 결정 사항과 할 일을 고정해 매 요청에 싣는다

A Claude Code mod that draws a usage HUD above the prompt. Two ASCII dogs keep an eye on your limits.
Claude Code 프롬프트 위에 사용량 HUD를 그려주는 mod입니다. 강아지 두 마리가 한도를 지켜봅니다.
__ 5h ████░░░░ 13% / \__ 7d █████░░░ 64% ctx ██░░░░░░ 23% 226k/1.0M
o-''|\_____/) ↻ 40m ( @\___ ↻ 17h40m last 1m54s · tools 13 +$0.80 2.2M↑ 10k↓
\_/|_) ) “공 물어올게” / O “장부 정리 중” cache 99%
\ __ / / (_____/ Opus 5.5 · medium AUTO $7.06 ⎇ main ✓
(_/ (_/ /_____/ U
Claude Code 터미널 세션의 프롬프트에서 다음을 입력합니다.
/plugin install usage-hud --marketplace Jang-seungminn/usage-hud
Add marketplace?에 y를 입력하고, scope를 고릅니다. 모든 세션에서 쓰려면 user scope를 선택합니다. 설치한 세션에서 바로 동작합니다.
같은 marketplace에 다른 mod도 있습니다. 필요한 것만 같은 방식으로 설치합니다.
/plugin install plan-board --marketplace Jang-seungminn/usage-hud
| mod | 하는 일 |
|---|---|
| plan-board | 사이드 패널: 목표, superpowers 플랜 진행률, 작업, 서브에이전트, 핀, 다음 단계, 인수인계 |
| auto-handoff | /handoff로 대화를 .claude/handoff/HANDOFF.md에 정리, 새 세션에서 /pickup으로 이어받기, /note로 메모. 컨텍스트 70%·90%에서 알림 |
| pinboard | /pin <결정>, /pin 할일: <내용>, /unpin <번호>, /pins. 핀은 .claude/pinboard.md에 저장되고 매 요청의 시스템 프롬프트에 실림 |
| next-steps | 작업을 끝낸 응답 끝에 다음 단계 체크박스 2~3개를 제안. /next로 목록, /next <번호>로 입력창에 넣기 |
| prompt-coach | /coach <요청>(또는 입력창의 내용)을 대상·완료 기준·제약이 분명한 요청으로 다듬어 입력창에 넣기. 실행 전 불명확하면 질문하라는 규칙 포함 |
| korean-ui | 한국어 응답 규칙, 기본 슬래시 명령 설명을 한국어로 |
auto-handoff, pinboard, next-steps, prompt-coach, korean-ui는 시스템 프롬프트에 짧은 규칙을 하나씩 덧붙입니다. 쓰지 않는 것은 설치하지 않는 편이 요청당 토큰을 아낍니다.
| 항목 | 내용 |
|---|---|
| 5h / 7d | 사용 한도 게이지, 리셋까지 남은 시간, 리셋 전에 한도가 바닥날 것 같으면 ⚡N 후 소진 |
| ctx | context window 사용량 |
| 턴 | 진행 중에는 경과 시간과 도구 호출 수, 끝난 뒤에는 그 턴의 비용과 입출력 토큰 |
| cache | 마지막 응답의 prompt cache 적중률 |
| 세션 | 모델 · effort, 권한 모드 배지, 누적 비용 |
| git | 브랜치, ahead/behind, staged/수정/추적 안 된 파일 수 |
| agents / bg | 실행 중인 서브에이전트와 백그라운드 작업 수 |
알림(toast):
수치에 따라 상태가 바뀝니다.
| 상태 | 조건 |
|---|---|
| 여유 | 기본 |
| 작업 중 | Claude가 일하는 동안 |
| 완료 | 턴이 끝나고 20초 동안 |
| 진땀 | 50% 이상 |
| 패닉 | 80% 이상 |
| 소진 | 100% |
| 수면 | 10분 넘게 쉬는 중 |
/hud를 입력하면 강아지를 끄고 켭니다. 강아지는 band가 5줄을 쓸 수 있을 때만 나오고, 터미널이 130칸 이상이면 모든 정보가 한 줄로 나란히 배치됩니다.
사이드 패널(fullscreen에서는 transcript 옆, 아니면 프롬프트 위)에 이 세션이 무엇을 하려는지와 어디까지 왔는지를 보여줍니다. 보여줄 것이 처음 생길 때 패널이 한 번 열리고, 닫은 뒤에는 /board로 다시 엽니다.
◎ 목표
GitHub 로그인 붙이기
plan Users can sign in with GitHub.
/goal bun test exits 0
▤ 플랜 2026-10-11-auth.md
plan Auth Feature Implementation Plan
task ██████░░░░░░ 1/3
step ██████████░░ 3/5
▶ Task 2: Session cookie
✓ Step 1: Write the failing test
○ Step 2: Implement setCookie
○ Task 3: Logout
☰ 작업 ████░░░░ 1/2
▶ Writing plan
✓ Write spec
⚙ 에이전트 실행 1 · 완료 2
● Explore auth code [Explore] 1m05s
✓ Review spec 42s
| 영역 | 어디서 오나 |
|---|---|
| 목표 | /mission <목표>로 정한 목표. 없으면 세션의 첫 프롬프트. 플랜 파일의 Goal: 줄, 내장 /goal(또는 ProposeGoal)의 완료 조건도 함께 |
| 플랜 | …/plans/*.md 파일을 Write/Edit 하면 읽어서, 체크박스가 있는 제목(### Task N: …)을 작업으로, 체크박스를 단계로 셉니다. 매 턴이 끝날 때 다시 읽으므로 sed 등으로 체크해도 반영됩니다. superpowers의 docs/superpowers/plans/, docs/plans/ 모두 해당 |
| spec | …/specs/*.md 또는 *-design.md를 쓰면 제목을 표시 |
| 작업 / 할 일 | TaskCreate·TaskUpdate·TaskList, TodoWrite. 진행 중 → 대기 → 완료 순으로, 완료는 최근 3개만 |
| 에이전트 | Agent 호출과 SubagentStart/Stop. 실행 중인 것은 경과 시간, 끝난 것은 최근 3개 |
| 핀 | pinboard의 .claude/pinboard.md (10초마다, 턴이 끝날 때 다시 읽음) |
| 다음 단계 | 마지막 응답의 다음 단계 아래 체크박스 (next-steps가 제안하게 함) |
| 인수인계 | auto-handoff의 .claude/handoff/HANDOFF.md 작성 시각과 NOTES.md 메모 수 |
핀·다음 단계·인수인계는 해당 mod가 없어도 파일이나 응답 형식만 맞으면 표시됩니다.
명령:
/board 패널 열기, /board clear 작업·플랜·에이전트 목록 비우기/mission <목표> 목표 정하기, /mission 현재 목표 보기, /mission clear 지우기각 mod는 plugins/<이름>/에 있고, 저장소 루트는 marketplace입니다.
claude --plugin-dir ./plugins/usage-hud --plugin-dir ./plugins/plan-board
claude plugin validate . # marketplace
for m in plugins/*; do claude plugin validate $m && claude plugin test $m; done
MIT
hooks/register.tsx 113 lines1import { atom, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pin } from '../types'
5
6// plan-board 패널이 .claude/pinboard.md 를 읽어 핀을 보여준다. 프롬프트 위 영역은 usage-hud 가 쓴다.
7const pins = atom({ plugin: 'pinboard', key: 'pins' } as const, [])
8const LINE = /^- \[(결정|할일)\] (\d{4}-\d{2}-\d{2}) (.+)$/
9const HEAD = '# Pinboard\n\n/pin, /unpin 으로 관리한다. 한 줄에 핀 하나.\n\n'
10
11const RULES = `# Pinboard
12- 아래 핀은 사용자가 고정한 결정 사항과 진행 중인 할 일이다. 요청이 핀의 결정과 충돌하면 진행하기 전에 해당 핀을 인용해 알린다.
13- 사용자가 무언가를 확정하면("이걸로 가자", "결정" 등) \`/pin <내용>\`으로 기록할지 한 줄로 제안한다. 핀 파일을 직접 고치지 않는다.`
14
15export const parse = (text: string): Pin[] =>
16 text.split('\n').flatMap(line => {
17 const found = LINE.exec(line.trim())
18
19 return found === null
20 ? []
21 : [{ kind: found[1] as Pin['kind'], date: found[2] ?? '', text: found[3] ?? '' }]
22 })
23
24export const format = (list: readonly Pin[]): string =>
25 HEAD + list.map(pin => `- [${pin.kind}] ${pin.date} ${pin.text}`).join('\n') + '\n'
26
27const file = async ($: EngineInterface) => `${await $.session.root()}/.claude/pinboard.md`
28
29const load = async ($: EngineInterface): Promise<Pin[]> => {
30 try {
31 return parse(await $.fs.read(await file($)))
32 } catch {
33 return []
34 }
35}
36
37const save = async ($: EngineInterface, list: Pin[]) => {
38 await $.fs.write(await file($), format(list))
39 await update($, pins, () => list)
40}
41
42// 한국 시간 기준 날짜
43const today = async ($: EngineInterface) =>
44 new Date((await $.clock.now()) + 9 * 3_600_000).toISOString().slice(0, 10)
45
46const listing = (list: readonly Pin[]) =>
47 list.map((pin, i) => `${i + 1}. [${pin.kind}] ${pin.text} (${pin.date})`).join('\n')
48
49export const register: Register = on => {
50 on('session.start', async ($, e, next) => {
51 await $.command.register({
52 name: 'pin',
53 description: '결정 사항을 고정한다. "할일:"로 시작하면 진행 중 할 일로 고정',
54 argumentHint: '[할일:] <내용>',
55 })
56 await $.command.register({
57 name: 'unpin',
58 description: '번호의 핀을 내린다 (끝난 할 일, 바뀐 결정)',
59 argumentHint: '<번호>',
60 })
61 await $.command.register({ name: 'pins', description: '고정된 핀 목록 보기' })
62 await update($, pins, await load($).then(list => () => list))
63
64 return next(e)
65 })
66
67 on('command.run', { command: 'pin' }, async ($, e) => {
68 const raw = e.args.trim()
69 const isTodo = /^(할일|todo)\s*[::]/i.test(raw)
70 const text = raw.replace(/^(할일|todo|결정)\s*[::]\s*/i, '').replace(/\s+/g, ' ')
71
72 if (text === '') {
73 return { text: '사용법: /pin <결정 내용> 또는 /pin 할일: <내용>' }
74 }
75
76 const list = [...(await load($)), { kind: isTodo ? '할일' : '결정', date: await today($), text } as Pin]
77 await save($, list)
78
79 return { text: `고정했습니다.\n${listing(list)}` }
80 })
81
82 on('command.run', { command: 'unpin' }, async ($, e) => {
83 const list = await load($)
84 const at = Number(e.args.trim()) - 1
85 const gone = list[at]
86
87 if (gone === undefined) {
88 return { text: `사용법: /unpin <번호>\n${listing(list) || '고정된 핀이 없습니다.'}` }
89 }
90
91 await save($, list.filter((_, i) => i !== at))
92
93 return { text: `내렸습니다: [${gone.kind}] ${gone.text}` }
94 })
95
96 on('command.run', { command: 'pins' }, async $ => {
97 const list = await load($)
98 await update($, pins, () => list)
99
100 return { text: listing(list) || '고정된 핀이 없습니다. /pin <내용>으로 추가하세요.' }
101 })
102
103 on('prompt.compose', async ($, e, next) => {
104 const composed = await next(e)
105 const list = await load($)
106 const text = `${RULES}\n\n현재 핀:\n${listing(list) || '(없음)'}`
107
108 return {
109 sections: [...composed.sections, { id: 'pinboard:pins', text, scope: 'session' }],
110 }
111 })
112}
113types/index.d.ts 8 lines1export type Pin = { kind: '결정' | '할일'; date: string; text: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 pinboard: { pins: Pin[] }
6 }
7}
8