SLOPSHOPPER

project-colors

Every project gets its own prompt-bar colour and session name, set when a session starts. It learns from your own /color and /rename.

newcommandtimer
v0.1.0no licenseupdated 2026-10-08RenegadeRocks/claude-mods/project-colors
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · project-colors
› 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 › /project-colors ⎿ project-colors: app: colour blue, name app (from the folder). Use /color or /rename to change them; this project remembers. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

project-colors

Each project folder keeps its own prompt-bar colour and a session name, so you can tell your Claude Code windows apart at a glance.

  • A new project gets a colour picked from its folder name, the same one every time.
  • A new session is named after the project, using the name you gave it before or else the folder's. A resumed or already-named session keeps its name.
  • Your own choices win. Type /color pink or /rename Fairy mocap and that project remembers it from then on.

Commands

CommandWhat it does
/project-colorsShows this project's colour and name
/project-colors resetForgets them; the next session picks fresh ones
/project-colors offStops colouring and naming, in every session until you turn it back on; the bar goes back to the default colour
/project-colors onTurns it back on, with your saved colours and names

Each session starts with a short "Session color set to: …" line. That's this mod at work.

Source 1 files
hooks/register.ts 163 lines
1// project-colors: each project folder keeps its own prompt-bar colour and a
2// session name. A new folder gets a colour picked from its name; type /color
3// or /rename yourself and that project remembers your choice from then on.
4
5import type { EngineInterface, Register } from 'claude-code'
6
7type Project = { color?: string; name?: string }
8type Projects = Record<string, Project>
9
10// what /color took as of Oct 2026; replaced by the list /color prints if it refuses one
11const DEFAULT_COLORS = ['red', 'blue', 'green', 'yellow', 'purple', 'orange', 'pink', 'cyan']
12
13function keyOf(root: string): string {
14  return root.replace(/[\\/]+$/, '').toLowerCase()
15}
16
17function folderName(root: string): string {
18  return root.split(/[\\/]/).filter(Boolean).pop() ?? root
19}
20
21/** The same folder name always lands on the same colour. */
22function pickColor(name: string, colors: string[]): string {
23  let hash = 0
24  for (const ch of name.toLowerCase()) hash = (Math.imul(hash, 31) + ch.charCodeAt(0)) >>> 0
25  return colors[hash % colors.length] ?? 'blue'
26}
27
28async function loadProjects($: EngineInterface): Promise<Projects> {
29  const saved = await $.store.get('projects')
30  return saved && typeof saved === 'object' ? (saved as Projects) : {}
31}
32
33async function thisProject($: EngineInterface): Promise<{ key: string; folder: string; projects: Projects }> {
34  const root = await $.session.root()
35  return { key: keyOf(root), folder: folderName(root), projects: await loadProjects($) }
36}
37
38async function remember($: EngineInterface, change: Project) {
39  try {
40    const { key, projects } = await thisProject($)
41    await $.store.set('projects', { ...projects, [key]: { ...projects[key], ...change } })
42  } catch {
43    // forgetting a choice is harmless
44  }
45}
46
47/** Whether `/project-colors off` is in force; read from the store each time, so it holds across sessions. */
48async function isOff($: EngineInterface): Promise<boolean> {
49  return (await $.store.get('isOn')) === false
50}
51
52/** The colours /color takes: the list it last printed, else the known ones. */
53async function knownColors($: EngineInterface): Promise<string[]> {
54  const saved = await $.store.get('colors')
55  return Array.isArray(saved) && saved.length > 0 ? (saved as string[]) : DEFAULT_COLORS
56}
57
58/**
59 * Sets the prompt bar to this project's colour, choosing and saving one for a
60 * new project. A project you set to `default` yourself stays plain.
61 */
62async function applyColor($: EngineInterface) {
63  try {
64    if (await isOff($)) return
65    const { key, folder, projects } = await thisProject($)
66    if (projects[key]?.color === 'default') return
67    const colors = await knownColors($)
68    let color = projects[key]?.color ?? pickColor(folder, colors)
69    const { text } = await $.command.run({ command: 'color', args: color } as never)
70    // a colour /color no longer takes: learn the real list from its answer and pick again
71    const listed = /Available colors:\s*(.+)$/im.exec(text ?? '')
72    if (listed?.[1]) {
73      const valid = listed[1]
74        .split(/[,|\s]+/)
75        .map(c => c.trim())
76        .filter(c => c && c !== 'default')
77      if (valid.length === 0) return
78      await $.store.set('colors', valid)
79      color = pickColor(folder, valid)
80      await $.command.run({ command: 'color', args: color } as never)
81    }
82    if (projects[key]?.color !== color) await remember($, { color })
83  } catch {
84    // no colour this time; the default stays
85  }
86}
87
88/** Names a fresh session after its project: the name you gave it before, else the folder's. */
89async function applyName($: EngineInterface) {
90  try {
91    if (await isOff($)) return
92    const { key, folder, projects } = await thisProject($)
93    await $.command.run({ command: 'rename', args: projects[key]?.name ?? folder } as never)
94  } catch {
95    // unnamed is fine
96  }
97}
98
99export const register: Register = on => {
100  on('session.start', async ($, e, next) => {
101    await $.command.register({
102      name: 'project-colors',
103      description: "Show this project's colour and name, forget them, or switch the mod off and on: /project-colors [reset|off|on]",
104    })
105    const result = await next(e)
106    void applyColor($)
107    return result
108  })
109
110  // the classic start event says whether the session is new and already named
111  on('classic.SessionStart', async ($, e, next) => {
112    const result = await next(e)
113    if (e.source === 'startup' && !e.session_title) void applyName($)
114    return result
115  })
116
117  // learn from what you choose yourself; this mod's own runs come from a plugin
118  on('command.run', { command: 'color' }, async ($, e, next) => {
119    const result = await next(e)
120    // only a colour /color took; a typo it refused (it lists the colours then) is not remembered
121    const color = e.args.trim().toLowerCase()
122    const isRefused = /invalid|cannot|available colors/i.test(result.text ?? '')
123    if (e.origin.kind === 'composer' && color && !isRefused) {
124      void remember($, { color })
125    }
126    return result
127  })
128
129  on('command.run', { command: 'rename' }, async ($, e, next) => {
130    const result = await next(e)
131    const name = e.args.trim()
132    if (e.origin.kind === 'composer' && name) void remember($, { name })
133    return result
134  })
135
136  on('command.run', { command: 'project-colors' }, async ($, e) => {
137    const arg = e.args.trim().toLowerCase()
138    if (arg === 'off' || arg === 'on' || arg === 'toggle') {
139      const isOn = arg === 'on' ? true : arg === 'off' ? false : await isOff($)
140      await $.store.set('isOn', isOn)
141      if (isOn) {
142        void applyColor($)
143        return { text: "Project colours on. This project's colour is back." }
144      }
145      // a command cannot run another from inside itself: reset the colour just after it ends
146      $.clock.after(0, () => void $.command.run({ command: 'color', args: 'default' } as never).catch(() => {}))
147      return { text: 'Project colours off, in every session until /project-colors on. Your saved colours and names are kept.' }
148    }
149
150    const { key, folder, projects } = await thisProject($)
151    if (arg === 'reset') {
152      const { [key]: _forgotten, ...rest } = projects
153      await $.store.set('projects', rest)
154      return { text: `Forgot ${folder}'s colour and name. The next session picks fresh ones.` }
155    }
156    const p = projects[key] ?? {}
157    const state = (await isOff($)) ? ' Project colours are off; /project-colors on brings them back.' : ''
158    return {
159      text: `${folder}: colour ${p.color ?? 'not set yet'}, name ${p.name ?? `${folder} (from the folder)`}. Use /color or /rename to change them; this project remembers.${state}`,
160    }
161  })
162}
163