/handoff 로 대화를 인수인계서로 정리하고 /pickup 으로 새 세션에서 이어받는다

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.ts 163 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { WarnedAt } from '../types'
5
6/** 컨텍스트 사용률이 이 값(%)을 넘으면 /handoff 를 권한다. 이후 STEP %p 오를 때마다 다시 알린다. */
7export const WARN_AT: WarnedAt = 70
8// usage-hud 가 80%에서 알리므로 70%, 90%에 알려 겹치지 않게 한다.
9export const STEP = 20
10
11const warnedAt = atom({ plugin: 'auto-handoff', key: 'warnedAt' } as const, 0)
12
13const RULES = `# Auto-handoff
14- 큰 작업 단위가 끝났거나 사용자가 마무리 의사를 보이면 \`/handoff\` 실행을 한 줄로 제안한다.
15- 인수인계서는 \`.claude/handoff/HANDOFF.md\`, 수동 메모는 \`.claude/handoff/NOTES.md\`에 있다. 사용자가 이어서 하자고 하면 먼저 읽는다.`
16
17const ASK = `지금까지의 대화를 새 세션이 이어받을 수 있도록 인수인계서를 한국어 마크다운으로 작성한다. 도구는 쓰지 않는다. 대화에서 확인된 사실만 쓰고, 추정은 "(추정)"으로 표시하며, 모르는 것은 지어내지 않는다. 코드 식별자·경로·명령은 원문 그대로 쓴다. 아래 제목을 그대로 쓰고, 해당 없는 절은 "없음"으로 둔다.
18
19## 목표
20## 완료한 것
21## 결정 사항 (이유 포함)
22## 진행 중인 것 (현재 상태, 멈춘 지점)
23## 다음 단계 (우선순위 순, 체크박스)
24## 관련 파일·명령
25## 미해결 문제·주의사항`
26
27const dir = async ($: EngineInterface) => `${await $.session.root()}/.claude/handoff`
28
29// 한국 시간 기준
30const stamp = async ($: EngineInterface) =>
31 new Date((await $.clock.now()) + 9 * 3_600_000).toISOString().slice(0, 16).replace('T', ' ')
32
33const readOr = async ($: EngineInterface, path: string) => {
34 try {
35 return await $.fs.read(path)
36 } catch {
37 return ''
38 }
39}
40
41export const register: Register = on => {
42 on('session.start', async ($, e, next) => {
43 await $.command.register({
44 name: 'handoff',
45 description: '지금까지의 대화를 인수인계서(.claude/handoff/HANDOFF.md)로 정리한다',
46 argumentHint: '[강조할 내용]',
47 })
48 await $.command.register({
49 name: 'pickup',
50 description: '인수인계서를 읽고 이전 작업을 이어받는다',
51 })
52 await $.command.register({
53 name: 'note',
54 description: '인수인계에 포함할 메모를 직접 남긴다 (.claude/handoff/NOTES.md)',
55 argumentHint: '<메모>',
56 })
57
58 if (e.isInteractive && (await $.fs.exists(`${await dir($)}/HANDOFF.md`))) {
59 $.ui.toast('인수인계서가 있습니다. /pickup 으로 이어받을 수 있습니다.')
60 }
61
62 return next(e)
63 })
64
65 on('prompt.compose', async ($, e, next) => {
66 const composed = await next(e)
67
68 return {
69 sections: [
70 ...composed.sections,
71 { id: 'auto-handoff:rules', text: RULES, scope: 'session' },
72 ],
73 }
74 })
75
76 on('command.run', { command: 'note' }, async ($, e) => {
77 const text = e.args.trim()
78
79 if (text === '') {
80 return { text: '사용법: /note <메모>' }
81 }
82
83 const path = `${await dir($)}/NOTES.md`
84 const before = await readOr($, path)
85 await $.fs.write(path, `${before || '# 수동 메모\n\n'}- ${await stamp($)} ${text}\n`)
86
87 return { text: `메모했습니다: ${text}` }
88 })
89
90 on('command.run', { command: 'handoff' }, async ($, e) => {
91 const base = await dir($)
92 const notes = await readOr($, `${base}/NOTES.md`)
93 const stress = e.args.trim()
94 const reply = await $.model.fork({
95 prompt: [
96 ASK,
97 stress && `특히 다음을 빠뜨리지 않는다: ${stress}`,
98 notes && `사용자가 남긴 수동 메모(그대로 반영):\n${notes}`,
99 ]
100 .filter(Boolean)
101 .join('\n\n'),
102 })
103
104 if (!reply.isAnswered) {
105 return {
106 text:
107 reply.reason === 'nothing-to-fork'
108 ? '아직 정리할 대화가 없습니다.'
109 : `인수인계서를 만들지 못했습니다 (${reply.reason}).`,
110 }
111 }
112
113 const when = await stamp($)
114 const body = `# 인수인계서\n\n작성: ${when} (KST)\n\n${reply.text.trim()}\n`
115 await $.fs.write(`${base}/HANDOFF.md`, body)
116 await $.fs.write(`${base}/archive/${when.replace(/[: ]/g, '-')}.md`, body)
117 await update($, warnedAt, () => 0)
118
119 return {
120 text: `인수인계서를 저장했습니다: .claude/handoff/HANDOFF.md\n새 세션에서 /pickup 으로 이어받으세요.`,
121 }
122 })
123
124 on('command.run', { command: 'pickup' }, async $ => {
125 if (!(await $.fs.exists(`${await dir($)}/HANDOFF.md`))) {
126 return { text: '인수인계서가 없습니다. 이전 세션에서 /handoff 를 먼저 실행하세요.' }
127 }
128
129 void $.prompt.submit({
130 text: '.claude/handoff/HANDOFF.md 를 읽고(있으면 .claude/handoff/NOTES.md 도), 현재 상태를 5줄 이내로 요약한 뒤 "다음 단계"의 첫 항목부터 이어서 진행할지 물어봐. 문서 내용과 실제 파일 상태가 다르면 먼저 알려줘.',
131 asUser: true,
132 })
133
134 return { text: '인수인계서를 읽어 이어받습니다.' }
135 })
136
137 on('turn.complete', async ($, e, next) => {
138 const done = await next(e)
139
140 if (e.agentId === undefined) {
141 const percent = (await $.session.usage()).context.percent ?? 0
142 const last = await read($, warnedAt)
143
144 if (percent >= WARN_AT && percent >= last + STEP) {
145 await update($, warnedAt, () => percent)
146 $.ui.toast(`컨텍스트 ${percent}% 사용. /handoff 로 정리하고 새 세션에서 이어가는 것을 권합니다.`)
147 }
148 }
149
150 return done
151 })
152
153 on('session.compact', async ($, e, next) => {
154 const compacted = await next(e)
155
156 if (e.trigger === 'auto' && e.agentId === undefined) {
157 $.ui.toast('대화가 자동 압축되었습니다. 세부 맥락이 줄었으니 /handoff 를 권합니다.')
158 }
159
160 return compacted
161 }).catch(($, e, next) => next(e))
162}
163types/index.d.ts 9 lines1/** 마지막으로 /handoff 를 권했을 때의 컨텍스트 사용률(%). */
2export type WarnedAt = number
3
4declare module 'claude-code' {
5 interface PluginState {
6 'auto-handoff': { warnedAt: WarnedAt }
7 }
8}
9