Keeps changed, staged and unstaged file counts from git status in session state

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 37 lines1import type { EngineInterface, On } from 'claude-code'
2import { parsePorcelain } from './parse.js'
3
4const NOT_A_REPO = { isRepo: false, changed: 0, staged: 0, unstaged: 0 }
5const CHANGES_FILES = ['Bash', 'Edit', 'Write', 'MultiEdit', 'NotebookEdit']
6
7async function refresh($: EngineInterface) {
8 const { exitCode, stdout } = await $.process.run(['git', 'status', '--porcelain'])
9 const summary = exitCode === 0 ? parsePorcelain(stdout) : NOT_A_REPO
10 await $.state.set({ plugin: 'git-status', key: 'summary' }, summary)
11}
12
13export function register(on: On) {
14 on('session.start', async ($, e, next) => {
15 await refresh($)
16 return next(e)
17 }).catch((_$, e, next) => next(e))
18
19 on('tool.call', async ($, e, next) => {
20 const result = await next(e)
21 if (CHANGES_FILES.includes(e.tool)) {
22 refresh($).catch(() => {})
23 }
24 return result
25 }).catch((_$, e, next) => next(e))
26
27 on('prompt.submit', async ($, e, next) => {
28 refresh($).catch(() => {})
29 return next(e)
30 }).catch((_$, e, next) => next(e))
31
32 on('classic.CwdChanged', async ($, e, next) => {
33 refresh($).catch(() => {})
34 return next(e)
35 }).catch((_$, e, next) => next(e))
36}
37hooks/parse.ts 18 lines1// Untracked files ("??") count as unstaged
2export function parsePorcelain(stdout: string) {
3 const lines = stdout.split('\n').filter((line) => line.length > 0)
4 let staged = 0
5 let unstaged = 0
6 for (const line of lines) {
7 const x = line[0]
8 const y = line[1]
9 if (x !== ' ' && x !== '?') {
10 staged++
11 }
12 if (y !== ' ') {
13 unstaged++
14 }
15 }
16 return { isRepo: true, changed: lines.length, staged, unstaged }
17}
18types/index.d.ts 8 lines1declare module 'claude-code' {
2 interface PluginState {
3 'git-status': {
4 summary: { isRepo: boolean; changed: number; staged: number; unstaged: number }
5 }
6 }
7}
8