SLOPSHOPPER

mod-switch

Pane that lists the installed mods with a switch each to turn them on or off

newpanecommandtoastprocess
v0.1.0no licenseupdated 2026-10-22nalpari/claude-mods/mod-switch
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mod-switch
│ ┃ mod-switch ✕ › fix the failing auth test and add an audit log call │ ┃ 설치된 mod가 없습니다. │ ⏺ 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 │ │ › /mod-switch │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · mod-switch
설치된 mod가 없습니다.
README

claude-mods

Claude Code mod 모음. 이 저장소 자체가 마켓플레이스(.claude-plugin/marketplace.json)라서 누구나 아래 방법으로 설치할 수 있다.

Mod설명
context-bars컨텍스트 윈도우를 카테고리별 비례 막대 하나로 프롬프트 위에 표시
token-weather컨텍스트 윈도우 사용량을 날씨 아이콘·퍼센트·최근 턴 차트로 프롬프트 위에 표시
y-changeClaude가 코드를 수정할 때마다 diff와 변경 이유(처음 보는 언어의 문법 설명 포함)를 오른쪽 pane에 표시 (기본은 숨김, /y-change로 열기/닫기)
enhance-prompt입력한 프롬프트를 AI가 더 잘 알아듣도록 [THE JOB]/[THE WHY]/[GUARDRAILS]/[DONE MEAN] 구조로 개선해 입력창 위에 보여주고(불명확한 부분은 먼저 입력창 위 질문으로 되묻고 답하면 반복, 최대 3개), [허용]하면 개선본을, [거부]하면 원본을 전송. enhance-prompt: ON/OFF 버튼(데스크탑 입력창 위)을 눌러, 또는 /enhance-prompt로 켜기/끄기
task-progressClaude의 작업 목록(TodoWrite·TaskCreate/TaskUpdate) 진행률을 막대와 완료/전체, 진행 중인 항목 이름으로 프롬프트 위(스피너 아래)에 표시
mod-switch설치된 mod 목록을 오른쪽 pane에 토글 스위치와 함께 표시하고, 스위치를 누르면 해당 mod를 켜거나 끔 (기본은 숨김, /mod-switch로 열기/닫기)

설치

터미널의 Claude Code 세션 프롬프트에서 입력한다. 마켓플레이스 등록과 설치가 한 번에 된다.

/plugin install context-bars --marketplace nalpari/claude-mods
/plugin install token-weather --marketplace nalpari/claude-mods
/plugin install y-change --marketplace nalpari/claude-mods
/plugin install task-progress --marketplace nalpari/claude-mods
/plugin install enhance-prompt --marketplace nalpari/claude-mods
/plugin install mod-switch --marketplace nalpari/claude-mods

처음이면 Add marketplace? 에 y 로 답하고 scope(user 권장)를 고르면 Installed <mod>. Plugin is now active. 가 뜬다. 재시작이나 리로드 없이 바로 적용되고, user scope 는 이후 세션에도 유지된다. 마켓플레이스를 이미 추가했다면 확인 질문은 건너뛴다.

마켓플레이스를 먼저 등록하고 나중에 골라 설치해도 된다.

/plugin marketplace add nalpari/claude-mods
/plugin install context-bars@claude-mods

셸에서는 claude plugin marketplace add nalpari/claude-mods 와 claude plugin install context-bars@claude-mods --scope user 로 같은 일을 한다.

업데이트 / 제거

claude plugin marketplace update claude-mods   # 마켓플레이스 최신화
claude plugin update context-bars@claude-mods  # mod 업데이트
claude plugin list                             # 설치 확인
claude plugin uninstall context-bars           # 제거

로컬 checkout 에서 시험

git clone https://github.com/nalpari/claude-mods.git
cd claude-mods

# 이번 세션에서만 시험
claude --plugin-dir ./context-bars

# 로컬 마켓플레이스로 설치
claude plugin marketplace add ./
claude plugin install context-bars@claude-mods --scope user

참고

  • mod 는 Claude Code 2.1.287 이상에서 기본으로 로드된다 (token-weather/README.md 기준).
  • /plugin install 은 터미널 전용이다. 데스크톱 앱 Code 탭에서는 쓸 수 없지만, 터미널에서 user scope 로 설치하면 로컬 세션에서도 로드된다.
