SLOPSHOPPER

mod-scout

Watches your prompts at zero token cost: suggests the installed mod that fits, sets your focus goal, turns prompts you repeat into one-word slash commands, and…

newcommandtoastprompttimer
v0.1.0MITupdated 2026-10-20samarthsh-git/claude-mods/mods/mod-scout
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mod-scout
› 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 › /scout ⎿ mod-scout: mod-scout is on. ⎿ mod-scout: Macros (0): none yet. Send the same prompt 3 times and one is made. ⎿ mod-scout: Recent suggestions: none ⎿ mod-scout: Make a new mod: /scout build <what it should do> ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

Claude Code mods (hooks-module plugins) for subagent visibility, task tracking, token savings and daily workflow. Needs Claude Code 2.1.304 or newer (built and tested on 2.1.304; the mod API is early access and can change between releases).

Install

At the prompt of a Claude Code terminal session:

/plugin install agent-monitor --marketplace samarthsh-git/claude-mods
/plugin install task-inbox --marketplace samarthsh-git/claude-mods
/plugin install token-saver --marketplace samarthsh-git/claude-mods
/plugin install daily-driver --marketplace samarthsh-git/claude-mods
/plugin install clarify-first --marketplace samarthsh-git/claude-mods
/plugin install mod-scout --marketplace samarthsh-git/claude-mods

Answer y to add the marketplace, then pick the user scope. From a shell: claude plugin install <mod> --marketplace samarthsh-git/claude-mods.

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with samarthsh-git/claude-mods, then Add plugin.

If this repository is private, the machine needs a git credential for github.com (gh auth login, then gh auth setup-git); the install never prompts for one.

The mods

agent-monitor: what every subagent is doing

  • A sidebar (/subagents) that opens by itself when the first subagent starts.
  • Each subagent shows its task, type, model, run time, tool count and tokens.
  • A live "now:" line shows what it is doing at that moment ($ npm test, reading auth.ts, searching "TODO").
  • Finished agents show a one-line result. History stays across sessions (last 60).
  • Press a row to open that agent's transcript. /subagents clear wipes the history.
  • The status line shows ⚙ N subagents running, and a toast appears when one fails.

task-inbox: to-do list and task inbox

The Tasks pane (/todo or /inbox) has three parts:

  1. Claude's plan. A live mirror of Claude's own TodoWrite or Task list: ☐ pending, ◐ in progress, ☑ done.
  2. My to-dos. Your own list, saved per project. /todo <text> adds, /todo done 2 ticks, /todo rm 2 removes, /todo clear drops finished ones.
  3. Inbox. Queue tasks with /inbox <text>, then press Send (or /inbox send). Claude spawns one background subagent per task, in parallel, on the cheapest model that fits. You keep working while they run, and agent-monitor shows their progress.

token-saver: fewer tokens, same accuracy

FeatureWhat it doesSetting
Subagent model routingbalanced: search and lookup subagents (Explore, guides) run on Haiku. aggressive: general-purpose and Plan subagents spawned from an Opus-class session also run on Sonnet. A model Claude or you set explicitly is never changed, and neither are forks or plugin agents.routing
Lean-output rulesAdds about 150 tokens to the system prompt: no preamble or recaps, locate before reading, batch tool calls, delegate broad searches, prefer Edit, never trade correctness for brevity. It sits in the cached part of the prompt.leanRules
Duplicate-read guardRefuses once a Read of a file Claude already read with the same range in the same context while the file is unchanged on disk. Repeating the exact call goes through, so the worst case is one extra round trip. It resets on compaction.dedupeReads
Usage HUDStatus line: ctx 42% · $1.23 · 5h 37%. Toasts at 70% context (suggests /compact or /clear) and at 80% of a rate limit.always on
Model advisorA toast when a prompt looks trivial for an Opus-class model, or hard for Haiku. Pure regex, costs no tokens, at most once per 15 min.advisor
/tokensReport: fresh, cached and output tokens, cache hit rate, subagent share, what token-saver saved, and concrete tips.

Change settings with /config (or pluginConfigs in settings.json).

