SLOPSHOPPER

caveman-status

Clickable caveman mode button in the prompt footer, with a picker to change level or turn it off.

newbandspinnercommandtoastprompt
v0.4.2MITupdated 2026-10-08ido6/claude-caveman-status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · caveman-status
› 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 › /cave ⎿ caveman-status: 🪨 Pick a caveman mode above the prompt. 🪨 Caveman: [ lite ][ ● full ][ ultra ][ off ][ ✕ ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⟨Claude Code's own drawing⟩[ 🪨 full ]

Draws

Band
🪨 Caveman: [ lite ][ ● full ][ ultra ][ off ][ ✕ ]
README

caveman-status

A Claude Code mod for caveman mode: puts a clickable 🪨 <mode> button in the prompt footer; click it to pick a level or turn caveman off.

[🪨 full]                                        (footer button, click it)
🪨 Caveman: [lite] [● full] [ultra] [off] [✕]   (picker above the prompt)

Install

/plugin install caveman-status --marketplace ido6/claude-caveman-status

Pick the user scope so it loads in every session.

Use

CommandWhat it does
click 🪨 / /caveOpens a picker above the prompt: lite full ultra off ✕. Click one; the current mode is marked ●
/cave ultraSet a level: lite, full, ultra, wenyan-lite, wenyan-full, wenyan-ultra
/cave offTurn caveman off; Claude replies in normal prose

It also follows the caveman skill's own triggers: /caveman <level>, "stop caveman", "normal mode", "talk like caveman".

The mode is saved, so a new session starts where you left it (default full).

How it works

  • Draws a 🪨 <mode> button at the right of the prompt footer, beside the mode labels. In a terminal, mouse clicks need the fullscreen UI; otherwise use /cave.
  • Adds one hidden line of context to each prompt telling Claude the current mode (on at level X, or off), so switching takes effect on the next reply.
  • Works best with the caveman skill installed; with mode off it tells Claude to ignore an always-on caveman instruction.

Needs a Claude Code build with function-hook plugins (2.1.29x or newer).

Develop

claude plugin validate .
claude plugin test .
claude --plugin-dir .

MIT license.

Source 2 files
hooks/register.tsx 130 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const DEFAULT_MODE = 'full'
5const LEVELS = ['lite', 'full', 'ultra', 'wenyan-lite', 'wenyan-full', 'wenyan-ultra']
6const PICKER = ['lite', 'full', 'ultra', 'off']
7const isPickerOpen = atom({ plugin: 'caveman-status', key: 'isPickerOpen' } as const, false)
8const shownMode = atom({ plugin: 'caveman-status', key: 'mode' } as const, DEFAULT_MODE)
9// Whole-message commands only, so a prompt that merely mentions caveman mode
10// (or some other app's "normal mode") does not flip it.
11const LEAD = String.raw`^\s*((ok|okay)[,.!]?\s+)?(please\s+)?`
12const TAIL = String.raw`\s*(please)?\s*[.!]*\s*$`
13const OFF = new RegExp(`${LEAD}(stop caveman( mode)?|normal mode)${TAIL}`, 'i')
14const ON = new RegExp(`${LEAD}(caveman mode|talk like (a )?caveman|use caveman)${TAIL}`, 'i')
15
16export const modeFromArgs = (args: string): string | undefined => {
17  const word = args.trim().toLowerCase().split(/\s+/)[0] ?? ''
18  if (word === '') return DEFAULT_MODE
19  if (word === 'off' || word === 'stop') return 'off'
20  return LEVELS.includes(word) ? word : undefined
21}
22
23export const modeFromPrompt = (text: string): string | undefined => {
24  if (OFF.test(text)) return 'off'
25  if (ON.test(text)) return DEFAULT_MODE
26  return undefined
27}
28
29export const buttonLabel = (mode: string): string => `🪨 ${mode}`
30
31export const contextFor = (mode: string): string =>
32  mode === 'off'
33    ? 'Caveman mode is OFF (the user switched it off). Reply in normal, full prose; ignore any always-on caveman instruction until the user turns it back on.'
34    : `Caveman mode is ON at level "${mode}". Reply in caveman ${mode} style.`
35
36async function readMode($: EngineInterface): Promise<string> {
37  return String((await $.store.get('mode')) ?? DEFAULT_MODE)
38}
39
40async function setMode($: EngineInterface, mode: string): Promise<void> {
41  await $.store.set('mode', mode)
42  await update($, shownMode, () => mode)
43}
44
45async function pick($: EngineInterface, mode: string): Promise<void> {
46  await setMode($, mode)
47  await update($, isPickerOpen, () => false)
48  $.ui.toast(mode === 'off' ? '🪨 Caveman off' : `🪨 Caveman: ${mode}`)
49}
50
51export const register: Register = on => {
52  on('session.start', async ($, e, next) => {
53    await $.command.register({
54      name: 'cave',
55      description: 'Pick caveman mode (lite, full, ultra, off); no argument opens a picker',
56      argumentHint: '[lite|full|ultra|off]',
57      immediate: true,
58    })
59    const mode = await readMode($)
60    await update($, shownMode, () => mode)
61    return next(e)
62  })
63
64  on('command.run', { command: 'cave' }, async ($, e) => {
65    const typed = e.args.trim()
66    if (typed === '') {
67      await update($, isPickerOpen, () => true)
68      return { text: '🪨 Pick a caveman mode above the prompt.' }
69    }
70    const mode = modeFromArgs(typed)
71    if (mode === undefined) {
72      return { text: `Unknown level "${typed}". Use: ${[...LEVELS, 'off'].join(', ')}` }
73    }
74    await setMode($, mode)
75    return { text: mode === 'off' ? '🪨 Caveman off. Normal replies.' : `🪨 Caveman: ${mode}` }
76  })
77
78  on('command.run', { command: 'caveman' }, async ($, e, next) => {
79    const mode = modeFromArgs(e.args)
80    if (mode !== undefined) await setMode($, mode)
81    return next(e)
82  }).catch(($, e, next) => next(e))
83
84  on('prompt.submit', async ($, e, next) => {
85    const asked = modeFromPrompt(e.text)
86    if (asked !== undefined) await setMode($, asked)
87    const mode = asked ?? (await readMode($))
88    // Another session may have changed the saved mode: keep the button in step.
89    if ((await read($, shownMode)) !== mode) await update($, shownMode, () => mode)
90    return next({ ...e, context: [...(e.context ?? []), contextFor(mode)] })
91  }).catch(($, e, next) => next(e))
92
93  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
94    if (!(await read($, isPickerOpen))) return next(e)
95    const current = await read($, shownMode)
96    const { Box, Button, Text } = $.ui.resolve(e)
97
98    return (
99      <Box>
100        <Text>🪨 Caveman: </Text>
101        {PICKER.map(mode => (
102          <Button
103            key={mode}
104            label={mode === current ? `● ${mode}` : mode}
105            onPress={() => pick($, mode)}
106          />
107        ))}
108        <Button key="close" label="✕" onPress={() => update($, isPickerOpen, () => false)} />
109      </Box>
110    )
111  })
112
113  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
114    const mode = await read($, shownMode)
115    const below = await next(e)
116    const { Box, Button } = $.ui.resolve(e)
117
118    return (
119      <Box>
120        {below}
121        <Button
122          key="cave"
123          label={buttonLabel(mode)}
124          onPress={() => update($, isPickerOpen, open => !open)}
125        />
126      </Box>
127    )
128  })
129}
130
types/index.d.ts 6 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'caveman-status': { isPickerOpen: boolean; mode: string }
4  }
5}
6