SLOPSHOPPER

context-band

A row above the prompt with the working directory, its git branch and the skill loaded since your last message.

newbandguardprompt
v0.2.0MITupdated 2026-10-08zadoli/claude-mods/plugins/context-band
A shopper browsing a rack in a slop shop
README

zadoli-mods

Zádori Olivér's Claude Code mods (function-hook plugins), as the zadoli-mods plugin marketplace (repo: zadoli/claude-mods).

ModWhat it does
prompt-cache-controlCache meter above the prompt (hit rate, countdown, keepwarm, cold price) with a details button that opens a pane; /keepwarm pings the cache after idle stretches; a message to a cold cache of 50k+ tokens is stopped once with its price.
context-bandDesktop app only: a row above the prompt with the working directory, git branch and the skill loaded since your last message.

Needs Claude Code 2.1.287 or newer.

Install on a machine

claude plugin marketplace add zadoli/claude-mods
claude plugin install prompt-cache-control@zadoli-mods
claude plugin install context-band@zadoli-mods

Then /reload-plugins in a running session (or start a new one). The repo is private: the machine needs gh auth login (or git credentials) for GitHub first.

Update later with claude plugin marketplace update zadoli-mods.

Releasing

  1. Bump version in plugins/<name>/.claude-plugin/plugin.json, so an update shows up as a new version.
  2. Add the entry to CHANGELOG.md.
  3. Commit, tag with git tag -a <name>-v<version> -m "<name> <version>" (annotated: --follow-tags skips lightweight tags), and push with git push --follow-tags.
  4. On each machine: claude plugin marketplace update zadoli-mods, then /reload-plugins.

Options

In ~/.claude/settings.json (the key is <plugin>@zadoli-mods when installed from this marketplace):

"pluginConfigs": {
  "prompt-cache-control@zadoli-mods": {
    "options": { "ttl": "1h", "breakdown": false, "status": false, "guard": "refuse" }
  }
}
  • ttl: auto | 5m | 1h. Pin 1h behind a proxy (e.g. ANTHROPIC_BASE_URL to a local proxy) that drops the rate-limit headers the subscription is detected from.
  • breakdown: read / wrote / new in the band.
  • status: a short entry under the prompt.
  • autoKeepwarm: start a 6-hour keepwarm window with each message when none is running (off by default; /keepwarm off keeps it off for the session).
  • limit: the 5-hour plan window under the prompt, 5h 42% · reset 2h13m (on by default; only on a Claude subscription).
  • guard: refuse (stop a cold send once), warn (send and log the price), off.
  • also warnSeconds, compactAtTokens, band, toast (see the plugin's plugin.json).

Develop

claude plugin validate plugins/prompt-cache-control
claude plugin test plugins/prompt-cache-control
Source 1 files
hooks/register.tsx 116 lines
1/**
2 * context-band — a row above the prompt on the desktop app: working directory · git branch · skill.
3 *
4 *   - the branch is read from .git/HEAD (worktrees: the `gitdir:` file), no git process
5 *   - the skill is the last Skill tool call since your last message; a skill run as a
6 *     slash command does not go through that tool and is not shown
7 *   - refreshed at session start, on each prompt, after each tool call and turn
8 */
9import type { EngineInterface, Register } from 'claude-code'
10
11let cwd = ''
12let branch: string | undefined
13let skill: string | undefined
14
15const norm = (p: string) => {
16  let s = p.split('\\').join('/')
17  while (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1)
18  return s
19}
20const isAbs = (p: string) => p.startsWith('/') || p.charAt(1) === ':'
21
22/** the branch of the repo holding `dir`, a short sha when detached, undefined outside a repo */
23export type Fs = { exists: (p: string) => Promise<boolean>; read: (p: string) => Promise<string> }
24
25export async function branchOf(fs: Fs, dir: string): Promise<string | undefined> {
26  for (let d = norm(dir); d; ) {
27    const dotgit = `${d}/.git`
28    if (await fs.exists(dotgit)) {
29      let head: string
30      try {
31        head = await fs.read(`${dotgit}/HEAD`)
32      } catch {
33        // a worktree or submodule: .git is a file naming the real git dir
34        const m = /^gitdir:\s*(.+)$/m.exec(await fs.read(dotgit))
35        if (!m) return undefined
36        const gd = norm(m[1].trim())
37        head = await fs.read(`${isAbs(gd) ? gd : `${d}/${gd}`}/HEAD`)
38      }
39      const line = head.trim()
40      const prefix = 'ref: refs/heads/'
41      return line.startsWith(prefix) ? line.slice(prefix.length) : line.slice(0, 7)
42    }
43    const cut = d.lastIndexOf('/')
44    const up = cut > 0 ? d.slice(0, cut) : ''
45    if (!up || up === d || up.endsWith(':')) return undefined
46    d = up
47  }
48  return undefined
49}
50
51async function refresh($: EngineInterface) {
52  try {
53    const now = await $.session.cwd()
54    const b = await branchOf({ exists: p => $.fs.exists(p), read: p => $.fs.read(p) }, now).catch(() => undefined)
55    if (now !== cwd || b !== branch) {
56      cwd = now
57      branch = b
58      $.ui.invalidate('ui.render')
59    }
60  } catch (err) {
61    $.ui.log(`context-band: ${err}`, { to: 'debug' })
62  }
63}
64
65export const register: Register = on => {
66  on('session.start', async ($, e, next) => {
67    const r = await next(e)
68    skill = undefined
69    await refresh($)
70    return r
71  })
72
73  on('prompt.submit', async ($, e, next) => {
74    if (e.origin.kind !== 'plugin') {
75      skill = undefined
76      $.ui.invalidate('ui.render')
77      await refresh($)
78    }
79    return next(e)
80  })
81
82  on('tool.call', async ($, e, next) => {
83    if (e.tool === 'Skill' && typeof (e as { skill?: unknown }).skill === 'string') {
84      skill = (e as { skill: string }).skill
85      $.ui.invalidate('ui.render')
86    }
87    const r = await next(e)
88    await refresh($)
89    return r
90  })
91
92  on('turn.complete', async ($, e, next) => {
93    const r = await next(e)
94    await refresh($)
95    return r
96  })
97
98  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
99    const rest = await next(e)
100    // the terminal's status line already shows these
101    if (e.surface !== 'desktop' || e.props.hasSurvey || !cwd) return rest
102    const { Box, Text } = $.ui.resolve(e)
103    return (
104      <Box flexDirection="column">
105        {rest}
106        {/* a gap under the rows above (the cache band), only when there are any */}
107        <Box flexDirection="row" columnGap={2} marginTop={rest ? 1 : 0}>
108          <Text color="blue" wrap="truncate-start">{`📁 ${cwd}`}</Text>
109          {branch ? <Text color="green">{`⎇ ${branch}`}</Text> : null}
110          {skill ? <Text color="yellow">{`⚡ ${skill}`}</Text> : null}
111        </Box>
112      </Box>
113    )
114  })
115}
116