What saves the most, from experience: /clear between unrelated tasks, keeping the prompt cache warm (no model switches mid-thread), delegating read-heavy searches to subagents, and using Sonnet as the default with Opus only for hard problems. The mod nudges toward these; it cannot measure your exact savings.

daily-driver: get more done per day

  • /aim <goal> sets a focus goal (/aim auto <goal> sets one only when none is set), shown above the prompt with time spent and today's file and commit counts. Press Done or run /aim done to log it.
  • When a turn ran 60 s or longer (setting notifyAfterSeconds), you get a desktop or phone notification with Claude's answer. Hand Claude long work and switch tasks.
  • /eod (or /eod yesterday) prints a zero-token work log: Claude time, prompts, files changed, commits, test runs, subagents and goals done. Paste it into a standup.
  • /handoff has Claude write .claude/handoff.md (goal, done, state, next steps, gotchas). /pickup in the next session continues from it, and a toast reminds you when one exists.

clarify-first: questions before complex work

On a complex task or a deep, open-ended question, Claude first checks the repo and the chat for what it can learn itself. If something that would change the result is still missing, it asks you up to 4 multiple-choice questions in one round, with a recommended option on each. It then states the agreed spec in 3 lines or fewer, does the work, and checks the result against that spec. Everyday prompts ("fix the typo", "what is X") pass straight through.

Cost:

  • Detection: free keyword scoring decides most prompts. Only borderline ones get a classification call on the small, fast model, typically a few hundred tokens. Set detector to heuristic for zero detection tokens.
  • Instruction: about 200 tokens of instruction reach the model, and only on prompts flagged complex.
  • Payoff: one question round is usually cheaper than redoing work built on a wrong assumption. That trade is the point of the mod. It is not a measured gain.

Commands: /clarify on (default) · off · always · now <task> (force questions for one task) · /clarify alone shows status. Settings: detector (hybrid / heuristic / model), maxQuestions (1 to 4).

mod-scout: the right mod at the right time

It reads each prompt locally, with no model call and zero tokens, and never changes what Claude sees:

  • Suggests the installed mod that fits, as a toast, once per kind per session. For example "why is my usage so high" suggests /tokens, a list of 3 or more tasks suggests /inbox, and "wrapping up for today" suggests /handoff. It knows every mod here plus Podium and Grounded Engineering, and stays quiet about mods you have not installed.
  • Sets your focus goal (daily-driver's /aim) from your first real task of the session, unless you already set one.
  • Makes mods for you: send the same prompt a third time and it becomes a slash command on the spot (/run-test, say). This costs no tokens to make or to use beyond the prompt itself. /scout rm <name> removes one.
  • Builds bigger mods on request: /scout build <idea> has Claude write, validate and test a new mod, with zero run-time token cost wherever the feature allows. This is the one part that spends tokens, and only when you ask.

/scout shows recent suggestions and your macros. /scout off pauses it. Settings: autoMacros, autoFocus.

Claude chat (claude.ai, phone and desktop apps)

The mods above run only in Claude Code: the chat app has no plugin system. For chat, chat/ holds the closest equivalents, and both work on every device once set up:

  1. chat/preferences.txt: paste it into Settings → Profile → personal preferences. It holds the lean-output rules, clarifying questions on complex tasks only, and a model-fit hint. It is about 200 words, shorter than a typical preferences block, and preferences ride on every message, so shorter is cheaper.
  2. chat/deep-work-skill.zip: upload it in Settings → Capabilities → Skills. It is a gather → ask → spec → deliver → self-check procedure that Claude loads only for complex work, so everyday chats pay just its one-line description.

Chat habits that save the most: start a new chat per topic (each message resends the whole chat), put reference files in a Project instead of pasting them again, and use a lighter model for simple asks.

Cloud sessions from your phone

.claude/settings.json registers this marketplace and enables the six mods, so a Claude Code session opened on this repository (from claude.ai/code or the phone app) offers to install them with no commands typed.

Third-party mods from the same Reddit thread

These are other people's projects. Install them from their own repositories:

ModWhat it isInstall
PodiumHands jobs to other agent CLIs (Codex, Gemini, other Claude models) and tracks them in one pane, with sprints and PRs. Needs Node 22+, git 2.28+ and at least one other agent CLI./plugin marketplace add madjagstudios/plugins, then /plugin install podium@madjagstudios
Grounded Engineering/grounded pane: engineering practices that fit your repo, with sources, plus a reviewed shelf of skill repositories./plugin marketplace add madjagstudios/grounded-engineering, then /plugin install grounded-engineering@grounded-engineering
claude-ui-modNot a Claude Code plugin. It patches the files of the Claude Desktop app on Windows: subagent HUD, task rail, usage meters, provider routing.Windows only. Quit Claude Desktop, then git clone https://gitlab.com/blockshooter/claude-ui-mod && cd claude-ui-mod && npm ci && node scripts/verify-frontend.cjs && node patch.js apply. Read its SECURITY.md first: it rewrites your Claude Desktop install, and app updates can break it. node patch.js revert undoes it.

The "optimaizr" and "inbox monitor" mods mentioned in the thread had no links. task-inbox and token-saver cover the same ground.

Developing

claude plugin validate mods/<mod>
claude plugin test mods/<mod>

To run a working copy, add "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/abs/path/mods/agent-monitor:/abs/path/mods/task-inbox" } to ~/.claude/settings.json, or start with claude --plugin-dir mods/<mod>.

