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

__
__/ \
/ \-./
\_ 66\_
\ ____)o
)_(___________
.-. /| T O K E N S |
(_/ \ / | B E I N G ()
| \ \ S A V E D |
\ \_) |
'-'/ \"\"""""""""`
/ / \ \/^)
( \ \ /
jgs \__) "`
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 🪙
⏳ 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.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.# 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.
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).
| Path | What |
|---|---|
packages/core | Host-agnostic: event schema (the privacy gate), SQLite store, transcript parser, rules engine, tip engine, tip catalog |
packages/cli | The remy binary: ingest · statusline · report · dismiss · init · spinner · links · adapt |
packages/plugin-claude-code | First 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.md | Reference: hook payloads, statusline payload, spinnerTipsOverride mechanics |
.claude-plugin/marketplace.json | This repo doubles as a plugin marketplace |
CLAUDE.md | Full project doc |
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
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.
hooks/register.ts 94 lines1import 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