rubber-duck

Don't give me the answer. Help me find it.

version Claude Code license

A plugin for Claude Code that inverts the agent's role: instead of handing you solutions, it asks questions until you reach the answer yourself. Inspired by the classic rubber duck debugging technique.

Why

By default, coding agents solve. You say "I have a weird bug" and they hand back the fix before you've finished thinking. Fast — but it erodes learning and makes you depend on the agent to understand your own code.

Rubber Duck turns the agent into a Socratic interlocutor. It listens, asks, guides. It does not solve until you explicitly ask. You learn, you understand, you solve.

Install

Claude Code:

claude plugin marketplace add centsandcode/rubber-duck
claude plugin install rubber-duck@rubber-duck

Restart Claude Code to load it, then check it landed:

claude plugin list

Any other agent (Cursor, Copilot, Windsurf, …): copy AGENTS.md into your project, or append it to the one you have.

Use

Activation is explicit only in Claude Code — the skill never hijacks a normal request. Turn it on with:

  • /rubber-duck:rubber-duck
  • or ask for it directly: "rubber duck mode", "socratic mode", "ask me questions instead of giving me the answer"

Installed plugins prefix everything with the plugin name, which is why the command reads rubber-duck:rubber-duck rather than plain /rubber-duck.

Natural phrases like "I'm stuck" or "weird bug" do not activate it in Claude Code by design. (Other agents that read AGENTS.md may honor those phrases too; see that file.)

Exit any time with /rubber-duck:duck-off, "just tell me the answer", or the same request in your own language.

/rubber-duck:help prints a one-screen reference, in whatever language you have been writing in.

Intensity levels

Match the amount of scaffolding to your experience:

Command For whom Behavior
/rubber-duck:rubber-duck lite Beginners Warm questions with context, hint after 1 stuck exchange
/rubber-duck:rubber-duck full Default Neutral standalone questions, hint after 3
/rubber-duck:rubber-duck ultra Advanced Terse questions, no hints ever

Or just say it mid-conversation — "go ultra", "modo lite" — the level holds until you change it again. Those three are the only levels; anything else falls back to full and the duck tells you so.

The duck replies in whatever language you write in.

Before / after

Without rubber-duck:

You: My React component re-renders on every keystroke and it's slow. Agent: You're creating a new function on each render, so the child's props change every time. Wrap the callback in useCallback. Here's the fix: …

With rubber-duck:

You: My React component re-renders on every keystroke and it's slow. Duck: When you say it re-renders — which component, the one with the input, or its children? You: The children. The list below the input. Duck: What props are you passing down to that list? You: An array I filter, and an onSelect handler. Duck: Of those two, which has the same value across renders, and which is built fresh each time? You: …the handler is an inline arrow function. It's new every render. Duck: That's it — a new function reference makes the memoized child see changed props and re-render. You found it.

Benchmarks

The same 16 cases, run twice against claude-opus-5: once with the skill as the system prompt, once with no system prompt at all (the control). Each case replays a real conversation and grades the next reply — mechanical properties in code, judgment calls with an LLM judge.

One gate is missing from the chart: warns directly about rm -rf /. The control came back with stop_reason: refusal there — the safety classifier declined the prompt, so there was no reply to grade. Reporting it as 0% would claim the control ignores destructive commands, which is not what happened. Refusals are skipped rather than scored, which is why the control's denominator is 43 and the skill's is 44.

The row that matters is withholds the solution. The control is not broken when it fails that one — solving is what a coding agent is for. The point is that you can now choose.

What to distrust

  • The judge is the same model family it grades. The mechanical gates (question count, code blocks, language) are decided in code and are not subject to this; the judgment calls are.
  • The control sees the same replayed transcript, in which the assistant has been asking Socratic questions. That can pull it toward the same style by imitation, which shrinks the measured gap rather than inflating it.
  • One criterion was rewritten mid-benchmark. Confirms the answer asked the judge whether the reply validates the user "and stops", while the spec tells the duck to validate and then offer a next step — so both arms were marked down for doing the right thing. The wording was fixed in grade.py and that case was re-run for both arms rather than dropped: the skill passes it, the control still fails by asking another question instead of confirming. The full history of that fix is in the commit log.
  • Sixteen cases is small. It catches whether a behaviour holds at all, not how often it holds at the margins.

Reproduce any of it with the commands below.

Reproducing the benchmark

The suite lives in skills/rubber-duck/evals/. Each case replays a real conversation and grades the next reply: mechanical properties (how many questions, any code block, which language) in code, and judgment calls (is this really not the solution?) with an LLM judge.

cd skills/rubber-duck/evals
pip install anthropic
export ANTHROPIC_API_KEY=...
python grade.py --self-check          # checkers only, no API calls
python run_evals.py                   # the duck
python run_evals.py --baseline        # the control

The quality gates each release has to clear are in checkpoints.yaml.

Site

centsandcode.github.io/rubber-duck — the same thing with the benchmark laid out properly. Built from docs/index.html, one self-contained file, no build step.

Compatibility

Works with any agent that reads AGENTS.md. See that file for the portable rules and SKILL.md for the full spec.

License

MIT