Keeps a session's name and role across restarts and compactions

Claude Code mods for running orchestrator and worker sessions. Built and tested on Claude Code v2.1.289.
| Mod | What it does |
|---|---|
limit-resume | When the 5-hour limit resets, sends "Continue" to the session and nudges idle workers. A dim usage tail on the prompt hint line from 80%, /limits, and a one-line usage note for Claude each turn. |
workers | /workers opens a pane with every worker: state, branch, commits ahead of main, changed files, last message, and who needs you. /workers all shows every session. |
identity-keeper | Remembers the session name (from /rename or "You are <name>" in the first prompt), restores it after a restart, and gives Claude a short role card after each compaction. /identity, /identity forget. |
proof-gate | When a worker reports done without screenshots, asks it for them. When proof arrives, sends the files to you and shows a band above the prompt with Approve and Ask changes. |
effort-gate | Denies a subagent or claude --bg session that runs Opus at xhigh or max effort unless you said so: your own next prompt (terminal or Remote Control) must mention Opus and xhigh/max, e.g. "ok opus xhigh". The OK lasts until your next prompt. Sonnet, Haiku, and the main session are not gated. Also denies a claude --bg with no --model (or --agent that sets one), since it would silently run Opus, and an Opus one with no --effort (settings could raise it). It fails closed on claude --bg text it cannot read, so a message that merely mentions claude --bg is denied too. It only sees the literal command text: variables, aliases or functions, scripts written then run, and xargs get through. |
route-ledger | Records every subagent and claude --bg launch (model, effort, agent, label, never the prompt) and, for subagents (background ones too), ok/error, duration and tokens. A relaunch with the same label in the same session marks the earlier run retried or escalated. /routing-review [days] prints launches, outcomes, retries, escalations and median tokens and time per model@effort. Background sessions are launch-only: their outcome is not visible to the mod. |
config-sync | At session start (at most every 10 minutes per device) pulls ~/.claude/shared (the claude-config repo) with --ff-only. Tells you when the pull fails, when there are unpushed local changes, or when settings.shared.json changed; /config-sync apply then runs the installer, /config-sync shows the status. Skips private CLAUDE_CONFIG_DIR setups. |
savvy-progress | A progress bar above the prompt and /agents-info, a panel of every subagent with model, context, cost and time. scout, builder and reviewer get their own crabs: a ranger with binoculars, a builder with a hammer, a reviewer in a mortarboard with a clipboard. The bar's crab is the orchestrator, a conductor with a baton. Also lists related background sessions and draws the crabs as half-block text in the terminal. Copy of johnnyvizz/claude-kit (MIT). |
cache-tax | Keeps the one-hour prompt cache warm: every session starts an 8-hour keepwarm window that pings after 50 idle minutes, and a cold send shows its rewrite cost. Changed from upstream: keepwarm is always on for 8h, and the guard warns instead of dropping the message. /keepwarm off turns it off on this device. Pings stop after 3h idle and while weekly usage is 75% or more. Pings cost usage. Copy of karanb192/cache-tax (MIT). |
claude plugin marketplace add Berkay2002/berkays-mods
claude plugin install limit-resume@berkays-mods
claude plugin install workers@berkays-mods
claude plugin install identity-keeper@berkays-mods
claude plugin install proof-gate@berkays-mods
claude plugin install effort-gate@berkays-mods
claude plugin install route-ledger@berkays-mods
claude plugin install config-sync@berkays-mods
claude plugin install savvy-progress@berkays-mods
claude plugin install cache-tax@berkays-mods
Plugins carry no version, so each commit is a new version:
claude plugin marketplace update berkays-mods
claude plugin update workers@berkays-mods # and the others
Then run /reload-plugins in open sessions.
Work against the checkout, not the installed copy:
claude --plugin-dir plugins/workers
claude plugin validate plugins/workers
(cd plugins/workers && claude plugin test)
MIT
hooks/register.ts 171 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Identity } from '../types'
5
6// The name this process last applied with /rename. Survives hot reloads,
7// not restarts, so a fresh process renames once and a reload does not.
8const applied = atom({ plugin: 'identity-keeper', key: 'applied' } as const, null)
9
10// "You are brf-app-reading, a background worker ..." -> brf-app-reading.
11// Hyphenated names only, so "You are Claude Code" never matches.
12const ROLE = /\bYou are ([a-z0-9]+(?:-[a-z0-9]+)+)\b/i
13const ENVELOPE = /<cross-session-message from="([^"]+)"(?: from-name="([^"]+)")?/
14
15const FIRST_PROMPT_MAX = 1000
16const CARD_PROMPT_MAX = 600
17
18const keyFor = async ($: EngineInterface) => `id:${await $.session.id()}`
19
20const load = async ($: EngineInterface): Promise<Identity> =>
21 ((await $.store.get(await keyFor($))) as Identity | undefined) ?? {}
22
23const save = async ($: EngineInterface, patch: Partial<Identity>) => {
24 const key = await keyFor($)
25 const prev = ((await $.store.get(key)) as Identity | undefined) ?? {}
26 await $.store.set(key, { ...prev, ...patch })
27}
28
29export const roleCard = (id: Identity): string | null => {
30 if (!id.name) return null
31 const lines = [`[identity-keeper] You are ${id.name}. Keep this name and role after the compaction.`]
32 if (id.firstPrompt) {
33 const brief =
34 id.firstPrompt.length > CARD_PROMPT_MAX
35 ? `${id.firstPrompt.slice(0, CARD_PROMPT_MAX)}...`
36 : id.firstPrompt
37 lines.push(`Your original instructions: ${brief}`)
38 }
39 if (id.orchestrator) {
40 const who = id.orchestrator.name ? `${id.orchestrator.name} at ` : ''
41 lines.push(`Orchestrator: ${who}${id.orchestrator.address} (latest address it wrote from).`)
42 }
43 return lines.join('\n')
44}
45
46export const describe = (id: Identity): string => {
47 if (!id.name) return 'No name kept for this session'
48 const source = id.source === 'rename' ? '/rename' : 'first prompt'
49 const lines = [`Name: ${id.name} (from ${source})`]
50 if (id.orchestrator) {
51 const who = id.orchestrator.name ? `${id.orchestrator.name} · ` : ''
52 lines.push(`Orchestrator: ${who}${id.orchestrator.address}`)
53 }
54 lines.push('Role card: active after each compaction')
55 return lines.join('\n')
56}
57
58type Mode = 'set' | 'restore' | 'silent'
59
60// Runs /rename once the session is idle. $.command.run rejects inside a hook
61// the turn waits on, so it goes out on a timer, outside the dispatch.
62// `silent` re-applies a name that most likely never changed, so no toast.
63const rename = ($: EngineInterface, name: string, mode: Mode) => {
64 $.clock.after(0, () => {
65 $.command
66 .run({ command: 'rename', args: name })
67 .then(async () => {
68 await update($, applied, () => name)
69 if (mode === 'set') $.ui.toast(`Renamed to ${name}`)
70 if (mode === 'restore') $.ui.toast(`Name restored · ${name}`)
71 })
72 .catch(async () => {
73 const { isFilled } = await $.prompt.fill({ text: `/rename ${name}` })
74 if (!isFilled) return
75 $.ui.toast(mode === 'set' ? `Press Enter to rename to ${name}` : `Press Enter to restore name ${name}`)
76 })
77 })
78}
79
80export const register: Register = on => {
81 on('session.start', async ($, e, next) => {
82 const result = await next(e)
83 await $.command.register({
84 name: 'identity',
85 description: "Show this session's kept name and role card, or forget them",
86 argumentHint: '[forget]',
87 })
88 const { name } = await load($)
89 if (name && (await read($, applied)) !== name) rename($, name, 'restore')
90 return result
91 })
92
93 on('command.run', { command: 'identity' }, async ($, e) => {
94 const arg = e.args.trim().toLowerCase()
95 if (arg === 'forget') {
96 const id = await load($)
97 if (!id.name && !id.firstPrompt && !id.orchestrator) return { text: 'Nothing kept for this session' }
98 await $.store.delete(await keyFor($))
99 await update($, applied, () => null)
100 return { text: 'Forgot the name and role card for this session' }
101 }
102 if (arg !== '') return { text: 'Usage: /identity or /identity forget' }
103 return { text: describe(await load($)) }
104 })
105
106 // The person's own /rename: remember it. Our own run never reaches here.
107 on('command.run', { command: 'rename' }, async ($, e, next) => {
108 const result = await next(e)
109 const name = e.args.trim()
110 if (name) {
111 await save($, { name, source: 'rename' })
112 await update($, applied, () => name)
113 }
114 return result
115 })
116
117 on('prompt.submit', async ($, e, next) => {
118 const kind = e.origin.kind
119 if (kind === 'plugin' || kind === 'task-notification') return next(e)
120
121 const id = await load($)
122 if (id.firstPrompt === undefined) {
123 const patch: Partial<Identity> = { firstPrompt: e.text.slice(0, FIRST_PROMPT_MAX) }
124 const match = id.name ? null : ROLE.exec(e.text.slice(0, 300))
125 if (match?.[1]) {
126 patch.name = match[1]
127 patch.source = 'first-prompt'
128 }
129 await save($, patch)
130 if (patch.name) rename($, patch.name, 'set')
131 }
132 return next(e)
133 })
134
135 on('session.receive', async ($, e, next) => {
136 if (e.agentId === undefined) {
137 const match = ENVELOPE.exec(e.text)
138 if (match?.[1]) {
139 await save($, { orchestrator: { address: match[1], name: match[2] } })
140 }
141 }
142 return next(e)
143 })
144
145 on('session.compact', async ($, e, next) => {
146 const result = await next(e)
147 if (e.trigger === 'precompute' || e.agentId !== undefined || result.skip !== undefined) {
148 return result
149 }
150
151 const id = await load($)
152 const card = roleCard(id)
153 if (!card || !id.name) return result
154 const name = id.name
155
156 // After the compaction is in place: the card as a user row the person does
157 // not see as typed, then the name again in case the compaction dropped it.
158 $.clock.after(0, () => {
159 $.session
160 .append({ message: { type: 'user', content: [{ type: 'text', text: card }] } })
161 .then(appended => {
162 if (appended.deny === undefined) $.ui.toast('Role card restored after compaction')
163 })
164 .catch(() => undefined)
165 rename($, name, 'silent')
166 })
167
168 return result
169 })
170}
171types/index.d.ts 13 lines1export type Identity = {
2 name?: string
3 source?: 'rename' | 'first-prompt'
4 firstPrompt?: string
5 orchestrator?: { address: string; name?: string }
6}
7
8declare module 'claude-code' {
9 interface PluginState {
10 'identity-keeper': { applied: string | null }
11 }
12}
13