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

Zádori Olivér's Claude Code mods (function-hook plugins), as the zadoli-mods plugin marketplace (repo: zadoli/claude-mods).
| Mod | What it does |
|---|---|
prompt-cache-control | Cache 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-band | Desktop 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.
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.
version in plugins/<name>/.claude-plugin/plugin.json, so an update shows up as a new version.CHANGELOG.md.git tag -a <name>-v<version> -m "<name> <version>" (annotated: --follow-tags skips lightweight tags), and push with git push --follow-tags.claude plugin marketplace update zadoli-mods, then /reload-plugins.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.warnSeconds, compactAtTokens, band, toast (see the plugin's plugin.json).claude plugin validate plugins/prompt-cache-control
claude plugin test plugins/prompt-cache-controlhooks/register.tsx 116 lines1/**
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