Reports the session state to the terminal (OSC 7501) through the program-status command.

Just various config files with some setup files for quickly setting up a new dev environment on another computer.
The primary for this is going to be nix, where the instructions on how to install everything is outlined in nix/flake.nix. This is definitely the fastest way.
The configurations are both valid for OSX (either nix-darwin or home-manager) and a Linux system where home-manager is primarily used. Additionally the configs are actually also stow-compatible if so desired.

hooks/register.ts 232 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// Reports the session to the terminal through the program-status command.
4// https://www.superlogical.com/rex/docs/build/program-status
5// The session is the record "claude". Each subagent is a record below it.
6
7type State = 'idle' | 'working' | 'done' | 'blocked' | 'error' | 'clear'
8type Kind = 'permission' | 'question' | 'auth'
9type Report = { state: State; kind?: Kind; msg?: string }
10type Input = Record<string, unknown>
11
12const ROOT = 'claude'
13
14const clip = (text: string, max = 200) => {
15 const line = text.trim().split('\n')[0] ?? ''
16 return line.length > max ? `${line.slice(0, max - 1)}…` : line
17}
18
19const basename = (path: string) => path.split('/').filter(Boolean).pop() ?? path
20
21const host = (url: string) => {
22 try {
23 return new URL(url).host
24 } catch {
25 return url
26 }
27}
28
29const str = (input: Input, key: string) => (typeof input[key] === 'string' ? (input[key] as string) : '')
30
31// One line about what a tool call does.
32const activity = (tool: string, input: Input): string => {
33 switch (tool) {
34 case 'Bash':
35 return clip(str(input, 'description') || str(input, 'command'))
36 case 'Read':
37 case 'Edit':
38 case 'MultiEdit':
39 case 'Write':
40 return `${tool} ${basename(str(input, 'file_path'))}`
41 case 'NotebookEdit':
42 return `Edit ${basename(str(input, 'notebook_path'))}`
43 case 'Grep':
44 case 'Glob':
45 return clip(`Search ${str(input, 'pattern')}`)
46 case 'WebFetch':
47 return `Fetch ${host(str(input, 'url'))}`
48 case 'WebSearch':
49 return clip(`Search the web: ${str(input, 'query')}`)
50 case 'Agent':
51 case 'Task':
52 return clip(`Agent: ${str(input, 'description')}`)
53 case 'Skill':
54 return `Skill ${str(input, 'skill')}`
55 case 'AskUserQuestion': {
56 const questions = input.questions as { question?: string }[] | undefined
57 return clip(questions?.[0]?.question ?? 'Question')
58 }
59 case 'ExitPlanMode':
60 return 'Review the plan'
61 }
62 if (tool.startsWith('mcp__')) {
63 const [, server, name] = tool.split('__')
64 return `${server}: ${name}`
65 }
66 return tool
67}
68
69// The tools that wait for an answer, not for permission.
70const QUESTIONS = new Set(['AskUserQuestion', 'ExitPlanMode'])
71
72const state = {
73 isEnabled: false,
74 title: '',
75 main: undefined as Report | undefined,
76 // Subagent record ids and their agent types.
77 agents: new Map<string, string>(),
78 tail: Promise.resolve(),
79}
80
81// Runs program-status. The calls run in order, so the last report wins.
82function send($: EngineInterface, id: string, report: Report, extra: string[] = []): Promise<void> {
83 if (!state.isEnabled) return Promise.resolve()
84 const argv = ['program-status', report.state, `id=${id}`, ...extra]
85 if (report.kind) argv.push(`kind=${report.kind}`)
86 if (report.msg) argv.push(`msg=${report.msg}`)
87 state.tail = state.tail
88 .then(() => $.process.run(argv, { timeoutMs: 5000 }))
89 .then(
90 () => undefined,
91 () => undefined,
92 )
93 return state.tail
94}
95
96// Reports the session record when it changes.
97function report($: EngineInterface, next: Report): Promise<void> {
98 const { main } = state
99 if (main?.state === next.state && main.kind === next.kind && main.msg === next.msg) {
100 return Promise.resolve()
101 }
102 state.main = next
103 return send($, ROOT, next, ['app=claude', `title=${state.title}`])
104}
105
106const agentId = (id: string) => `${ROOT}/${id.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 32)}`
107
108export const register: Register = on => {
109 on('session.start', async ($, e, next) => {
110 state.isEnabled = e.isInteractive
111 state.title = basename(e.cwd)
112 state.main = undefined
113 await report($, { state: 'idle' })
114 return next(e)
115 })
116
117 on('session.end', async ($, e, next) => {
118 state.main = undefined
119 state.agents.clear()
120 await send($, ROOT, { state: 'clear' })
121 // A /clear goes on with a new conversation in the same process.
122 if (e.reason === 'clear') await report($, { state: 'idle' })
123 return next(e)
124 })
125
126 on('turn.start', async ($, e, next) => {
127 await report($, { state: 'working', msg: 'Thinking' })
128 return next(e)
129 })
130
131 on('turn.complete', async ($, e, next) => {
132 // Subagents and the engine's own forks have an agentId.
133 if (e.agentId === undefined) {
134 // No answer: the person dismissed a question or a permission prompt.
135 if (e.reason === 'answer') await report($, e.answer.trim() ? { state: 'done', msg: clip(e.answer) } : { state: 'idle' })
136 else if (e.reason === 'aborted') await report($, { state: 'idle', msg: 'Interrupted' })
137 else if (e.reason === 'refusal') await report($, { state: 'error', msg: 'The model refused' })
138 else if (state.main?.state !== 'error') await report($, { state: 'error', msg: 'The turn failed' })
139 }
140 return next(e)
141 })
142
143 on('classic.StopFailure', async ($, e, next) => {
144 await report($, { state: 'error', msg: clip(e.error_details ?? String(e.error)) })
145 return next(e)
146 })
147
148 on('tool.call', async ($, e, next) => {
149 const input = e as unknown as Input
150 const msg = activity(e.tool, input)
151 let started: Promise<void>
152 if (e.agentId !== undefined) {
153 const id = agentId(e.agentId)
154 const type = state.agents.get(id)
155 started = type === undefined ? Promise.resolve() : send($, id, { state: 'working', msg }, [`title=${type}`])
156 } else if (QUESTIONS.has(e.tool)) {
157 started = report($, { state: 'blocked', kind: 'question', msg })
158 } else {
159 started = report($, { state: 'working', msg })
160 }
161 const result = await next(e)
162 await started
163 if (state.main?.state === 'blocked') {
164 await report($, { state: 'working', msg: 'Thinking' })
165 }
166 return result
167 })
168
169 on('classic.PermissionRequest', async ($, e, next) => {
170 const input = (e.tool_input ?? {}) as Input
171 const kind: Kind = QUESTIONS.has(e.tool_name) ? 'question' : 'permission'
172 const msg = kind === 'question' ? activity(e.tool_name, input) : clip(`Allow ${activity(e.tool_name, input)}?`)
173 await report($, { state: 'blocked', kind, msg })
174 return next(e)
175 })
176
177 // The background hint shows only while an approved command runs.
178 on('ui.render', { component: 'ToolProgress' }, async ($, e, next) => {
179 if (state.main?.state === 'blocked' && state.main.kind === 'permission') {
180 await report($, { state: 'working', msg: 'Running' })
181 }
182 return next(e)
183 })
184
185 on('classic.Notification', async ($, e, next) => {
186 if (state.main?.state !== 'blocked') {
187 if (e.notification_type === 'permission_prompt') {
188 await report($, { state: 'blocked', kind: 'permission', msg: clip(e.message) })
189 } else if (e.notification_type === 'elicitation_dialog') {
190 await report($, { state: 'blocked', kind: 'question', msg: clip(e.message) })
191 }
192 }
193 return next(e)
194 })
195
196 on('classic.Elicitation', async ($, e, next) => {
197 const kind: Kind = e.mode === 'url' ? 'auth' : 'question'
198 await report($, { state: 'blocked', kind, msg: clip(`${e.mcp_server_name}: ${e.message}`) })
199 return next(e)
200 })
201
202 on('classic.ElicitationResult', async ($, e, next) => {
203 await report($, { state: 'working', msg: 'Thinking' })
204 return next(e)
205 })
206
207 on('classic.PreCompact', async ($, e, next) => {
208 await report($, { state: 'working', msg: 'Compacting' })
209 return next(e)
210 })
211
212 on('classic.PostCompact', async ($, e, next) => {
213 // A manual /compact runs at the prompt. An automatic one runs in a turn.
214 await report($, e.trigger === 'manual' ? { state: 'idle' } : { state: 'working', msg: 'Thinking' })
215 return next(e)
216 })
217
218 on('classic.SubagentStart', async ($, e, next) => {
219 const id = agentId(e.agent_id)
220 state.agents.set(id, e.agent_type)
221 await send($, id, { state: 'working' }, [`title=${e.agent_type}`])
222 return next(e)
223 })
224
225 on('classic.SubagentStop', async ($, e, next) => {
226 const id = agentId(e.agent_id)
227 state.agents.delete(id)
228 await send($, id, { state: 'clear' })
229 return next(e)
230 })
231}
232