EMCC's shared base: the $.emcc noun (evidence of tests, builds, lints and edits; badges; profile; kill switch) and one status band above the prompt.

The base EMCC mods build on. Tested with Claude Code 2.1.287, terminal and desktop.
test pass, lint FAIL 1), the badges other EMCC mods post, and a Halt button. It stays hidden until there is something to show./emcc prints the profile, the evidence and the badges./emcc-halt turns every EMCC gate off for the session, or back on. The band's Halt button does the same./config, or claude plugin install emcc-core@emcc --config profile=strict): standard (gates report what they let through; goalpost-guard still asks when there is no screen), strict (gates deny) or off.$.emccemcc-core may not be installed, and claude plugin validate refuses a mod that reads $.emcc as a value (if ($.emcc)), so call a method and catch: with emcc-core missing, the call throws.
async function emccProfile($: EngineInterface): Promise<EmccProfile | null> {
try {
return await $.emcc.profile()
} catch {
return null // emcc-core is not installed
}
}
// in a hook
const profile = await emccProfile($)
if (profile === 'strict') {
const test = await $.emcc.evidenceLatest({ kind: 'test' })
if (!test?.isOk) {
return { deny: 'Run the tests first.' }
}
}
| Method | Answers |
|---|---|
evidenceLatest({ kind }) | The newest evidence of test, build, lint, typecheck or edit, or null |
evidenceSince({ at }) | All evidence at or after at (ms, as $.clock.now() reads) |
badge({ id, text?, tone? }) | Shows or replaces your slot in the band; without text removes it. Not while drawing |
profile() | standard, strict or off (off also while halted) |
isHalted() | Whether the kill switch is on |
flag({ source, rule, file?, text }) | Records a lowered bar for other mods to show. Not while drawing |
flagsSince({ at }) | All flags raised at or after at (ms, as $.clock.now() reads); the newest 200 are kept for the session |
kindsOf({ command }) | The evidence kinds a shell command runs, as the log classifies it |
The types are in types/index.d.ts; /plugin-types copies them for a mod that depends on emcc-core.
How evidence is read. A finished Bash command is classified by its text (npm test, pytest, tsc, eslint, cargo build…, see hooks/evidence.ts). Its outcome comes from what the engine reported: no error is exit 0, an error is the Exit code N the model read. Commands sent to the background are not recorded, since their outcome is not known yet. Edits come from Edit, Write and NotebookEdit. The newest 200 are kept for the session.
engine.create (adds $.emcc), session.start, command.run (emcc, emcc-halt), tool.call (Bash, Edit, Write, NotebookEdit, after they run, never changing them), ui.render (AbovePrompt)$.clock.now, $.command.register, $.state.get, $.state.set, $.ui.resolveclaude plugin test mods/emcc-core
12 tests: the classifier, the evidence bus, profile and kill switch, badges, and the band on both terminal and desktop.
hooks/register.tsx 255 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderElement } from 'claude-code'
3
4import type { EmccBadge, EmccEvidence, EmccEvidenceKind, EmccFlag, EmccProfile, EmccTone } from '../types'
5import { exitCodeOf, kindsOf } from './evidence'
6
7const evidence = atom({ plugin: 'emcc-core', key: 'evidence' } as const, [])
8const badges = atom({ plugin: 'emcc-core', key: 'badges' } as const, [])
9const isHalted = atom({ plugin: 'emcc-core', key: 'isHalted' } as const, false)
10
11const KEEP = 200
12const SHOWN: readonly EmccEvidenceKind[] = ['test', 'typecheck', 'lint', 'build']
13// Anthropic's palette: olive for what holds, clay for what needs a look,
14// stone for everything secondary.
15const OLIVE = '#788c5d'
16const CLAY = '#d97757'
17const STONE = '#b0aea5'
18
19const TONE_COLOR: Record<EmccTone, string | undefined> = {
20 neutral: undefined,
21 good: OLIVE,
22 warn: CLAY,
23 bad: CLAY,
24}
25
26const PROFILES: readonly EmccProfile[] = ['standard', 'strict', 'off']
27
28function configuredProfile(value: unknown): EmccProfile {
29 return PROFILES.find(profile => profile === value) ?? 'standard'
30}
31
32function latestOf(list: readonly EmccEvidence[], kind: EmccEvidenceKind): EmccEvidence | null {
33 for (let index = list.length - 1; index >= 0; index -= 1) {
34 const item = list[index]
35 if (item?.kind === kind) {
36 return item
37 }
38 }
39
40 return null
41}
42
43function summaryOf(item: EmccEvidence): string {
44 return item.isOk ? `${item.kind} pass` : `${item.kind} FAIL ${item.exitCode ?? ''}`.trim()
45}
46
47async function record($: EngineInterface, item: EmccEvidence): Promise<void> {
48 await update($, evidence, list => [...list, item].slice(-KEEP))
49}
50
51export const register: Register = (on, options) => {
52 const configured = configuredProfile(options.profile)
53
54 on('engine.create', async (_$, e, next) => {
55 const beneath = await next(e)
56
57 const emcc: EngineInterface['emcc'] = {
58 evidenceLatest: async ({ kind }) => {
59 const { value = [] } = await beneath.state.get({ plugin: 'emcc-core', key: 'evidence' })
60
61 return latestOf(value, kind)
62 },
63 evidenceSince: async ({ at }) => {
64 const { value = [] } = await beneath.state.get({ plugin: 'emcc-core', key: 'evidence' })
65
66 return value.filter(item => item.at >= at)
67 },
68 badge: async ({ id, text, tone }) => {
69 for (let attempt = 0; attempt < 5; attempt += 1) {
70 const held = await beneath.state.get({ plugin: 'emcc-core', key: 'badges' })
71 const others = (held.value ?? []).filter(slot => slot.id !== id)
72 const slot: EmccBadge = { id, text: text ?? '', tone: tone ?? 'neutral' }
73 const shown = text === undefined || text === '' ? others : [...others, slot]
74 const written = await beneath.state.set({ plugin: 'emcc-core', key: 'badges' }, shown, {
75 ifVersion: held.version,
76 })
77 if (written.isSet) {
78 return
79 }
80 }
81 throw new Error('emcc-core: the badges kept changing; try again')
82 },
83 profile: async () => {
84 const { value = false } = await beneath.state.get({ plugin: 'emcc-core', key: 'isHalted' })
85
86 return value ? 'off' : configured
87 },
88 isHalted: async () => {
89 const { value = false } = await beneath.state.get({ plugin: 'emcc-core', key: 'isHalted' })
90
91 return value
92 },
93 flag: async ({ source, rule, file, text }) => {
94 const raised: EmccFlag = { source, rule, ...(file === undefined ? {} : { file }), text, at: await beneath.clock.now() }
95 for (let attempt = 0; attempt < 5; attempt += 1) {
96 const held = await beneath.state.get({ plugin: 'emcc-core', key: 'flags' })
97 const written = await beneath.state.set(
98 { plugin: 'emcc-core', key: 'flags' },
99 [...(held.value ?? []), raised].slice(-KEEP),
100 { ifVersion: held.version },
101 )
102 if (written.isSet) {
103 return
104 }
105 }
106 throw new Error('emcc-core: the flags kept changing; try again')
107 },
108 flagsSince: async ({ at }) => {
109 const { value = [] } = await beneath.state.get({ plugin: 'emcc-core', key: 'flags' })
110
111 return value.filter(item => item.at >= at)
112 },
113 kindsOf: async ({ command }) => kindsOf(command),
114 }
115
116 const added = { emcc }
117
118 return { ...added, ...beneath }
119 })
120
121 on('session.start', async ($, e, next) => {
122 await $.command.register({ name: 'emcc', description: 'Show the EMCC profile, evidence and badges' })
123 await $.command.register({ name: 'emcc-halt', description: 'Turn every EMCC gate off for this session, or back on' })
124
125 return next(e)
126 })
127
128 on('command.run', { command: 'emcc' }, async $ => {
129 const list = await read($, evidence)
130 const profile = (await read($, isHalted)) ? 'off' : configured
131 const facts = SHOWN.map(kind => latestOf(list, kind))
132 .filter((item): item is EmccEvidence => item !== null)
133 .map(summaryOf)
134 const slots = (await read($, badges)).map(slot => `${slot.id}: ${slot.text}`)
135 const lines = [
136 `EMCC profile: ${profile}${(await read($, isHalted)) ? ' (halted)' : ''}`,
137 `Evidence: ${facts.length > 0 ? facts.join(', ') : 'none yet'}`,
138 `Badges: ${slots.length > 0 ? slots.join(', ') : 'none'}`,
139 ]
140
141 return { text: lines.join('\n') }
142 })
143
144 on('command.run', { command: 'emcc-halt' }, async $ => {
145 const now = await update($, isHalted, halted => !halted)
146
147 return { text: now ? 'EMCC halted: every gate is off until /emcc-halt again.' : 'EMCC resumed.' }
148 })
149
150 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
151 const ran = await next(e)
152 const kinds = kindsOf(e.command)
153 const isBackground = ran.deny === undefined && !ran.isError && ran.result.backgroundTaskId !== undefined
154 if (ran.deny !== undefined || kinds.length === 0 || isBackground) {
155 return ran
156 }
157 const exitCode = exitCodeOf(ran.isError === true, ran.text)
158 const at = await $.clock.now()
159 for (const kind of kinds) {
160 await record($, { kind, at, command: e.command.slice(0, 200), exitCode, isOk: exitCode === 0 })
161 }
162
163 return ran
164 })
165
166 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
167 const ran = await next(e)
168 if (ran.deny === undefined && ran.isError !== true) {
169 await record($, { kind: 'edit', at: await $.clock.now(), file: e.file_path })
170 }
171
172 return ran
173 })
174
175 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
176 const ran = await next(e)
177 if (ran.deny === undefined && ran.isError !== true) {
178 await record($, { kind: 'edit', at: await $.clock.now(), file: e.file_path })
179 }
180
181 return ran
182 })
183
184 on('tool.call', { tool: 'NotebookEdit' }, async ($, e, next) => {
185 const ran = await next(e)
186 if (ran.deny === undefined && ran.isError !== true) {
187 await record($, { kind: 'edit', at: await $.clock.now(), file: e.notebook_path })
188 }
189
190 return ran
191 })
192
193 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
194 const halted = await read($, isHalted)
195 if (e.props.hasSurvey || (configured === 'off' && !halted)) {
196 return next(e)
197 }
198 const list = await read($, evidence)
199 const facts = SHOWN.map(kind => latestOf(list, kind)).filter((item): item is EmccEvidence => item !== null)
200 const slots = await read($, badges)
201 if (!halted && facts.length === 0 && slots.length === 0) {
202 return next(e)
203 }
204 // One band for every plugin: what draws beneath (headspace, witness) keeps the rows EMCC's one row leaves.
205 const below = await next({ ...e, props: { ...e.props, maxRows: Math.max(1, e.props.maxRows - 1) } })
206 const { Box, Button, Text } = $.ui.resolve(e)
207
208 const row = (
209 <Box>
210 <Text color={CLAY} bold>
211 EMCC{' '}
212 </Text>
213 <Text color={STONE}>{halted ? 'halted' : configured} </Text>
214 {facts.map(item => (
215 <Text color={item.isOk ? OLIVE : CLAY} bold={!item.isOk}>
216 {summaryOf(item)}{' '}
217 </Text>
218 ))}
219 {slots.map(slot => (
220 <Text color={TONE_COLOR[slot.tone]} bold={slot.tone === 'bad'}>
221 {slot.text}{' '}
222 </Text>
223 ))}
224 <Button
225 key="halt"
226 label={halted ? 'Resume' : 'Halt'}
227 onPress={() => update($, isHalted, value => !value)}
228 />
229 </Box>
230 )
231 if (isBlank(below)) {
232 return row
233 }
234
235 return (
236 <Box flexDirection="column">
237 {row}
238 {below}
239 </Box>
240 )
241 })
242}
243
244/** Whether a tree beneath draws nothing a person would see (no tree, or empty Boxes and Texts). */
245function isBlank(tree: RenderElement | null | undefined): boolean {
246 if (tree === null || tree === undefined) {
247 return true
248 }
249 if (tree.type !== 'Box' && tree.type !== 'Text') {
250 return false
251 }
252
253 return (tree.children ?? []).every(child => (typeof child === 'string' ? child === '' : typeof child === 'object' && child !== null && 'type' in child && isBlank(child as RenderElement)))
254}
255hooks/evidence.ts 40 lines1import type { EmccCommandKind } from '../types'
2
3const PATTERNS: ReadonlyArray<readonly [EmccCommandKind, RegExp]> = [
4 [
5 'test',
6 /\b(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test|vitest|jest|pytest|python3?\s+-m\s+(?:pytest|unittest)|go\s+test|cargo\s+(?:test|nextest)|ctest|mocha|playwright\s+test|rspec|phpunit|dotnet\s+test|deno\s+test|claude\s+plugin\s+test)\b/,
7 ],
8 ['typecheck', /\b(?:tsc|vue-tsc|mypy|pyright|cargo\s+check|flow\s+check)\b/],
9 [
10 'lint',
11 /\b(?:eslint|ruff|flake8|pylint|biome\s+(?:check|lint)|cargo\s+clippy|golangci-lint|stylelint|prettier\s+--check|claude\s+plugin\s+validate)\b/,
12 ],
13 [
14 'build',
15 /\b(?:(?:npm|pnpm|yarn|bun)\s+run\s+build|cargo\s+build|go\s+build|make|cmake\s+--build|ninja|vite\s+build|next\s+build|gradlew?\s+build|mvn\s+(?:package|install)|dotnet\s+build)\b/,
16 ],
17]
18
19/**
20 * The kinds a shell command runs, in the order checked: a chain such as
21 * `npm run build && npm test` is both a build and a test.
22 */
23export function kindsOf(command: string): EmccCommandKind[] {
24 return PATTERNS.filter(([, pattern]) => pattern.test(command)).map(([kind]) => kind)
25}
26
27/**
28 * The exit code of a finished command, from what the engine reported: 0 when
29 * it did not error, the "Exit code N" the model read when it did, 1 when the
30 * error carries no code.
31 */
32export function exitCodeOf(isError: boolean, text: string | undefined): number {
33 if (!isError) {
34 return 0
35 }
36 const match = /Exit code (\d+)/.exec(text ?? '')
37
38 return match ? Number(match[1]) : 1
39}
40types/index.d.ts 125 lines1/**
2 * The `$.emcc` noun as every caller sees it: EMCC's shared base for mods.
3 *
4 * emcc-core adds the noun in its `engine.create` step. Another mod calls it
5 * (`await $.emcc.profile()`) after checking that it exists, since emcc-core
6 * may not be installed. Nothing here is imported, so it stands on its own.
7 */
8
9/**
10 * What a piece of evidence is about: a command of one of four kinds whose
11 * outcome is known, or a file the session edited.
12 */
13export type EmccEvidenceKind = 'test' | 'build' | 'lint' | 'typecheck' | 'edit'
14
15/**
16 * The kinds a shell command can run: every evidence kind but `edit`.
17 */
18export type EmccCommandKind = Exclude<EmccEvidenceKind, 'edit'>
19
20/**
21 * One observed fact. Commands carry their outcome; edits carry the file.
22 * `at` is the session clock in milliseconds, as `$.clock.now()` reads it.
23 */
24export type EmccEvidence = {
25 kind: EmccEvidenceKind
26 at: number
27 command?: string
28 exitCode?: number
29 isOk?: boolean
30 file?: string
31}
32
33/**
34 * How strict the EMCC gates are. `off` also stands while the session is
35 * halted, so a mod that reads `profile()` alone respects the kill switch.
36 */
37export type EmccProfile = 'standard' | 'strict' | 'off'
38
39/**
40 * A mod's slot in the one EMCC band above the prompt.
41 */
42export type EmccTone = 'neutral' | 'good' | 'warn' | 'bad'
43
44export type EmccBadge = { id: string; text: string; tone: EmccTone }
45
46/**
47 * A lowered bar a mod noticed: what rule, where, and one line for people.
48 * `at` is the session clock in milliseconds, set by emcc-core.
49 */
50export type EmccFlag = {
51 source: string
52 rule: string
53 file?: string
54 text: string
55 at: number
56}
57
58export type Emcc = {
59 /**
60 * The newest evidence of a kind, or null when there is none yet.
61 * @example
62 * const test = await $.emcc.evidenceLatest({ kind: 'test' })
63 */
64 evidenceLatest: (input: { kind: EmccEvidenceKind }) => Promise<EmccEvidence | null>
65
66 /**
67 * Every evidence recorded at or after `at`, oldest first.
68 * @example
69 * const since = await $.emcc.evidenceSince({ at: turnStartedAt })
70 */
71 evidenceSince: (input: { at: number }) => Promise<EmccEvidence[]>
72
73 /**
74 * Shows, replaces or (without `text`) removes a mod's slot in the band.
75 * Call it from a hook or a handler, never while drawing.
76 * @example
77 * await $.emcc.badge({ id: 'redgreen', text: 'RED', tone: 'bad' })
78 */
79 badge: (input: { id: string; text?: string; tone?: EmccTone }) => Promise<void>
80
81 /**
82 * The active profile: the configured one, or `off` while halted.
83 */
84 profile: () => Promise<EmccProfile>
85
86 /**
87 * Whether the kill switch is on (`/emcc-halt` or the band's Halt button).
88 */
89 isHalted: () => Promise<boolean>
90
91 /**
92 * Records a lowered bar for other mods to show. Call it from a hook or a
93 * handler, never while drawing.
94 * @example
95 * await $.emcc.flag({ source: 'goalpost-guard', rule: 'tests.expected', file, text: 'changes the values in 1 assertion' })
96 */
97 flag: (input: { source: string; rule: string; file?: string; text: string }) => Promise<void>
98
99 /**
100 * Every flag raised at or after `at`, in the order they were recorded.
101 */
102 flagsSince: (input: { at: number }) => Promise<EmccFlag[]>
103
104 /**
105 * The evidence kinds a shell command runs, as the log classifies it:
106 * `npm run build && npm test` is `['test', 'build']`, `git status` is `[]`.
107 */
108 kindsOf: (input: { command: string }) => Promise<EmccCommandKind[]>
109}
110
111declare module 'claude-code' {
112 interface EngineInterface {
113 emcc: Emcc
114 }
115
116 interface PluginState {
117 'emcc-core': {
118 evidence: EmccEvidence[]
119 badges: EmccBadge[]
120 flags: EmccFlag[]
121 isHalted: boolean
122 }
123 }
124}
125