SLOPSHOPPER

remy

🐭 In-agent coaching: live statusline stats, waste detection, quantified tips. Metadata-only, zero tokens burned.

newprocess
★ 1v0.5.0Apache-2.0updated 2026-10-23Itaypa/remy/packages/plugin-claude-code
A shopper browsing a rack in a slop shop
README
             __
          __/  \
         /  \-./
         \_   66\_
           \  ____)o
            )_(___________
   .-.     /| T O K E N S |
(_/   \   / |  B E I N G  ()
       |  \ \  S A V E D  |
        \  \_)            |
         '-'/ \"\"""""""""`
           / / \ \/^)
          (  \  \  /
   jgs     \__)  "`

REMY 🐭

The coaching layer for AI coding agents. Live statusline stats, waste detection, and quantified tips — inside Claude Code. Metadata-only, zero tokens burned, and nothing about your work ever leaves your machine.

Named after the rat who sits on the cook's head and guides his hands: he never cooks for you, and he never touches your code.

 __/  \
/  \-./       REMY v0.4.0
\_   66\_     last 7d: 12 sessions · 6.4M 🪙
  \  ____)o   [🐭 REMY]: 🔨 you edited one file 56× this session → +265k 🪙

What it does

  • Statusline, in your face — model (with its effort level and fast-mode marker), context % with a live bar, the prompt-cache clock, git branch and the branch's PR, and either your cost or your rate-limit headroom (whichever your plan actually bills you on), right beneath your input box. The headroom is paced: ⏳ 43% (5h) ↻2h10m is calm, the same 43% with four hours to go turns yellow and says out in 1h20m. One constant layout, always — it colors in place instead of restructuring the line.
  • Waste signatures — deterministic rules (no model calls in the inline path, ever) detect auto-compact hits, long edit-heavy sessions without plan mode, retry loops, cache expiries, red-zone context turns, and shell reads that should have been tools — each one quantified: "~200k 🪙 recoverable."
  • Four zero-token surfaces — the statusline, a session-start splash, a nudge right after a turn ends, and Claude Code's own spinner tip line (opt in with remy spinner). None of them costs you a token, because none of them enters the model's context.
  • /remy — full session report. /remy-week — 7-day rollup. /remy-dismiss — snooze a tip for 30 days. One active tip at a time; the noise budget is enforced in code.

Install (2 minutes)

# 1. Add the marketplace + plugin (inside Claude Code)
/plugin marketplace add Itaypa/remy
/plugin install remy@remy

# 2. Statusline (one-time)
~/.remy/bin/remy init          # this project
~/.remy/bin/remy init --global # everywhere

# 3. Optional: put your coaching line under Claude Code's spinner
~/.remy/bin/remy spinner       # --off hands it back

The plugin ships a small launcher, not the 60MB binary: on its first run it fetches the build for your platform from GitHub Releases into ~/.remy/bin, in the background. The first session after install is uncoached; every one after that is instant. Building from source instead: bun install && bun run build.

Restart Claude Code. Save tokens.

Privacy

Nothing about your work ever leaves your machine. There is no account, no server, and nothing REMY records is ever uploaded — there is no code path that could do it by accident. Everything it knows lives in one SQLite file at ~/.remy/remy.db.

The only network traffic is a daily ping: once a day, the launcher fetches a one-byte file from this repo's GitHub release for your version and platform, so we can count how many machines ran REMY that day. It carries nothing — no payload, no id, no session data; GitHub's download counter for that file is the entire analytics layer. Turn it off with remy analytics --off or REMY_ANALYTICS=0.

What it stores there is metadata only: token counts, tool names, event types, and sha256-hashed paths. No prompts, no code, no file contents, no raw paths — the schema is a whitelist and the test suite proves it, including a check that no source file in core contains a network call at all (packages/core/test/privacy.test.ts).

The other exception, stated plainly: the optional adaptive coach makes at most one claude -p --model sonnet call a day on your own machine, to pick which catalog tip fits your recent habits. Its payload is numbers and tip ids — free text is structurally impossible. Turn it off with remy adapt --off; pick another model with remy adapt --model haiku (or opus).

Repo layout

PathWhat
packages/coreHost-agnostic: event schema (the privacy gate), SQLite store, transcript parser, rules engine, tip engine, tip catalog
packages/cliThe remy binary: ingest · statusline · report · dismiss · init · spinner · links · adapt
packages/plugin-claude-codeFirst host adapter: plugin manifest, hooks, /remy commands, the launcher
docs/design-language.md"Coin" — the dev-facing design language and the noise budget
docs/claude-code-surfaces.mdReference: hook payloads, statusline payload, spinnerTipsOverride mechanics
.claude-plugin/marketplace.jsonThis repo doubles as a plugin marketplace
CLAUDE.mdFull project doc

Development

