Shows in the prompt footer when your Extended Brain vault was last linted, and hints when it is time to run /LintBrain.

A Claude Code mod that shows in the prompt footer when your Extended Brain vault was last linted, and tells you when it is time to run /LintBrain again.
focus & 🧹 Lint: 3 days ago
focus & 🧹 Lint: 12 days ago — /LintBrain
focus & 🧹 Lint: never
The entry sits with the dim mode labels at the right of the prompt (focus, memory paused, ...). It adds to them and never replaces one.
| Stamp file | Footer |
|---|---|
| Holds today's date | 🧹 Lint: today |
1 to 7 days old (the limit, see staleAfterDays) | 🧹 Lint: 1 day ago, 🧹 Lint: 5 days ago, ... |
| Older than the limit | 🧹 Lint: 12 days ago — /LintBrain |
| Does not exist (never linted) | 🧹 Lint: never |
| Exists, but holds no valid date or cannot be read | 🧹 Lint: unknown — /LintBrain |
The — /LintBrain part is the hint that the lint skill should run. A stamp dated in the future counts as today.
The stamp is read once, when the session starts. There is no polling: the value only changes when the lint skill runs, which is rare inside a session. Start a new session (or reload the mod) to see a fresh value.
The mod reads one file, .lintbrain, in the root of your vault. It holds a single line with an ISO date:
2026-10-07
Your lint skill writes it at the end of every successful run, with the vault folder as the working directory:
date +%Y-%m-%d > .lintbrain
The mod ships no lint skill. Any tool that writes that file works. Keep the file out of version control if you do not want a commit per run; it is local state.
| Option | Default | Meaning |
|---|---|---|
vaultPath | empty | Absolute path of your vault, the folder that holds .lintbrain. A leading ~ is expanded. |
staleAfterDays | 7 | From how many days without a run the footer adds — /LintBrain. |
Left empty, vaultPath falls back to the EXTENDED_BRAIN_PATH environment variable. If neither is set, or the folder does not exist, the footer stays as it was and a toast says why.
In a terminal session:
/plugin install lintbrain-status --marketplace pmahli/lintbrain-status
Answer y to add the marketplace, pick the user scope, then fill in vaultPath on the settings screen.
The /plugin command is not available in the Code tab of the desktop app, but a mod installed from a terminal at the user scope also loads there.
claude plugin validate .
claude plugin test .
hooks/lint.ts holds the pure helpers (date parsing, footer text), hooks/register.ts the two hooks: session.start reads the stamp into the session's $.state, and a ui.render hook on SessionMode appends the label.
hooks/register.ts 75 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginOptions, Register } from 'claude-code'
3
4import type { LintLabel } from '../types'
5import {
6 dayNumber,
7 describe,
8 expandHome,
9 readStamp,
10 stampPath,
11 staleAfter,
12} from './lint'
13import type { Reading } from './lint'
14
15const lint = atom({ plugin: 'lintbrain-status', key: 'label' } as const, null)
16
17// The vault folder: the option, else EXTENDED_BRAIN_PATH, with `~` expanded.
18// Empty when neither names one.
19async function vaultFolder($: EngineInterface, options: PluginOptions): Promise<string> {
20 const option = typeof options.vaultPath === 'string' ? options.vaultPath.trim() : ''
21 const raw = option !== '' ? option : ((await $.env.get('EXTENDED_BRAIN_PATH')) ?? '').trim()
22
23 return raw === '' ? '' : expandHome(raw, await $.env.get('HOME'))
24}
25
26async function readFooter($: EngineInterface, options: PluginOptions): Promise<LintLabel> {
27 const vault = await vaultFolder($, options)
28
29 if (vault === '') {
30 $.ui.toast('lintbrain-status: set the vaultPath option (or EXTENDED_BRAIN_PATH) to show the last lint run.')
31 return null
32 }
33
34 if (!(await $.fs.exists(vault))) {
35 $.ui.toast(`lintbrain-status: vault folder not found: ${vault}`)
36 return null
37 }
38
39 const file = stampPath(vault)
40 let reading: Reading
41
42 if (!(await $.fs.exists(file))) {
43 reading = { kind: 'never' }
44 } else {
45 try {
46 const today = dayNumber(new Date(await $.clock.now()))
47 reading = readStamp(await $.fs.read(file), today)
48 } catch {
49 // The file is there but cannot be read (a cloud file not downloaded yet, say).
50 reading = { kind: 'unknown' }
51 }
52 }
53
54 return describe(reading, staleAfter(options.staleAfterDays))
55}
56
57export const register: Register = (on, options) => {
58 // Read once per session; the stamp only changes when the lint skill runs.
59 on('session.start', async ($, e, next) => {
60 const footer = await readFooter($, options)
61 await update($, lint, () => footer)
62
63 return next(e)
64 })
65
66 // The footer's dim mode labels sit at the right of the prompt.
67 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
68 const footer = await read($, lint)
69
70 return footer === null
71 ? next(e)
72 : next({ ...e, props: { ...e.props, modes: [...e.props.modes, footer] } })
73 })
74}
75hooks/lint.ts 92 lines1export const STAMP_FILE = '.lintbrain'
2export const DEFAULT_STALE_DAYS = 7
3
4const DAY_MS = 86_400_000
5const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/
6
7/** What the stamp file says: no file, a file that holds no date, or a day count. */
8export type Reading =
9 | { kind: 'never' }
10 | { kind: 'unknown' }
11 | { kind: 'ago'; days: number }
12
13/**
14 * The calendar day a stamp names, as whole days since 1970-01-01, or undefined
15 * when the first non-blank line is no real ISO date (`2026-02-31` is none).
16 */
17export function parseStamp(text: string): number | undefined {
18 const line = text
19 .split(/\r?\n/)
20 .map(part => part.trim())
21 .find(part => part !== '')
22 const match = line === undefined ? null : ISO_DATE.exec(line)
23
24 if (match === null) return undefined
25
26 const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])]
27 const ms = Date.UTC(year, month - 1, day)
28 const date = new Date(ms)
29 // Date.UTC rolls an impossible day over into the next month; a real one round-trips.
30 const isReal =
31 date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
32
33 return isReal ? ms / DAY_MS : undefined
34}
35
36/** A moment's local calendar day, as whole days since 1970-01-01. */
37export function dayNumber(date: Date): number {
38 return Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / DAY_MS
39}
40
41/**
42 * `text` is the stamp file's content, undefined when the file does not exist;
43 * `today` a `dayNumber`. A stamp in the future (clock skew) counts as today.
44 */
45export function readStamp(text: string | undefined, today: number): Reading {
46 if (text === undefined) return { kind: 'never' }
47
48 const day = parseStamp(text)
49
50 return day === undefined ? { kind: 'unknown' } : { kind: 'ago', days: Math.max(0, today - day) }
51}
52
53export function formatAge(days: number): string {
54 if (days === 0) return 'today'
55
56 return days === 1 ? '1 day ago' : `${days} days ago`
57}
58
59/** The footer text. The /LintBrain hint follows a stamp older than `staleAfterDays`. */
60export function describe(reading: Reading, staleAfterDays: number): string {
61 switch (reading.kind) {
62 case 'never':
63 return '🧹 Lint: never'
64 case 'unknown':
65 return '🧹 Lint: unknown — /LintBrain'
66 case 'ago':
67 return reading.days > staleAfterDays
68 ? `🧹 Lint: ${formatAge(reading.days)} — /LintBrain`
69 : `🧹 Lint: ${formatAge(reading.days)}`
70 }
71}
72
73/** The configured threshold when it is a usable number of days, else the default. */
74export function staleAfter(value: unknown): number {
75 return typeof value === 'number' && Number.isFinite(value) && value >= 0
76 ? Math.floor(value)
77 : DEFAULT_STALE_DAYS
78}
79
80/** Expands a leading `~`; `home` is undefined where the environment has none. */
81export function expandHome(path: string, home: string | undefined): string {
82 if (home === undefined || home === '') return path
83 if (path === '~') return home
84
85 return path.startsWith('~/') ? `${home.replace(/\/+$/, '')}${path.slice(1)}` : path
86}
87
88/** The stamp file inside a vault folder. */
89export function stampPath(vault: string): string {
90 return `${vault.replace(/\/+$/, '')}/${STAMP_FILE}`
91}
92types/index.d.ts 12 lines1/** The footer text for the last lint run, or null when there is nothing to show. */
2export type LintLabel = string | null
3
4declare module 'claude-code' {
5 interface PluginState {
6 'lintbrain-status': {
7 /** Read once when the session starts; null hides the footer entry. */
8 label: LintLabel
9 }
10 }
11}
12