There’s a pattern I keep seeing in how we use AI today, and I’m guilty of it too.
Claude writes a plan. The plan looks solid. We read it, nod, and say “looks good.” Then we implement it.
The problem isn’t that Claude is bad at planning. The problem is that we’ve outsourced the critical thinking — and we don’t always have the depth to spot what’s missing. Sometimes it’s laziness. More often, it’s that the plan touches areas we don’t fully master, and we trust the model precisely because we can’t easily second-guess it.
So I built a fix: a Model Council — an async hook that automatically calls a second AI to peer-review every plan Claude writes, before a single line of code is touched.
What Is a Model Council?
The idea is simple: no plan gets implemented without a second opinion from a different model.
Every time Claude writes an implementation plan to .claude/plans/, a hook fires in the background and calls nvidia/nemotron-3-super-120b-a12b:free via OpenRouter. Nemotron reads the plan, critiques it as a senior software architect, and saves its review to .claude/council-reviews/. Claude never blocks — the call is fully async. By the time you’re reading Claude’s summary, Nemotron is already working.
This is infrastructure with a feedback loop. Not “AI writes code.” AI reviews AI before AI writes code.
Why Nemotron? Why OpenRouter?
Honestly? Because it’s free.
nvidia/nemotron-3-super-120b-a12b:free is available on OpenRouter’s free tier and it’s a capable 120B model. The point isn’t the specific model — the architecture supports any OpenAI-compatible endpoint. You could swap in GPT-4.1, Gemini 2.5, or a local Ollama instance with one line change. The free tier just means your feedback loop costs you nothing to run.
The Architecture
Claude writes plan → PostToolUse hook fires → background script spawns
↓
OpenRouter API (Nemotron) reads plan
↓
Review saved to .claude/council-reviews/[plan]-[timestamp]-review.md
The hook is project-scoped only — no global Claude Code settings are touched. The API key lives in .claude/settings.local.json, which is gitignored.
Setup: PowerShell (Windows) and Bash (Linux/Mac)
all the below code is just for example, working copy is on Github
1. .claude/settings.json — the hook
json{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "powershell -NonInteractive -File .claude/scripts/model-council.ps1", "async": true } ] } ] }}
For bash (Linux/Mac), replace the command with:
json"command": "bash .claude/scripts/model-council.sh"
2. PowerShell: model-council.ps1
powershellparam()$input_json = [Console]::In.ReadToEnd()$payload = $input_json | ConvertFrom-Json$filepath = $payload.tool_input.path# Only act on plan filesif ($filepath -notmatch '\.claude[/\\]plans[/\\]') { exit 0 }$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"$basename = [System.IO.Path]::GetFileNameWithoutExtension($filepath)$outfile = ".claude/council-reviews/$basename-$timestamp-review.md"# Spawn background call — non-blockingStart-Process powershell -ArgumentList ` "-NonInteractive -File .claude/scripts/call-nemotron.ps1 -PlanFile `"$filepath`" -OutFile `"$outfile`"" ` -WindowStyle HiddenWrite-Output "🧠 Model Council review started → $outfile"exit 0
3. Bash: model-council.sh
bash#!/bin/bashINPUT=$(cat)FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.path')if [[ "$FILEPATH" != *".claude/plans/"* ]]; then exit 0; fiTIMESTAMP=$(date +"%Y%m%d-%H%M%S")BASENAME=$(basename "$FILEPATH" .md)OUTFILE=".claude/council-reviews/${BASENAME}-${TIMESTAMP}-review.md"# Non-blocking background callbash .claude/scripts/call-nemotron.sh "$FILEPATH" "$OUTFILE" &echo "🧠 Model Council review started → $OUTFILE"exit 0
4. The API call: call-nemotron.ps1 / call-nemotron.sh
PowerShell:
powershellparam([string]$PlanFile, [string]$OutFile)$plan = Get-Content $PlanFile -Raw$apiKey = $env:OPENROUTER_API_KEY$body = @{ model = "nvidia/nemotron-3-super-120b-a12b:free" messages = @( @{ role = "system"; content = "You are a senior software architect performing a council review. Critically review the implementation plan. Identify missing edge cases, security risks, scalability gaps, and unclear steps. Be direct and prioritize." } @{ role = "user"; content = $plan } )} | ConvertTo-Json -Depth 5New-Item -ItemType Directory -Force -Path (Split-Path $OutFile) | Out-Nulltry { $response = Invoke-RestMethod -Uri "https://openrouter.ai/api/v1/chat/completions" ` -Method POST -Headers @{ Authorization = "Bearer $apiKey"; "Content-Type" = "application/json" } ` -Body $body $review = $response.choices[0].message.content} catch { $review = "API call failed: $_"}"# Model Council Review`n`n$review" | Set-Content $OutFile
Bash:
bash#!/bin/bashPLAN_FILE=$1OUT_FILE=$2PLAN=$(cat "$PLAN_FILE")mkdir -p "$(dirname "$OUT_FILE")"RESPONSE=$(curl -s https://openrouter.ai/api/v1/chat/completions \ -H "Authorization: Bearer $OPENROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d "$(jq -n \ --arg plan "$PLAN" \ '{model: "nvidia/nemotron-3-super-120b-a12b:free", messages: [ {role: "system", content: "You are a senior software architect performing a council review. Critically review the implementation plan. Identify missing edge cases, security risks, scalability gaps, and unclear steps. Be direct and prioritize."}, {role: "user", content: $plan} ]}')")REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')echo -e "# Model Council Review\n\n$REVIEW" > "$OUT_FILE"
5. API key e .gitignore
bash# .claude/settings.local.json (DO NOT commit this file){ "env": { "OPENROUTER_API_KEY": "sk-or-..." }}
text.claude/settings.local.json
.claude/council-reviews/
Does It Actually Work?
Yes — and the first real test made it obvious.
I gave Claude a task: design a real-time notification system with WebSocket, Redis pub/sub, and PostgreSQL. Claude produced a competent plan. Clean architecture diagram, solid schema, good reuse of existing utilities.
Then Nemotron reviewed it.
The council came back with a 4/5 — and the one point deducted was earned. These were the gaps it found:
| Category | Gap |
|---|---|
| Scalability | socket.io-redis adapter completely absent — multiple WS instances would cause duplicate delivery |
| Security | No JWT revocation check on WebSocket — a stolen token keeps access forever |
| Security | No WS origin whitelist — any site could hijack the real-time channel |
| Reliability | No outbox pattern — a server restart between Redis receive and emit loses notifications silently |
| Correctness | Offset pagination breaks under live inserts — keyset pagination required |
These aren’t edge cases. The socket.io-redis adapter alone would have caused a hard production bug the moment you scale to two instances. I would have caught some of these in code review. I would have missed at least two.
That’s the point of the council.
The Deeper Idea
There’s something interesting about what happens when two models talk to each other. The LinkedIn post that inspired this experiment framed it well: LLMs may process each other’s output with less noise than they process human language. Human prompts are ambiguous, contextual, emotionally loaded. A structured technical plan is closer to the “native format” of a reasoning model.
Whether or not that’s literally true, the practical result holds: you get a better output when a second model has to justify its critique to the first.
We’re moving toward a world where the DevOps layer isn’t just “deploy AI.” It’s “run AI infrastructure that monitors, reviews, and corrects other AI.” The Model Council is one small step in that direction — and it costs nothing to run.
What’s Next
- Extend the council to code review post-write, not just planning
- Add a
--council-skipflag for trivial file writes - Experiment with routing different plan types to different reviewer models (Gemini for architecture, a code-specialized model for implementation details)
The hook is already there. The only thing that changes is which model sits on the council.
The full scripts are available on GitHub. Found a gap the council missed? Open an issue — it saved you from a production bug? ⭐ Star the repo

Leave a comment