# I Rebuilt My Claude Code Router: It Now Reads Your Code Graph and Learns From Its Mistakes

## TL;DR

A while back I wrote about [routing every Claude Code prompt to the right model](https://devlore.hashnode.dev/how-i-route-every-claude-code-prompt-to-the-right-model) using a small hook, three subagents and a `CLAUDE.md` file. It worked, and it showed me exactly where keyword routing breaks down.

**ccr (Claude Code Router)** is the sequel. It's a pip-installable tool that picks the cheapest model and effort level likely to be enough for each task. It looks at what the task *says*, what code it *touches*, how *risky* that code is, and how similar tasks *went before*. It's rule-based and runs locally, so routing costs zero extra tokens.

```sh
pip install claude-code-router    # installs the `ccr` command
cd your-project && ccr init && claude
```

Repo: [github.com/ai-solution-architect-roadmap/claude-code-router](https://github.com/ai-solution-architect-roadmap/claude-code-router)

## Where v1 fell short

The first version scored prompts on keywords and length. A word like "refactor" pushed a prompt toward one tier; a long prompt pushed it toward another. It was cheap, easy to reason about, and handled the obvious cases well. But three things kept bothering me:

*   **Prompt length is a weak signal.** "Drop the legacy\_id column from the users table in production" is short and dangerous. A long, polite paragraph asking for one typo fix is trivial.
    
*   **It never looked at the code.** Renaming a helper in a utils file and changing the payment ledger can be described in the same words. They are not the same risk.
    
*   **It never learned.** If a tier kept failing on a certain kind of task, v1 kept sending that work there anyway.
    

## The idea behind v2

Ask the question a senior engineer asks before handing out work: *who is the cheapest person who can do this safely?* The answer depends on four things:

1.  **What kind of task is it?** A question, a bug fix, a feature, a migration, security work, or a design request.
    
2.  **What does it touch?** ccr builds a local tree-sitter code graph and matches the prompt against file paths, function names and keywords.
    
3.  **How risky is that code?** It scores blast radius (how many files depend on it), complexity, test coverage, git churn, critical paths like `auth/` and `payments/`, and any notes you've left in `CLAUDE.md`.
    
4.  **What happened last time?** Past outcomes for that kind of task, in that part of the codebase, nudge the tier up or down.
    

## Six tiers

| Tier | Model / effort | Typical work |
| --- | --- | --- |
| T0 | haiku / low | questions, lookups, typos, renames |
| T1 | sonnet / low | tests, clear bug fixes |
| T2 | sonnet / medium | features, refactors |
| T2H | sonnet / high | risky multi-file changes |
| T3 | opus / high | migrations, security, unclear bugs, design |
| T4 | opus / xhigh | critical-path changes only |

T2H fills v1's biggest gap. The jump from Sonnet/medium to Opus/high is large, and a lot of real work sits somewhere in between.

## How a tier gets picked

Every step is plain Python. There's no model call, and a decision takes well under half a second.

1.  **Start from the task class.** A question starts at T0, a bug fix at T1, a feature at T2, a migration at T3.
    
2.  **Move up for risk.** Add one tier for any of: large scope, a vague or intermittent bug ("sometimes", "flaky"), complex code, high risk, a critical path, or a memory flag. Add two if three or more apply.
    
3.  **Move down for clarity.** Subtract one for a small scope or explicit step-by-step instructions — but never on a critical path.
    
4.  **Apply what it has learned.** If a tier keeps failing for that task type, start higher. If it keeps succeeding, try one lower.
    
5.  **Respect the cap.** Nothing goes above T3 unless a critical-path rule applies. T4 requires an explicit rule, such as a destructive migration or a security change on a critical path.
    
6.  **Check the budget.** Once you're past 80% of your weekly usage, non-critical work drops a tier.
    

You can see the reasoning for any prompt before running it with `ccr explain`:

![ccr explain output](https://raw.githubusercontent.com/ai-solution-architect-roadmap/claude-code-router/main/docs/images/explain.png align="center")

Here, "Add a --dry-run flag to ccr/cli.py" starts as a feature at T2. The target file has 13 dependents and a function with 30 branches, so risk scores high and ccr bumps it to T2H. The file already has tests, which is why test proximity scores zero. The output also shows churn, the critical-path check and the full reasoning.

## How it plugs into Claude Code

You keep typing plain `claude`. `ccr init` installs:

*   **Hooks** that add a one-line routing hint to each prompt, for example: `ccr: T2 (sonnet/medium) — touches app/payments/ledger.py, 2 dependents, no tests found, critical path: payments. Delegate to ccr-implement`
    
*   **One subagent per tier** (`ccr-quick`, `ccr-small`, `ccr-implement`, `ccr-senior`, `ccr-architect`, `ccr-critical`), each pinned to its model and effort.
    
*   **A status line relay** that reads your weekly rate limit to feed budget pressure.
    

As in v1, the hint is advice — Claude decides whether to follow it. Unlike v1, the hooks are **fail-open**: bad input, a corrupt config or a timeout produces no output and exits 0. ccr can't block your work.

## A feedback loop

v1 had no memory. v2 records every routed task and whether it worked. Each new prompt closes out the previous task: a retry phrase like "still failing" counts as a failure and pushes the retry up one tier. You can also correct it by hand with `ccr feedback good|bad`.

That history powers several commands:

*   `ccr stats` — success and escalation rates per tier, plus a list of misrouted tasks.
    
*   `ccr calibrate` — checks whether the routing signals actually predict failures.
    
*   `ccr tune` — proposes config edits as a diff; nothing changes without `--apply`.
    
*   `ccr shadow` — re-runs a sample of deep-tier tasks at a cheaper tier to measure over-routing.
    

`ccr dashboard` turns it all into a report:

![ccr dashboard](https://raw.githubusercontent.com/ai-solution-architect-roadmap/claude-code-router/main/docs/images/dashboard.png align="center")

## What the numbers say — and what they don't

*   **Benchmark.** `scripts/bench_routing.py` replays 26 labelled prompts against a fixture project, with no API calls. Result: zero under-routed, zero over-routed, and an estimated cost about 52% below all-Opus. But I wrote those labels myself and tuned the rules after a 400-prompt replay, so a perfect score is expected. It tests the router against my judgment, not against the real world.
    
*   **My own usage.** The dashboard above covers 60 tasks from building ccr itself: 98% success, 13% over-routed, 2% under-routed, and an estimated 78% saving versus all-Opus.
    
*   **That saving is an estimate.** It prices measured tokens at configurable per-million rates and only counts tasks that finished. And a task that succeeded on a cheap tier proves the tier was *enough* — not that it was *necessary*.
    

I also have no measurement of answer quality. ccr measures routing, not whether the cheaper model's answer was as good as the expensive one's would have been.

## Where it's weak

*   **Prompts that name no code.** "Make it faster" or "it's broken" gives the locator nothing to work with. ccr falls back on keyword cues and sends vague bug reports to T3 — safe, but potentially more expensive.
    
*   **Unusual project layouts.** The critical-path rules match common folder names like `auth/`, `payments/` and `migrations/`. If your payments code lives in `settlement-core/`, add your own `critical_paths`, `critical_terms` and `hard_terms` in `.ccr/config.yaml`.
    
*   **Unsupported languages and stale graphs.** Without a fresh graph, risk scoring falls back to file-name matching, and dependent counts and complexity become unreliable.
    
*   **Hard tasks described in plain words.** "Add caching" to a subtle concurrent module looks like an ordinary feature. Rules can't see subtlety that neither the words nor the code metrics reveal.
    

## Try it

```sh
pip install claude-code-router
cd your-project
ccr init --dry-run     # shows every file it would touch; writes nothing
ccr init               # applies it (modified files are backed up)
ccr doctor             # checks the setup
ccr explain "Fix the off-by-one in paginate() in app/api/pagination.py"
claude
```

It's MIT licensed, and the README documents every command. To skip routing for a single prompt, add `!noroute` to the end of it.

## Help me find the misroutes

Like v1, ccr gets some prompts wrong — and the most useful bug reports are specific. If you see a bad tier, open an issue on the repo with:

1.  the prompt (redact anything private),
    
2.  the output of `ccr explain "<prompt>" --json`, and
    
3.  the tier you expected.
    

Every misroute you send makes the rules better for everyone.