Source 2 files
hooks/register.tsx 91 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Mod } from '../types'
5
6const PANE = 'mod-switch'
7const TITLE = 'mods'
8
9const mods = atom({ plugin: 'mod-switch', key: 'mods' } as const, [])
10
11type Installed = { id: string; scope: string; enabled: boolean; installPath: string; projectPath?: string }
12
13// a mod is a plugin whose hooks.json names a hooks module; the others (skills, MCP servers) are not this pane's to switch
14const isMod = async ($: EngineInterface, dir: string) => {
15  try {
16    return 'modules' in JSON.parse(await $.fs.read(`${dir}/hooks/hooks.json`))
17  } catch {
18    return false
19  }
20}
21
22// the mods installed for this session (user scope, or this project's), on or off
23const load = async ($: EngineInterface) => {
24  const root = await $.session.root()
25  const { stdout } = await $.process.run(['claude', 'plugin', 'list', '--json'])
26  const list: Mod[] = []
27  for (const p of JSON.parse(stdout) as Installed[]) {
28    if ((p.projectPath ?? root) === root && await isMod($, p.installPath)) list.push({ id: p.id, scope: p.scope, enabled: p.enabled })
29  }
30  await update($, mods, () => list)
31}
32
33// the CLI writes the setting; /reload-plugins makes the running session follow it. The switch is flipped before the reload, which reloads this module too
34const flip = async ($: EngineInterface, id: string, scope: string) => {
35  const same = (m: Mod) => m.id === id && m.scope === scope
36  const mod = (await read($, mods)).find(same)
37  if (mod === undefined) return
38
39  const ran = await $.process.run(['claude', 'plugin', mod.enabled ? 'disable' : 'enable', id, '--scope', scope])
40  if (ran.exitCode !== 0) {
41    $.ui.toast(`mod-switch: ${id} 전환 실패 (${(ran.stderr || ran.stdout).trim()})`)
42
43    return
44  }
45  await update($, mods, list => list.map(m => (same(m) ? { ...m, enabled: !mod.enabled } : m)))
46  await $.command.run({ command: 'reload-plugins' }).catch(() => $.ui.toast('mod-switch: /reload-plugins 를 직접 실행하면 적용됩니다'))
47}
48
49export const register: Register = on => {
50  on('session.start', async ($, e, next) => {
51    await $.command.register({ name: 'mod-switch', description: '설치된 mod 목록 pane 열기/닫기' })
52
53    return next(e)
54  })
55
56  // a toggle; a pane waiting undrawn (opened unasked in a narrow terminal) is opened, not closed
57  on('command.run', { command: 'mod-switch' }, async $ => {
58    const isShown = (await $.ui.panes()).some(pane => pane.id === PANE && pane.isPlaced)
59    if (isShown) {
60      await $.ui.close({ id: PANE })
61
62      return { text: 'mod 목록 pane을 닫았습니다.' }
63    }
64    // read afresh at each opening: /plugin may have installed or switched mods since
65    await load($)
66    await $.ui.open({ id: PANE, title: TITLE })
67
68    return { text: 'mod 목록 pane을 열었습니다.' }
69  })
70
71  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
72    const { Box, Button, Text } = $.ui.resolve(e)
73    const list = await read($, mods)
74
75    return (
76      <Box flexDirection="column">
77        {list.length === 0 && <Text dimColor>설치된 mod가 없습니다.</Text>}
78        {list.map(m => (
79          <Box marginBottom={1}>
80            {/* switching itself off would take this pane, and the way back, with it */}
81            {m.id.split('@')[0] === $.plugin.name
82              ? <Text dimColor>{'  -  '}</Text>
83              : <Button key={`${m.id}/${m.scope}`} label={m.enabled ? '●  ON' : '○ OFF'} variant={m.enabled ? 'primary' : 'secondary'} onPress={() => flip($, m.id, m.scope)} />}
84            <Text wrap="truncate" dimColor={!m.enabled}>{`  ${m.id}`}</Text>
85          </Box>
86        ))}
87      </Box>
88    )
89  })
90}
91
types/index.d.ts 9 lines
1// one installed mod: `id` is `<name>@<marketplace>`, `scope` where it was installed (user, project, local)
2export type Mod = { id: string; scope: string; enabled: boolean }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'mod-switch': { mods: Mod[] }
7  }
8}
9