Pane that shows git-status above tool-calls

A few Claude Code mods. A mod is a plugin with a TypeScript or JavaScript hooks module that Claude Code runs on events such as tool calls, prompts and rendering. The mods need Claude Code v2.1.287 or later.
| Mod | What it does |
|---|---|
git-status | Runs git status --porcelain at session start, after each prompt, after file-changing tool calls and when the working directory changes. It keeps the changed, staged and unstaged file counts in session state. |
tool-calls | Counts how often each tool and each skill was used in the session. /clear resets the counts. |
sidebar | A pane that shows the git-status counts above the tool-calls counts. It opens at session start in terminals at least 144 columns wide. /sidebar opens it at any width. It needs git-status and tool-calls to be loaded. |
jev-guard | Asks Jev, a classifier model on the Vercel AI Gateway, two kinds of questions. See below. |
On each prompt you type, jev-guard asks Jev whether the request leaves a choice open, such as which fix or what scope. If it does, and the prompt is not a question, a dialog asks you to clarify, send the prompt as written, or cancel it.
Before each Edit, Write, MultiEdit, NotebookEdit and Bash call, jev-guard asks Jev whether the call stays within what you approved and whether it removes behavior you did not ask to remove. It denies the call if the in-scope rating is below 0.5 or the removes-behavior rating is 0.7 or more. Claude then sees the reason and is told to ask you. A Bash command that Jev rates as read-only is allowed without those checks. The thresholds are at the top of jev-guard/hooks/judge.ts.
Things to know before you enable jev-guard:
https://ai-gateway.vercel.sh. For a Write call it also sends the current content of the file being overwritten.jev-guard/.debug/last-request.json. That folder is git-ignored.It needs a Vercel AI Gateway API key. Run jev-guard/set-key.sh, which saves the key to ~/.config/jev-guard/key with permissions 600.
Clone the repo:
git clone https://github.com/klefix/claude-code-mods.git ~/claude-code-mods
To load mods in every session, add their directories to CLAUDE_CODE_PLUGIN_DIRS in ~/.claude/settings.json. The value is a list of absolute paths separated by :. Use the path you cloned to:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "/Users/you/claude-code-mods/git-status:/Users/you/claude-code-mods/tool-calls:/Users/you/claude-code-mods/sidebar"
}
}
Add /Users/you/claude-code-mods/jev-guard to the list if you want jev-guard, after you have set the key.
To try one mod for a single session, run claude --plugin-dir ./sidebar from the repo. Claude Code reloads the mod when you save a file.
To check that a mod is loaded, open /plugin and look at the Installed tab.
npm install
npm run lint # Biome lint and format check
npm run lint:fix # apply the fixes
claude plugin validate ./jev-guard
claude plugin test jev-guard
Claude Code generates .claude-plugin/types/ and tsconfig.json in each mod directory when it loads the mod. Both are git-ignored.
hooks/register.ts 77 lines1import type { ElementConstructor, On, PluginState, TextProps } from 'claude-code'
2
3const PANE = 'sidebar'
4
5function gitLines(Text: ElementConstructor<TextProps>, summary: PluginState['git-status']['summary'] | undefined) {
6 if (!summary) {
7 return [Text({ dimColor: true, children: ['Waiting for git-status'] })]
8 }
9 if (!summary.isRepo) {
10 return [Text({ dimColor: true, children: ['Not a git repository'] })]
11 }
12 return [
13 Text({ children: [`Changed: ${summary.changed}`] }),
14 Text({ children: [`Staged: ${summary.staged}`] }),
15 Text({ children: [`Unstaged: ${summary.unstaged}`] }),
16 ]
17}
18
19function toolLines(Text: ElementConstructor<TextProps>, counts: PluginState['tool-calls']['counts'] | undefined) {
20 const rows = Object.entries(counts?.tools ?? {}).sort((a, b) => b[1] - a[1])
21 if (rows.length === 0) {
22 return [Text({ dimColor: true, children: ['No tool calls yet'] })]
23 }
24 const lines = []
25 for (const [tool, n] of rows) {
26 if (tool !== 'Skill') {
27 lines.push(Text({ children: [`${tool}: ${n}x`] }))
28 continue
29 }
30 lines.push(Text({ children: ['Skill:'] }))
31 const skillRows = Object.entries(counts?.skills ?? {}).sort((a, b) => b[1] - a[1])
32 skillRows.forEach(([skill, m], i) => {
33 const branch = i === skillRows.length - 1 ? '└ ' : '├ '
34 lines.push(Text({ children: [`${branch + skill}: ${m}x`] }))
35 })
36 }
37 return lines
38}
39
40export function register(on: On) {
41 on('session.start', async ($, e, next) => {
42 await $.command.register({ name: 'sidebar', description: 'Open the sidebar pane', immediate: true })
43 // Opened without user action, so it only shows in terminals 144+ columns wide; /sidebar opens it at any width
44 await $.ui.open({ id: PANE, title: 'Sidebar' })
45 return next(e)
46 })
47
48 on('command.run', { command: 'sidebar' }, async ($) => {
49 await $.ui.open({ id: PANE, title: 'Sidebar' })
50 return {}
51 })
52
53 // A $.state.get during ui.render subscribes the pane, so writes by the other mods redraw it
54 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
55 if (e.requestId !== PANE) {
56 return next(e)
57 }
58 const { Box, Text } = $.ui.resolve(e)
59 const { value: summary } = await $.state.get({ plugin: 'git-status', key: 'summary' })
60 const { value: counts } = await $.state.get({ plugin: 'tool-calls', key: 'counts' })
61 return Box({
62 flexDirection: 'column',
63 children: [
64 Box({
65 flexDirection: 'column',
66 children: [Text({ bold: true, children: ['Git'] }), ...gitLines(Text, summary)],
67 }),
68 Text({ dimColor: true, children: ['─'.repeat(e.props?.bodyColumns ?? 20)] }),
69 Box({
70 flexDirection: 'column',
71 children: [Text({ bold: true, children: ['Tools'] }), ...toolLines(Text, counts)],
72 }),
73 ],
74 })
75 })
76}
77types/index.d.ts 12 lines1// Copies of the contracts declared by the tool-calls and git-status mods
2declare module 'claude-code' {
3 interface PluginState {
4 'tool-calls': {
5 counts: { tools: Record<string, number>; skills: Record<string, number> }
6 }
7 'git-status': {
8 summary: { isRepo: boolean; changed: number; staged: number; unstaged: number }
9 }
10 }
11}
12