License

MIT

Source 1 files
hooks/register.ts 197 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3type Macro = { name: string; text: string; createdAt: number }
4type Count = { n: number; text: string; at: number }
5type Rule = { id: string; command: string; test: RegExp; tip: string }
6
7// Origins that are a person typing: the terminal, Remote Control (phone), an app host.
8const PERSON = new Set(['composer', 'bridge', 'sdk'])
9
10// Each rule fires only when its command is installed, at most once a session.
11const RULES: readonly Rule[] = [
12  { id: 'tokens', command: 'tokens', test: /\b(tokens?|cost|spend|usage|limit|expensive|cheaper|quota)\b/i, tip: '/tokens shows where tokens go and what to change' },
13  { id: 'subagents', command: 'subagents', test: /\b(sub-?agents?|agents? (are|is) (doing|running)|background (jobs?|agents?))\b/i, tip: '/subagents shows every subagent and what it is doing now' },
14  { id: 'todo', command: 'todo', test: /\b(remind me|don'?t forget|to-?do|later i (need|have) to|note to self)\b/i, tip: '/todo <text> keeps it on your list across sessions' },
15  { id: 'eod', command: 'eod', test: /\b(stand-?up|end of (the )?day|what did i (do|get done)|daily (summary|report)|status update)\b/i, tip: '/eod prints today\'s work log for free' },
16  { id: 'pickup', command: 'pickup', test: /\b(where (was i|did we leave)|pick up|continue (from )?(yesterday|last time)|resume (the|my) work)\b/i, tip: '/pickup continues from the last /handoff note' },
17  { id: 'handoff', command: 'handoff', test: /\b(wrap(ping)? up|calling it a day|done for (today|the day)|stop(ping)? for now|tomorrow i)\b/i, tip: '/handoff saves where you are so the next session resumes exactly here' },
18  { id: 'podium', command: 'podium', test: /\b(codex|gemini|muse|other agents?|second opinion|cross-?review)\b/i, tip: '/podium hands the job to another agent CLI and tracks it' },
19  { id: 'grounded', command: 'grounded', test: /\b(best practices?|ci gate|repo (structure|hygiene)|engineering (standards|practices)|agents\.md|claude\.md)\b/i, tip: '/grounded lists practices that fit this repo, with sources' },
20  { id: 'clarify', command: 'clarify', test: /\b(ask me (questions|first)|clarify|before you start)\b/i, tip: '/clarify now <task> makes Claude ask its questions first' },
21]
22
23const STOP = new Set('a an the to of in on for and or but is are be it this that with my me i you we please can could would should do make just then also from at by as so what how'.split(' '))
24
25function normalize(text: string): string {
26  return text.toLowerCase().replace(/\d+/g, '#').replace(/[^\w#\s]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200)
27}
28
29function slugOf(text: string, taken: ReadonlySet<string>): string {
30  const words = normalize(text).replace(/#/g, '').split(' ').filter(w => w.length > 1 && !STOP.has(w))
31  const base = (words.slice(0, 2).join('-') || 'macro').slice(0, 24)
32  let name = base
33  for (let i = 2; taken.has(name); i++) name = `${base}-${i}`
34  return name
35}
36
37// Independent items in one prompt: 3+ numbered or bulleted lines.
38function listCount(text: string): number {
39  return text.match(/^\s*([-*•]|\d+[.)])\s+\S/gm)?.length ?? 0
40}
41
42const TASK = /\b(build|implement|add|create|fix|refactor|migrate|write|design|set ?up|debug|optimi[sz]e|integrate|ship|deploy)\b/i
43
44async function commandNames($: EngineInterface): Promise<Set<string>> {
45  const list = await $.command.list().catch(() => [])
46  return new Set(list.map(c => c.name))
47}
48
49function clip(text: string, n: number): string {
50  const one = text.replace(/\s+/g, ' ').trim()
51  return one.length > n ? one.slice(0, n - 1) + '…' : one
52}
53
54function buildPrompt(idea: string): string {
55  return [
56    `Build a Claude Code mod for this: ${idea}`,
57    'Load the plugin-authoring skill and follow it: write the mod in this session\'s mods folder, then validate, type-check and test it.',
58    'Constraints: zero model tokens at run time wherever possible (no model calls in hooks unless the feature needs one, and then the small fast model);',
59    'never rewrite or trim prompts, system prompt or tool results in a way that could lower answer quality; prefer suggestions over silent actions.',
60    'Check the installed commands first so it does not duplicate an existing mod. Finish with 3 lines: what it does, its commands, its cost.',
61  ].join(' ')
62}
63
64async function registerMacro($: EngineInterface, m: Macro) {
65  await $.command.register({ name: m.name, description: `Macro: ${clip(m.text, 90)}` })
66}
67
68export const register: Register = (on, options) => {
69  const isAutoMacros = options.autoMacros !== false
70  const isAutoFocus = options.autoFocus !== false
71  let isOn = true
72  let macros: Macro[] = []
73  let installed = new Set<string>()
74  const shown = new Set<string>()
75  const recent: string[] = []
76  let lastToast = -Infinity
77  let isFocusDone = false
78
79  function remember(line: string) {
80    recent.push(line)
81    if (recent.length > 10) recent.shift()
82  }
83
84  on('session.start', async ($, e, next) => {
85    await $.command.register({
86      name: 'scout',
87      description: 'mod-scout: suggestions so far, your macros, and /scout build <idea> to have Claude make a new mod',
88      argumentHint: '[build <idea> | macros | rm <macro> | on | off]',
89    })
90    isOn = (await $.store.get('isOn')) !== false
91    const saved = await $.store.get('macros')
92    macros = Array.isArray(saved) ? (saved as Macro[]) : []
93    installed = await commandNames($)
94    for (const m of macros) if (!installed.has(m.name)) await registerMacro($, m).catch(() => undefined)
95
96    return next(e)
97  })
98
99  on('command.run', async ($, e, next) => {
100    const macro = macros.find(m => m.name === e.command)
101    if (macro === undefined) return next(e)
102    const extra = e.args.trim()
103    const text = extra ? `${macro.text}\n\n${extra}` : macro.text
104    $.clock.after(50, () => void $.prompt.submit({ text, asUser: true }))
105
106    return { text: `/${macro.name} sent its prompt.` }
107  }).catch(($, e, next) => next(e))
108
109  on('command.run', { command: 'scout' }, async ($, e) => {
110    const args = e.args.trim()
111    const [word = '', ...rest] = args.split(/\s+/)
112    if (word === 'build' && rest.length > 0) {
113      const idea = rest.join(' ')
114      $.clock.after(50, () => void $.prompt.submit({ text: buildPrompt(idea), asUser: true }))
115      return { text: `Asking Claude to build a mod for: ${idea}` }
116    }
117    if (word === 'on' || word === 'off') {
118      isOn = word === 'on'
119      await $.store.set('isOn', isOn)
120      return { text: `mod-scout ${word}.` }
121    }
122    if (word === 'rm' && rest[0]) {
123      const name = rest[0].replace(/^\//, '')
124      if (!macros.some(m => m.name === name)) return { text: `No macro /${name}.` }
125      macros = macros.filter(m => m.name !== name)
126      await $.store.set('macros', macros)
127      return { text: `Removed /${name}. It disappears from the command list next session.` }
128    }
129    const lines = [
130      `mod-scout is ${isOn ? 'on' : 'off'}.`,
131      `Macros (${macros.length}): ${macros.map(m => `/${m.name}`).join(', ') || 'none yet. Send the same prompt 3 times and one is made.'}`,
132      `Recent suggestions: ${recent.length === 0 ? 'none' : ''}`,
133      ...recent.map(r => `• ${r}`),
134      'Make a new mod: /scout build <what it should do>',
135    ]
136
137    return { text: lines.join('\n') }
138  })
139
140  on('prompt.submit', async ($, e, next) => {
141    if (!isOn || e.command || e.isMeta || !PERSON.has(e.origin.kind)) return next(e)
142    const text = e.text.trim()
143    const now = await $.clock.now()
144    // Other mods register their commands as they start: read the list fresh.
145    installed = new Set([...(await commandNames($)), ...macros.map(m => m.name)])
146
147    // 1. The one rule that fits, if its mod is installed and it was not shown yet.
148    const rule = RULES.find(r => !shown.has(r.id) && installed.has(r.command) && r.test.test(text))
149    let tip: string | undefined = rule?.tip
150    if (rule) shown.add(rule.id)
151    if (!tip && !shown.has('inbox') && installed.has('inbox') && listCount(text) >= 3) {
152      shown.add('inbox')
153      tip = 'Several independent tasks: /inbox each one, then Send, and they run in parallel subagents'
154    }
155    if (tip) {
156      remember(tip)
157      if (now - lastToast > 60_000) {
158        lastToast = now
159        $.ui.toast(`💡 ${tip}`)
160      }
161    }
162
163    // 2. The session's first real task becomes the focus goal, when none is set.
164    if (isAutoFocus && !isFocusDone && installed.has('aim') && TASK.test(text) && text.length >= 30 && text.length <= 600) {
165      isFocusDone = true
166      const goal = clip(text.split(/(?<=[.?!])\s|\n/)[0] ?? text, 70)
167      $.clock.after(50, () => void $.command.run({ command: 'aim', args: `auto ${goal}` }).catch(() => undefined))
168    }
169
170    // 3. A prompt sent for the third time becomes a slash command.
171    if (isAutoMacros && text.length >= 15 && text.length <= 1500) {
172      const key = normalize(text)
173      const counts = ((await $.store.get('counts')) ?? {}) as Record<string, Count>
174      const seen = counts[key]
175      counts[key] = { n: (seen?.n ?? 0) + 1, text, at: now }
176      const keys = Object.keys(counts)
177      if (keys.length > 300) {
178        keys.sort((a, b) => (counts[a]?.at ?? 0) - (counts[b]?.at ?? 0))
179        for (const old of keys.slice(0, keys.length - 300)) delete counts[old]
180      }
181      await $.store.set('counts', counts)
182      if (counts[key]?.n === 3 && !macros.some(m => normalize(m.text) === key)) {
183        const taken = new Set([...installed, ...macros.map(m => m.name)])
184        const macro: Macro = { name: slugOf(text, taken), text, createdAt: now }
185        macros = [...macros, macro]
186        await $.store.set('macros', macros)
187        installed.add(macro.name)
188        $.clock.after(50, () => void registerMacro($, macro).catch(() => undefined))
189        remember(`Made /${macro.name} for a prompt you sent 3 times`)
190        $.ui.toast(`⚡ You sent this 3 times: /${macro.name} now sends it. /scout rm ${macro.name} undoes it.`)
191      }
192    }
193
194    return next(e)
195  }).catch(($, e, next) => next(e))
196}
197