Shows your Obsidian Kanban board (BOARD.md) in the status line, with a /board pane listing every card

Small, tested mods for Claude Code. Each one does one job, asks for the least access it can, and says exactly what it touches.
Mods need Claude Code 2.1.287 or later. They run in the terminal and in the Claude desktop app's Code tab.
Type the line at the prompt of a terminal Claude Code session, answer y to add the marketplace, then pick a scope:
/plugin install secret-guard --marketplace Sajid-Rahman0126/claude-studio-mods
Swap secret-guard for touched-files or kanban-status to install the others.
Blocks Claude's Read, Edit and Write tools, and shell commands, from touching files that usually hold secrets: .env and .env.*, *.pem / *.key / *.p12, SSH private keys, ~/.aws/credentials, .npmrc, .netrc, .pypirc, Docker and GitHub CLI auth files, and *credentials*.json / *service-account*.json. Template files such as .env.example stay allowed. Symlinks are followed, so a harmless-looking link to a key is still blocked. When it blocks something, Claude is told why, and you see a short notice.
Keeps a list of every file Claude edited or wrote in this session. The status line shows a count, and /touched opens a pane with each file and how many times it changed.
Shows an Obsidian Kanban board in the status line, such as Board · Todo 3 · Doing 1. /board opens a pane with every open card. Works with any Markdown file that uses ## Column headings and - [ ] cards.
BOARD.md in the session folder by default. Change it with the Board file option in the plugin's settings. It writes nothing and never uses the network.Mods run with your permissions and are not sandboxed. Read the code before installing any mod, these included; each one is a few dozen lines in mods/<name>/hooks/. To start Claude Code with no mods loaded, run claude --safe-mode.
Each mod has tests. From a mod's folder:
claude plugin validate .
claude plugin test .
MIT
hooks/register.tsx 63 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { BoardState } from '../types'
5import { parseBoard, statusLine } from './parse'
6
7const PANE = 'kanban-status'
8const board = atom({ plugin: 'kanban-status', key: 'board' } as const, null)
9
10async function refresh($: EngineInterface, file: string): Promise<BoardState> {
11 let next: BoardState
12 try {
13 next = { file, columns: parseBoard(await $.fs.read(file)), missing: false }
14 } catch {
15 next = { file, columns: [], missing: true }
16 }
17 await update($, board, () => next)
18 $.ui.status(next.missing ? undefined : statusLine(next.columns))
19 return next
20}
21
22export const register: Register = (on, options) => {
23 const file = typeof options?.file === 'string' && options.file.trim() ? options.file.trim() : 'BOARD.md'
24
25 on('session.start', async ($, e, next) => {
26 await $.command.register({ name: 'board', description: `Show the Kanban board in ${file}` })
27 await refresh($, file)
28 return next(e)
29 })
30
31 on('turn.complete', async ($, e, next) => {
32 await refresh($, file)
33 return next(e)
34 })
35
36 on('command.run', { command: 'board' }, async $ => {
37 const now = await refresh($, file)
38 if (now.missing) return { text: `No ${file} in this folder. Set the "Board file" option to point at your board.` }
39 await $.ui.open({ id: PANE, title: 'Board' })
40 return { text: statusLine(now.columns) ?? 'Board opened.' }
41 })
42
43 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
44 const { Box, Text, Button } = $.ui.resolve(e)
45 const now = await read($, board)
46 if (!now || now.missing) return <Text dimColor>No board file found.</Text>
47 return (
48 <Box flexDirection="column">
49 {now.columns.map(column => (
50 <Box flexDirection="column">
51 <Text bold>
52 {column.name} ({column.open.length}{column.done ? `, ${column.done} done` : ''})
53 </Text>
54 {column.open.map(card => <Text>• {card}</Text>)}
55 <Text> </Text>
56 </Box>
57 ))}
58 <Button key="refresh" label="Refresh" onPress={() => refresh($, file)} />
59 </Box>
60 )
61 })
62}
63hooks/parse.ts 43 lines1import type { Column } from '../types'
2
3// Parses an Obsidian Kanban plugin board (or any markdown with ## columns and - [ ] cards).
4export function parseBoard(text: string): Column[] {
5 const columns: Column[] = []
6 let inSettings = false
7 for (const raw of text.split('\n')) {
8 const line = raw.trim()
9 // Obsidian comments (%% ... %%) hold the Kanban plugin's settings; skip them.
10 if (line.startsWith('%%')) {
11 const closesOnSameLine = line.length > 2 && line.endsWith('%%')
12 if (!closesOnSameLine) inSettings = !inSettings
13 continue
14 }
15 if (inSettings) continue
16 const heading = /^##\s+(.+)$/.exec(line)
17 if (heading) {
18 columns.push({ name: (heading[1] ?? '').trim(), open: [], done: 0 })
19 continue
20 }
21 const current = columns.at(-1)
22 if (!current) continue
23 const open = /^- \[ \]\s+(.+)$/.exec(line)
24 if (open) current.open.push(cleanCard(open[1] ?? ''))
25 else if (/^- \[[xX]\]\s+/.test(line)) current.done += 1
26 }
27 return columns
28}
29
30// [[Wiki links]] become plain text; long cards are shortened for one-line display.
31export function cleanCard(card: string): string {
32 const text = card.replace(/\[\[([^\]|]+)(\|([^\]]+))?\]\]/g, (_m, target: string, _p, alias?: string) => alias ?? target)
33 return text.length > 90 ? `${text.slice(0, 87)}...` : text
34}
35
36export function statusLine(columns: Column[]): string | undefined {
37 const busy = columns.filter(column => column.open.length > 0)
38 if (busy.length === 0) return columns.length ? 'Board: all clear' : undefined
39 const shown = busy.slice(0, 3).map(column => `${column.name} ${column.open.length}`)
40 const more = busy.length > 3 ? ` +${busy.length - 3}` : ''
41 return `Board · ${shown.join(' · ')}${more}`
42}
43types/index.d.ts 10 lines1export type Column = { name: string; open: string[]; done: number }
2
3export type BoardState = { file: string; columns: Column[]; missing: boolean }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'kanban-status': { board: BoardState | null }
8 }
9}
10