SLOPSHOPPER

config-sync

Pulls the shared claude-config repo at session start, tells you when settings.shared.json changed, and applies it with /config-sync apply

newcommandtoastpromptprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · config-sync
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ 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 › /config-sync ⎿ config-sync: config-sync: ~/.claude/shared is not a git checkout here, or CLAUDE_CONFIG_DIR is set to a private config. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

berkays-mods

Claude Code mods for running orchestrator and worker sessions. Built and tested on Claude Code v2.1.289.

ModWhat it does
limit-resumeWhen 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-keeperRemembers 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-gateWhen 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-gateDenies 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-ledgerRecords 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-syncAt 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-progressA 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-taxKeeps 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).

Install

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

Update

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.

Develop

Work against the checkout, not the installed copy:

claude --plugin-dir plugins/workers
claude plugin validate plugins/workers
(cd plugins/workers && claude plugin test)

License

MIT

Source 1 files
hooks/register.ts 185 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3const GAP_MS = 10 * 60_000
4const DAY_MS = 24 * 60 * 60_000
5const STALE_LOCK_MS = 2 * 60_000 // a git lock older than this is not a concurrent session
6const LAST_PULL = 'last-pull' // { at: ms, error: string }: when we last pulled and what it said
7const NOTIFIED = 'notified' // Notified: what the last notices were about, so an unchanged state stays quiet
8
9type Notified = { settings: string; local: boolean; pull: string; at: number } // at: when a notice last went out
10
11// Set by a failed pull at session start; handed to Claude with the first prompt's context.
12let note: string | undefined
13
14type Run = { exitCode: number; stdout: string; stderr: string }
15
16const slash = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, '')
17const firstLine = (r: Run) =>
18  ((r.stderr || r.stdout).split(/\r?\n/).find(l => l.trim()) ?? `exit ${r.exitCode}`).trim()
19// A concurrent session is pulling or committing: not worth telling anyone. A lock left behind for minutes is a real failure.
20async function isLockRace($: EngineInterface, w: Where, r: Run, now: number) {
21  if (!/index\.lock|another git process/i.test(r.stderr + r.stdout)) return false
22  // Ask git where the lock is: in a worktree `.git` is a file and the lock lives elsewhere.
23  const where = await run($, ['git', '-C', w.shared, 'rev-parse', '--path-format=absolute', '--git-path', 'index.lock'], 5000)
24  const lock = where.stdout.trim().split('\n')[0].trim()
25  if (where.exitCode !== 0 || !lock) return false // cannot check: report it rather than hide it
26  try {
27    const { mtimeMs } = await $.fs.stat(lock)
28    return now - mtimeMs < STALE_LOCK_MS
29  } catch {
30    return true // gone already: the other git finished
31  }
32}
33
34function tell($: EngineInterface, text: string, forClaude = false) {
35  $.ui.log(text)
36  $.ui.toast(text)
37  if (forClaude) note = text
38}
39
40// A timeout rejects; report it like a failed exit.
41async function run($: EngineInterface, argv: string[], timeoutMs: number, env?: Record<string, string>): Promise<Run> {
42  try {
43    return await $.process.run(argv, { timeoutMs, env })
44  } catch (err) {
45    return { exitCode: 1, stdout: '', stderr: err instanceof Error ? err.message : String(err) }
46  }
47}
48
49type Where = { claudeDir: string; shared: string; isWindows: boolean }
50
51// undefined: not this device's shared config (a private CLAUDE_CONFIG_DIR, or no checkout).
52async function locate($: EngineInterface): Promise<Where | undefined> {
53  const isWindows = (await $.env.get('OS')) === 'Windows_NT'
54  // `||`: an empty variable falls through. On Windows a corporate HOME can be a network drive; USERPROFILE is the real one.
55  const home = slash(
56    isWindows
57      ? (await $.env.get('USERPROFILE')) || (await $.env.get('HOME')) || ''
58      : (await $.env.get('HOME')) || (await $.env.get('USERPROFILE')) || '',
59  )
60  const claudeDir = `${home}/.claude`
61  const shared = `${claudeDir}/shared`
62  // Hindsight and other private configs set CLAUDE_CONFIG_DIR; they are not this device's shared config.
63  const cfg = await $.env.get('CLAUDE_CONFIG_DIR')
64  if (cfg && slash(cfg).toLowerCase() !== claudeDir.toLowerCase()) return
65  if (!(await $.fs.exists(`${shared}/.git`))) return
66  return { claudeDir, shared, isWindows }
67}
68
69// The installer records this same hash after it has applied settings.shared.json.
70async function settingsState($: EngineInterface, w: Where) {
71  const hash = await run($, ['git', '-C', w.shared, 'hash-object', 'settings.shared.json'], 5000)
72  if (hash.exitCode !== 0) return { stale: false, hash: '', installed: '' }
73  const current = hash.stdout.trim()
74  const installed = await $.fs.read(`${w.claudeDir}/claude-config.installed`).then(
75    t => String(t).trim(),
76    () => '',
77  )
78  return { stale: installed !== current, hash: current, installed }
79}
80
81async function localChanges($: EngineInterface, w: Where) {
82  const git = (...args: string[]) => run($, ['git', '-C', w.shared, ...args], 5000)
83  const dirty = (await git('status', '--porcelain')).stdout.trim() !== ''
84  const ahead = Number((await git('rev-list', '--count', '@{u}..HEAD')).stdout.trim()) || 0 // no upstream: errors, reads 0
85  return { dirty, ahead }
86}
87
88async function sync($: EngineInterface) {
89  const w = await locate($)
90  if (!w) return
91
92  // Read-then-write: two sessions starting together may both pull; a pull is idempotent.
93  const now = await $.clock.now()
94  const last = (await $.store.get(LAST_PULL)) as { at?: number } | undefined
95  if (typeof last?.at === 'number' && now - last.at < GAP_MS) return
96
97  const prev = (((await $.store.get(NOTIFIED)) as Partial<Notified> | undefined) ?? {}) as Partial<Notified>
98  const pull = await run($, ['git', '-C', w.shared, 'pull', '--ff-only', '-q'], 15_000, {
99    GIT_TERMINAL_PROMPT: '0',
100    GCM_INTERACTIVE: 'never',
101  })
102  const raced = pull.exitCode !== 0 && (await isLockRace($, w, pull, now))
103  const pullError = raced ? (prev.pull ?? '') : pull.exitCode !== 0 ? firstLine(pull) : ''
104  await $.store.set(LAST_PULL, { at: now, error: pullError })
105
106  const { dirty, ahead } = await localChanges($, w)
107  const s = await settingsState($, w)
108  const state = { settings: s.stale ? s.hash : '', local: dirty || ahead > 0, pull: pullError }
109
110  // A new or changed state is announced at once; an unchanged bad one again after a day.
111  const again = now - (prev.at ?? 0) > DAY_MS
112  let told = false
113  if (state.pull && (state.pull !== prev.pull || again)) {
114    tell($, `claude-config pull failed: ${state.pull}`, true)
115    told = true
116  }
117  if (state.local && (!prev.local || again)) {
118    const host = (await $.env.get('COMPUTERNAME')) || (await $.env.get('HOSTNAME')) || 'this device'
119    tell($, `claude-config has local changes on ${host}; push them`)
120    told = true
121  }
122  if (state.settings && (state.settings !== prev.settings || again)) {
123    tell($, 'Shared settings changed: run /config-sync apply')
124    told = true
125  }
126  await $.store.set(NOTIFIED, { ...state, at: told ? now : (prev.at ?? now) })
127}
128
129async function apply($: EngineInterface) {
130  const w = await locate($)
131  if (!w) return 'config-sync: ~/.claude/shared is not a git checkout here, or CLAUDE_CONFIG_DIR is set to a private config; nothing to apply.'
132  const argv = w.isWindows
133    ? ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', `${w.shared}/install.ps1`]
134    : ['bash', `${w.shared}/install.sh`]
135  const r = await run($, argv, 300_000) // plugin installs can be slow
136  return r.exitCode === 0
137    ? 'Applied new shared settings (backup in ~/.claude/backups)'
138    : `claude-config install failed: ${firstLine(r)}`
139}
140
141async function status($: EngineInterface) {
142  const w = await locate($)
143  if (!w) return 'config-sync: ~/.claude/shared is not a git checkout here, or CLAUDE_CONFIG_DIR is set to a private config.'
144  const last = (await $.store.get(LAST_PULL)) as { at?: number; error?: string } | undefined
145  const pull =
146    typeof last?.at !== 'number'
147      ? 'not pulled yet'
148      : `${Math.round(((await $.clock.now()) - last.at) / 60_000)} min ago, ${last.error ? `failed: ${last.error}` : 'ok'}`
149  const s = await settingsState($, w)
150  const { dirty, ahead } = await localChanges($, w)
151  const local = [dirty && 'uncommitted changes', ahead > 0 && `${ahead} unpushed commit${ahead === 1 ? '' : 's'}`].filter(Boolean)
152  return [
153    `Last pull: ${pull}`,
154    `Settings: ${s.hash ? (s.stale ? `changed (installed ${s.installed || 'none'}, current ${s.hash}); run /config-sync apply` : `up to date (${s.hash})`) : 'cannot hash settings.shared.json'}`,
155    `Local: ${local.length ? local.join(', ') : 'clean'}`,
156  ].join('\n')
157}
158
159export const register: Register = on => {
160  on('session.start', async ($, e, next) => {
161    note = undefined
162    const result = await next(e)
163    await $.command.register({
164      name: 'config-sync',
165      description: 'Show the claude-config sync status, or apply new shared settings',
166      argumentHint: '[apply]',
167    })
168    // Off the critical path: a slow or offline pull must not hold up the session starting.
169    void sync($).catch(err => tell($, `claude-config sync failed: ${err instanceof Error ? err.message : String(err)}`))
170    return result
171  })
172
173  on('command.run', { command: 'config-sync' }, async ($, e) => {
174    const arg = e.args.trim().toLowerCase()
175    if (arg === 'apply') return { text: await apply($) }
176    if (arg === '') return { text: await status($) }
177    return { text: 'Usage: /config-sync or /config-sync apply' }
178  })
179
180  on('prompt.context', async ($, e, next) => {
181    const r = await next(e)
182    return note ? { ...r, blocks: [...r.blocks, { name: 'configSync', text: note }] } : r
183  })
184}
185