The story
You are a staff engineer at a mid-sized company. Your inbox has eight open PRs waiting on your review. You are on a 45-minute train ride and have signal but no laptop. Historically, PR review from the phone means squinting at GitHub's diff view and typing hollow "LGTM" comments.
Rousseau lives on the little Intel NUC in your closet. It has a WhatsApp connection to your JID, bypassPermissions on the local claude CLI, and read access to your local checkouts of the three repos you review. You DM WhatsApp:
review PR #4821 in checkout-service
Rousseau runs gh pr diff 4821, reads the code, and replies:
PR #4821 adds retry-with-backoff to the Stripe webhook handler. Three observations:
- The backoff is uncapped — if Stripe is truly down, we'll accumulate goroutines forever. Suggest a
context.WithTimeoutat the caller.- Test file adds two new tests but neither covers the timeout path.
pkg/webhook.go:82— the mutex is held across the network call. Minor.Should I draft a review comment stub?
You reply:
yes, with all three
Rousseau uses gh pr review under --draft (approver blocks final approval; a human still has to click "submit review"). By the time you're at your desk, the draft comment is waiting.
What that requires
The daemon
Rousseau on the NUC as a rootless Podman container:
- Provider:
claudecli— inherits your local Claude Code auth. - Transport: WhatsApp — the transport of choice for mobile reach.
- State:
~/.local/share/rousseau/sessions.db.
Config
provider: claudecli
claudecli:
binary: claude
permission_mode: bypassPermissions
whatsapp:
reply_header: "🚂 *rousseau*\n\n"
log:
level: info
format: text # this is a single-user daemon; text logs are fine
agent:
max_iterations: 32
compression:
enabled: true # subscription-tier claudecli; compression is free
trigger_messages: 60
keep_recent: 8
approver:
mode: pattern
default: deny
reason: "denied — this daemon reviews code, it does not merge it"
allow:
- {tool: read, match: ".*"}
- {tool: grep, match: ".*"}
- {tool: bash, match: "^\\s*\"command\":\\s*\"(gh pr view|gh pr diff|gh pr list|gh pr review --draft|gh pr comment|git status|git diff|git log|git show) "}
- {tool: bash, match: "^\\s*\"command\":\\s*\"(go test|go vet|go build|npm test|pnpm test|cargo check) "}
deny:
- {tool: bash, match: "gh pr merge|gh pr close|gh pr approve"}
- {tool: bash, match: "git (push|reset --hard|clean)"}
- {tool: write, match: "\"path\":\"/etc/|/root/|/var/"}
- {tool: edit, match: "\"path\":\"/etc/|/root/|/var/"}
The bind mounts
~/repos/checkout-service/(read-only).~/repos/payments-api/(read-only).~/repos/web-frontend/(read-only).~/.claude/— Claude Code's OAuth tokens (read-write, but only for token refresh).~/.config/gh/— GitHub CLI's OAuth token (read-write, same reason).
Read-only mounts prevent the model from accidentally editing your working copy. Reviews go through GitHub, not through your checkout.
First launch
rousseau whatsapp --allow 447900123456@s.whatsapp.net
You scan the QR code once. From then on the daemon lives in the Quadlet unit and boots on host restart. Your allowlist is the JID of your own personal phone.
The security posture
- Allowlist locks the transport. Only your phone can drive the daemon. Anyone else who somehow discovers the phone number gets silently dropped.
- Pattern approver blocks every merge / push / close. Rousseau reviews, drafts, and comments — a human still has to click "Merge" or "Approve".
- Read-only mounts protect your working checkouts.
bypassPermissionson claudecli is only tolerable because the approver is doing the safety work. Never combinebypassPermissionswithmode: allow_all.
The reach
- Signal drops on the tube. WhatsApp's backpressure is graceful — you send a question, you get an answer when the daemon has signal to reply. Rousseau does not need to hold a live TCP session with your phone.
- Voice notes work. With voice mode enabled and
whisper.cppinstalled on the NUC, you can dictate a voice note "what's the diff on 4821" and get a text reply. Useful when typing on a phone in a moving train is annoying. - The daemon runs on your hardware. Nothing about your review reasoning goes to a third-party SaaS. The only outbound call is the
claudeCLI's subprocess to Anthropic, using your existing subscription.
What rousseau does not do here
- It does not click "Merge". That's a human decision, and the approver enforces it.
- It does not learn your review style. The next PR gets the same generic checklist unless you author a skill capturing your style.
- It does not queue reviews. Each request is independent; there's no "review all my open PRs" background job (unless you wire one via cron).
What you'd change under load
- Add a skill called
pr-review-checklist.mdthat codifies the six things you always check. Skills are spliced into the system prompt when a matching trigger appears in the user message. - Add a nightly cron:
0 8 * * 1-5 rousseau ... deliver a summary of every open PR. - Switch to a paid Anthropic API path if
claudeclisubscription rate limits become a bottleneck. Zero config changes downstream.
Related pages
- WhatsApp transport — the transport reference.
- claudecli provider — inherited auth.
- Skills — how to codify your review style.
- Voice mode — dictate reviews.