SLOPSHOPPER

sidebar

Pane that shows git-status above tool-calls

newpanecommand
v0.1.0no licenseupdated 2026-10-12klefix/claude-code-mods/sidebar
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · sidebar
│ ┃ Sidebar ✕ › fix the failing auth test and add an audit log call │ ┃ Git │ ┃ Waiting for git-status ⏺ Read(src/auth.ts) │ ┃ ──────────────────────────────────────────── ⎿ Read 6 lines │ ┃ ──────────── ⏺ Update(src/auth.ts) │ ┃ Tools ⎿ Added 2 lines, removed 1 line │ ┃ No tool calls yet ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /sidebar │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Sidebar
Git Waiting for git-status ──────────────────────────────────────────────────────── Tools No tool calls yet
README

claude-code-mods

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.

The mods

ModWhat it does
git-statusRuns 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-callsCounts how often each tool and each skill was used in the session. /clear resets the counts.
sidebarA 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-guardAsks Jev, a classifier model on the Vercel AI Gateway, two kinds of questions. See below.

jev-guard

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:

  • It sends your prompt, recent assistant text and the tool call input to https://ai-gateway.vercel.sh. For a Write call it also sends the current content of the file being overwritten.
  • It fails closed. If Jev cannot be reached or takes longer than 5 seconds, it drops the prompt or denies the tool call. Each block is reported in the transcript.
  • It writes the last request it sent to 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.

Setup

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.

Development

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.

Source 2 files
hooks/register.ts 77 lines
1import 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}
77
types/index.d.ts 12 lines
1// 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