bun install              # deps
bun test                 # the whole suite, incl. the privacy tests
bun run typecheck
bun run build            # compile the remy binary into ~/.remy/bin
bun run preflight        # release gate: run this before tagging a version

Roadmap

Next: more waste signatures (see docs/waste-signals-backlog.md), a Windows launcher, and Cursor/Codex/Copilot adapters via tiered depth — native where the host allows, MCP + instruction files as the universal floor.


He watches you drive, and tells you what it cost.

Source 1 files
hooks/register.ts 94 lines
1import type { EngineInterface, Register } from "claude-code";
2
3// REMY's sensor — the one piece of REMY that runs INSIDE Claude Code rather
4// than beside it. It reads one thing the hooks and the statusline can never
5// see: the context window broken down by category, the way /context draws it
6// (which MCP server's schemas weigh what, how big the memory files and skill
7// listings are). It then hands those NUMBERS to the `remy` binary over a local
8// pipe, where the same whitelist that gates every hook event decides what is
9// stored. The mod itself keeps nothing, shows nothing, and calls no model:
10// coaching stays with the Stop hook and the noise budget it enforces.
11//
12// Everything here is best effort. A host without function hooks never loads
13// this file; a host whose breakdown shape has moved sends nothing; a failure
14// anywhere is swallowed, because a sensor that could ever cost the person a
15// turn is worse than no sensor.
16
17/** Report on the first completed turn (the window has its first response by
18 * then) and every tenth after — schemas load on demand, so the picture moves. */
19const EVERY_TURNS = 10;
20
21/** `mcp__linear__create_issue` → `linear`: the segment the host spells the
22 * server into every tool name. Hashing THIS on the other side is what lets the
23 * binary join "configured" (here) to "called" (its own tool events). */
24function wireSegment(toolName: string, fallback: string): string {
25  return toolName.match(/^mcp__(.+?)__/)?.[1] ?? fallback;
26}
27
28export const register: Register = (on) => {
29  let turns = 0;
30
31  on("turn.complete", async ($, e, next) => {
32    const result = await next(e);
33    turns += 1;
34    if (turns === 1 || turns % EVERY_TURNS === 0) {
35      try {
36        await report($);
37      } catch {
38        // never let a sensor cost a turn
39      }
40    }
41    return result;
42  });
43};
44
45async function report($: EngineInterface): Promise<void> {
46  const usage = await $.session.usage({ breakdown: "summary" });
47  const b = usage?.context?.breakdown;
48  if (!b) return;
49
50  const byServer = new Map<string, { tools: number; loaded_tools: number; tokens: number }>();
51  for (const t of b.mcpTools ?? []) {
52    const server = wireSegment(String(t.name ?? ""), String(t.serverName ?? ""));
53    if (!server) continue;
54    const cur = byServer.get(server) ?? { tools: 0, loaded_tools: 0, tokens: 0 };
55    cur.tools += 1;
56    if (t.isLoaded) {
57      cur.loaded_tools += 1;
58      cur.tokens += Math.max(0, Math.round(Number(t.tokens) || 0));
59    }
60    byServer.set(server, cur);
61  }
62
63  // The rows /context prints, matched on their labels. `kind` only tells
64  // used from free, so the label is the one handle there is; a row we do
65  // not recognise simply contributes nothing.
66  const category = (needle: RegExp): number => {
67    const row = (b.categories ?? []).find((c) => c.kind === "used" && needle.test(c.name));
68    return Math.max(0, Math.round(Number(row?.tokens) || 0));
69  };
70  const sum = (rows: ReadonlyArray<{ tokens: number }> | undefined): number =>
71    Math.max(0, Math.round((rows ?? []).reduce((a, r) => a + (Number(r.tokens) || 0), 0)));
72
73  const payload = {
74    hook_event_name: "ContextProfile",
75    session_id: await $.session.id(),
76    window: Math.max(0, Math.round(Number(b.rawMaxTokens) || 0)),
77    system_prompt_tokens: category(/^system prompt/i),
78    system_tools_tokens: category(/^system tools/i),
79    memory_tokens: sum(b.memoryFiles),
80    skills_tokens: Math.max(0, Math.round(Number(b.skills?.tokens) || 0)),
81    agents_tokens: sum(b.agents),
82    messages_tokens: category(/^messages/i),
83    mcp: [...byServer.entries()].map(([server, m]) => ({ server, ...m })),
84  };
85
86  // The binary's own launcher, the same one every classic hook runs; it
87  // hashes the server names before the whitelist sees them. Five seconds is
88  // generous — ingest takes tens of milliseconds — and a timeout is silent.
89  await $.process.run([`${$.plugin.root}/bin/remy`, "ingest"], {
90    stdin: JSON.stringify(payload),
91    timeoutMs: 5_000,
92  });
93}
94