SLOPSHOPPER

theme-sync

Applies the theme in ~/.claude/settings.json to running sessions as soon as the file changes

newtoasttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · theme-sync
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ theme-sync │ ⏺ Read(src/auth.ts) │ theme-sync: ENOENT: │ ⎿ Read 6 lines │ /Users/dev/.claude/settings.json │ ⏺ 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

kokko-claude-mods

Three Claude Code mods in one marketplace:

  • context-bar: a context-window bar above the prompt.
  • theme-sync: applies theme changes in ~/.claude/settings.json to running sessions.
  • memory-sync: keeps every project's auto memory in a private git repo, synced across machines.

context-bar

A band above the prompt: a context-window bar split by category, and how many tokens each category holds.

▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 42%  84k / 200k  effort high
● system 4k  ● tools 20k  ● memory 200  ● messages 59.8k
  • Colours are Gruvbox Material (medium contrast), one fixed colour per category; the percentage turns yellow at 50% and red at 80%. The light palette is used while the Claude Code theme is a light* one, dark otherwise, and the band switches as soon as the theme changes (/config, or theme-sync).
  • The thinking effort of the conversation's latest request (/effort, the settings' effortLevel, or the model's default) follows the token count.
  • Sized to the band's width: as the terminal narrows it drops the effort, then the token count, then the legend's smallest categories, then the legend, then the bar, and never wraps.

Install

At a Claude Code prompt in a terminal:

/plugin install context-bar --marketplace kokko-ng/kokko-claude-mods

Answer y to add the marketplace, then pick the user scope.

Or in ~/.claude/settings.json:

{
  "extraKnownMarketplaces": {
    "kokko-claude-mods": {
      "source": { "source": "github", "repo": "kokko-ng/kokko-claude-mods" }
    }
  },
  "enabledPlugins": { "context-bar@kokko-claude-mods": true }
}

Mods (function-hook plugins) are an early-access Claude Code feature; this one is built and tested against Claude Code 2.1.292.

theme-sync

Claude Code reloads ~/.claude/settings.json when it changes but keeps the running session's theme. theme-sync checks the file once a second and, when its theme differs from the session's, sets it as /config would, with a toast. Edit the file from anywhere (a script that flips light-ansi and dark-ansi alongside the terminal theme, for instance) and every open session follows.

/plugin install theme-sync --marketplace kokko-ng/kokko-claude-mods

or "theme-sync@kokko-claude-mods": true under enabledPlugins.

memory-sync

Keeps the auto memory of every project (~/.claude/projects/<project>/memory/) in a git repo, so each machine sees the others' memories.

  • The git work tree is ~/.claude/projects itself, with the git directory at ~/.claude/memory-sync.git; only each project's memory/ folder is tracked, never transcripts. Memories stay ordinary files where Claude Code reads and writes them, so nothing about recall or permissions changes.
  • Session start pulls (waiting up to 15 s, so the session loads the latest memories), every turn that changed a memory commits and pushes, and an idle session pulls every ten minutes.
  • The first run on a machine imports its memories: it restores what the repo has that the machine lacks and commits the rest on top.
  • Machines never clash. MEMORY.md indexes merge line by line (merge=union), so every machine's entries are kept. Any other memory both machines changed gets a normal three-way merge; if that conflicts, the version already pushed stays and this machine's is kept beside it as <name>.from-<host>.md (a git merge driver the mod installs), so nothing is lost or interleaved and the sync carries on. Reconcile those pairs when they appear. Only a memory deleted on one machine and changed on another stops the sync, with a toast saying how to resolve it.
  • Projects are matched by folder name, which Claude Code derives from the project's absolute path: use the same username and code folder on every machine for a project's memories to be shared.

The repo defaults to kokko-ng/claude-memory; set CLAUDE_MEMORY_REPO to owner/name (GitHub, using your git credentials) or any git URL. Create it private and empty first:

gh repo create <owner>/claude-memory --private
/plugin install memory-sync --marketplace kokko-ng/kokko-claude-mods

Mods do not run where disableAllHooks is set, and a settings.local.json in ~/.claude sets it for sessions started in your home folder.

Development

Run pre-commit install once per clone. It installs the pre-commit and commit-msg hooks: file hygiene, gitleaks, claude plugin validate --strict and claude plugin test for every mod (on a pinned Claude Code), and Conventional Commits messages checked by commitizen. CI runs the same hooks and checks every commit message.

Each mod is its own plugin under plugins/:

claude plugin validate plugins/context-bar
claude plugin test plugins/context-bar
claude plugin validate plugins/theme-sync
claude plugin test plugins/theme-sync
claude plugin validate plugins/memory-sync
claude plugin test plugins/memory-sync
Source 1 files
hooks/register.ts 66 lines
1import type { Register } from 'claude-code'
2
3// How often to look at settings.json. A stat is cheap; the file is only read
4// and parsed when its modification time moves.
5const POLL_MS = 1000
6
7// Claude Code reloads settings.json when it changes but does not re-apply
8// `theme` to a running session; this applies it through the /config row.
9export const register: Register = on => {
10  on('session.start', async ($, e, next) => {
11    const result = await next(e)
12
13    const dir =
14      (await $.env.get('CLAUDE_CONFIG_DIR')) ?? `${await $.env.get('HOME')}/.claude`
15    const path = `${dir}/settings.json`
16    let seenMtime = -1
17    let isBusy = false
18    let lastError = ''
19
20    const readTheme = async (): Promise<unknown> => {
21      try {
22        return (JSON.parse(await $.fs.read(path)) as { theme?: unknown }).theme
23      } catch {
24        return undefined // missing or half-written: the next change retries
25      }
26    }
27
28    const sync = async () => {
29      const { mtimeMs } = await $.fs.stat(path)
30      if (mtimeMs === seenMtime) return
31      seenMtime = mtimeMs
32
33      const theme = await readTheme()
34      if (typeof theme !== 'string') return
35
36      const row = (await $.config.list()).find(r => r.key === 'theme')
37      if (!row || row.isLocked || row.value === theme) return
38
39      const { deny } = await $.config.set({ key: 'theme', value: theme })
40      $.ui.toast(deny ? `theme-sync: ${deny}` : `Theme: ${theme}`)
41    }
42
43    const tick = () => {
44      if (isBusy) return
45      isBusy = true
46      sync()
47        .then(() => {
48          lastError = ''
49        })
50        .catch((err: unknown) => {
51          const text = err instanceof Error ? err.message : String(err)
52          if (text !== lastError) $.ui.toast(`theme-sync: ${text}`)
53          lastError = text
54        })
55        .finally(() => {
56          isBusy = false
57        })
58    }
59
60    tick()
61    $.clock.every(POLL_MS, tick)
62
63    return result
64  })
65}
66