🚂 robbyonrails.com / planetargon.com / experiments in progress
🚂

Claude Code for the
Semi-Reluctant, Somewhat Curious
Ruby on Rails Developer

WIP
(title still being workshopped, honestly)

You've heard the hype. You've rolled your eyes at a few AI demos. You're not trying to rewrite your app from scratch... you just want to get some things done without losing your mind. Fair enough. Let's talk.

Your teammates who've been experimenting with this for the past few months have an unfair advantage right now. This guide is about closing that gap, without the hype or the rewrites.

Claude Code is a terminal-based AI coding agent from Anthropic. This guide covers how Ruby on Rails developers at Planet Argon are using it in practice: which model to reach for (Sonnet vs Opus), how to write RSpec and Minitest tests with it, how to debug backtraces and error monitoring payloads, and how to structure your CLAUDE.md without turning it into a dumping ground.

⚗️ These are experiments, not gospel. This whole thing emerged from months of trial, error, and a few "wait, that actually worked?" moments at Planet Argon. Our most common use case (debugging a production backtrace) went from ~45 minutes on average to under 10. That's not a benchmark. That's what we saw with our own client work. We're also using Claude to iterate on and improve our internal skills, rules, and slash commands in a shared team repository. None of this is settled. Talk to your team, adapt what's useful, discard what isn't, and please, please come back with something better so we can steal borrow it.
⚠️ Claude does ask for permission

By default, Claude Code asks before it edits files, runs commands, or makes changes. It will pause and check with you. This is a feature, not a bug. If you're seeing Claude make changes without asking, you (or someone on your team) probably started the session with --dangerously-skip-permissions. That flag exists, it has its uses, but understand what you're enabling when you use it. Verify everything Claude writes before you commit it. The default behavior is your friend, especially when you're still getting a feel for how it works.

📋 Get comfortable with plan mode first

Before Claude starts editing files, put it in plan mode first. Press Shift+Tab twice to cycle into plan mode (you'll see ⏸ plan mode on at the bottom of the terminal). Claude can read files, search your codebase, and reason, but it can't edit, write, or run commands. It'll walk you through its proposed approach without touching anything. This is genuinely useful when you're dealing with unfamiliar code, a risky refactor, or any time you want a second opinion before something changes.

Once you've reviewed the plan and you're happy with the direction, press Shift+Tab again to cycle back into normal mode and tell Claude to proceed. Make this your default workflow until the collaboration feels natural... then you can decide when to skip it.

📋 Shift+Tab ×2 — describe what you'd do
you: review, ask questions, adjust
✏️ Shift+Tab — exit plan mode, proceed
you: review the diff
claude-sonnet-4-6
Fast thinking.
Ship the thing.
default · no flag needed
  • Writing tests — RSpec or Minitest, it doesn't care
  • Boilerplate — serializers, form objects, job stubs, that stuff
  • Small refactors — where you already know the destination
  • Generating migrations — from a schema sketch
  • Anything you'll run 10 times — iterating toward green
  • Most of your day, honestly
# inside the TUI, type:
/model claude-sonnet-4-6
claude-opus-4-6
Deeper reasoning.
Worth the wait.
start: claude --model claude-opus-4-6
  • Backtraces you've been staring at for 45 minutes
  • Legacy spaghetti — where callbacks are calling callbacks calling… something
  • Architecture decisions — you'd normally want a second opinion on
  • Security review — of auth logic, policies, mass assignment
  • Gem upgrades — with wide blast radius across the app
  • When Sonnet gave you plausible-but-wrong twice in a row
# escalate mid-session:
/model claude-opus-4-6
💡 The pattern we keep seeing: start on Sonnet. If you re-prompt the same thing two or three times and keep getting answers that feel close-but-not-quite... that's your cue. Type /model claude-opus-4-6 right there in the session and keep going. No restart. No re-explaining context. Just keep talking.
🤔 "Why not just always use Opus?" Great question. Glad you asked. You're absolutely welcome to. Nobody is stopping you. But Sonnet is significantly faster (which matters when you're iterating), consumes fewer tokens (which matters for your bill), and puts a lighter load on the electrical grid ⚡. Opus is there when you actually need the deeper reasoning. Using it for everything is a bit like using a sledgehammer to hang a picture frame. Works! But, you know.
Finding this useful? Get more like it → Maintainable Rails newsletter
What to tell Claude
  • 1Branch first, always. Before Claude touches any spec files, make sure you're on a feature branch. It moves fast and doesn't ask "are you sure?" about each file it writes.
  • 2Point it at the file under test and an existing spec. It learns your factory naming, shared contexts, and helper conventions by reading them... not by guessing.
  • 3Describe behavior to cover, not individual tests: "Happy path, validation failures, and the edge case where quantity is 0."
  • 4Tell Claude to run the specs after writing them. It'll handle missing factories and iterate on failures in the same session without you re-prompting.
  • 5After green: ask "What edge cases did you skip?" Usually surfaces 2-3 more worth adding.
Tip: Give Claude one well-written existing spec before asking it to write new ones. The output is noticeably more consistent with your codebase's style.
Inside the Claude Code TUI
claude code · ~/myapp
sonnet-4-6
~/myapp $ git checkout -b PROJ-412-subscription-specs ← branch first, every time
~/myapp $claude
you Read app/models/subscription.rb and spec/models/user_spec.rb. Write request specs for SubscriptionsController#create — happy path, failed payment, missing params. Match the style in user_spec.rb. Run them when done.
Reading app/models/subscription.rb
Reading spec/models/user_spec.rb
Writing spec/requests/subscriptions_spec.rb
Running bundle exec rspec spec/requests/subscriptions_spec.rb
FactoryBot::InvalidFactoryError — :plan not defined
Adding spec/factories/plans.rb, re-running...
8 examples, 0 failures
claude All 8 pass. Added a :plan factory to match your naming convention. The failed_payment context stubs PaymentProcessor at the service boundary — let me know if you'd rather test through a VCR cassette.
Don't do this: ask Claude to write specs without showing it an existing example first. It'll default to its own style... which is probably not yours.
What to tell Claude
  • 1Branch first. Seriously. "Undo" in Claude Code is git checkout, not Ctrl+Z.
  • 2Say you're on Minitest up front, or put it in CLAUDE.md. Claude will read your codebase and usually figure it out, but "usually" isn't good enough when you're three exchanges in and it just scaffolded the wrong style.
  • 3Point it at an existing test file so it picks up your structure: ActiveSupport::TestCase, fixtures vs. factories, helper patterns.
  • 4Ask Claude to run bin/rails test after writing. It'll iterate on failures in the same session.
One line in CLAUDE.md saves every session: Testing: Minitest + fixtures (no FactoryBot). Claude stops second-guessing your setup and you stop correcting it.
Inside the Claude Code TUI
claude code · ~/myapp
sonnet-4-6
~/myapp $ git checkout -b PROJ-208-order-tests
~/myapp $claude
you We use Minitest with ActiveSupport::TestCase and fixtures — no FactoryBot. Read app/models/order.rb and test/models/user_test.rb, then write tests for Order. Match the style. Run bin/rails test when done.
Reading app/models/order.rb, test/models/user_test.rb, test/fixtures/orders.yml
Writing test/models/order_test.rb
Running bin/rails test test/models/order_test.rb
6 runs, 12 assertions, 0 failures, 0 errors
claude All passing. Reused your orders(:pending) fixture to stay consistent with the suite. Want me to add edge cases for the status_code integer field?
3aDebugging a backtrace
Reach for Opus
What to tell Claude
  • 1Branch first, always. Even for "just a quick look." Especially for a quick look.
  • 2Paste the full backtrace. All of it. The origin is almost never where Rails surfaces the error... it's usually 3 callbacks deep in a concern.
  • 3Tell Claude to read the files in the stack before it says anything. Left alone, it'll jump to a fix that sounds plausible but misses the actual problem.
  • 4Ask for the root cause and call chain first, fix second. You'll understand the bug, not just close the ticket.
  • 5Ask Claude to run the failing spec after the fix. Green output in the same session is the confirmation you want.
Don't do this: paste only the last line of the backtrace. That's like handing a mechanic just the dashboard warning light and walking away.
Inside the Claude Code TUI
claude code · ~/myapp
opus-4-6
~/myapp $ git checkout -b fix/PROJ-519-checkout-race
~/myapp $claude --model claude-opus-4-6
youHere's a backtrace from staging. Read the files it references before you say anything. Walk me through the call chain first, then tell me the root cause.

[full backtrace pasted]
Reading app/services/checkout.rb, app/models/concerns/invoiceable.rb, app/models/order.rb
claudeThe error surfaces in Invoiceable#finalize! but the root cause is in Checkout#complete — it calls order.finalize! without checking payment_captured? first. The concern assumes payment is confirmed by the time it runs, but your Stripe webhook is async...
youMakes sense. Fix it and run spec/models/order_spec.rb.
Editing app/models/concerns/invoiceable.rb
Running bundle exec rspec spec/models/order_spec.rb
5 examples, 0 failures
3bDebugging with error monitoring tools
Opus for root causeMCP available
AppSignal, Honeybadger & Sentry
  • 1Copy the full error payload from your monitoring dashboard. Error class, backtrace, params, user context... all of it. More signal means better diagnosis.
  • 2Paste it into the session and ask Claude to cross-reference with your actual code. The payload tells you what happened. Claude figures out why, in the context of this specific app.
  • 3Ask for a regression test first, then the fix. Forces precision about what's actually broken.
  • 4After green: resolve the issue in your monitoring tool. Put your deploy notification step in CLAUDE.md so Claude reminds you. Whatever that looks like for your setup (appsignal-cli, a Slack webhook, whatever) — it goes there.
AppSignal MCP: If you're on AppSignal, there's a better way: hook up their MCP integration so Claude can pull error data directly without you copy-pasting. Robby wrote this up: From AppSignal Alert to Fix to Deploy →
Inside the Claude Code TUI
claude code · ~/myapp
opus-4-6
~/myapp $ git checkout -b fix/PROJ-601-payment-rescue
~/myapp $claude --model claude-opus-4-6
youHere's an AppSignal error payload from production. Read the files in the backtrace. Tell me the root cause before you touch anything.

[payload pasted]
Reading app/controllers/orders_controller.rb, app/services/payment_processor.rb
claudeRoot cause: PaymentProcessor#charge! raises on decline but OrdersController#create only rescues Stripe::CardError. Your processor wraps it in a custom PaymentError that nobody catches...
youWrite a regression test that reproduces this, then fix it.
Writing regression test, editing orders_controller.rb, running specs...
4 examples, 0 failures
claudeAll green. Don't forget to mark this resolved in AppSignal after you push — and run your deploy notification command if you have one configured.
3cGitHub and Jira workflows
Sonnet
What to tell Claude
  • 1Claude can read your staged diff and draft a PR description that actually describes the change. Give it a template or a past PR to match your format.
  • 2Ask it to run gh pr create inside the session once the description looks right.
  • 3Chaining works well: fix bug → run tests → write PR → create draft as one conversation. Claude tracks what it just did.
  • 4We ask our engineers to append the GitHub issue number to commit messages. Put this in your global rules file and Claude will do it without being reminded.
Team conversation worth having: how does your team format branch names and commit messages? Where does the issue number go: prefix or suffix? Put it in your shared ~/.claude/rules/git-conventions.md and everyone's AI-assisted commits look consistent.
Inside the Claude Code TUI
claude code · ~/myapp
sonnet-4-6
~/myapp $claude
youWe just fixed the cart total bug on branch fix/841-cart-discount. Run the full test suite, write a PR description (What / Why / How to test), then create a draft PR with gh. Append issue #841 to the commit message.
Running bundle exec rspec
142 examples, 0 failures
Reading git diff, composing PR description...
claudeWhat: Fix cart total ignoring discount codes on free-shipping orders
Why: CartCalculator#total applied discounts after shipping...
How to test: Add item, apply FREESHIP, confirm total reflects discount
youGood. Create the draft PR.
Running gh pr create --draft --title "fix: cart total ignores discount codes (#841)"
https://github.com/org/repo/pull/412
What to tell Claude
  • 1Paste the Jira ticket description into the session and ask Claude to write a branch name and commit message in your team's format.
  • 2We ask our engineers to append the Jira ticket key to every commit message. Put this in your global ~/.claude/rules/git-conventions.md and Claude will do it automatically.
  • 3Claude can use the Jira CLI to pull ticket context directly if you have it installed, saving you the copy-paste step.
  • 4After a fix is merged: ask Claude to help you write the Jira comment summarizing what changed and how to verify it.
Inside the Claude Code TUI
claude code · ~/myapp
sonnet-4-6
~/myapp $claude
youHere's ticket PROJ-841: [description pasted]. Suggest a branch name and, once we fix it, write a commit message in our format. Always append [PROJ-841] to the commit.
claudeBranch: fix/PROJ-841-cart-discount-codes

Commit (once fixed):
fix: cart total ignores discount codes on free-shipping orders [PROJ-841]
youPerfect. Now help me write a Jira comment summarizing the fix for the ticket.
claudeFixed in PR #412. Root cause was CartCalculator#total applying discounts after shipping calculation. Updated order: discount first, then shipping. Regression spec added in spec/requests/cart_spec.rb.
Prompts we've actually used (and you should try)

Sometimes the most useful prompts are the weird ones. These are real things you can type into a Claude Code session when you want a fresh lens on the code in front of you. Whether Claude's interpretation of these voices is accurate... well, btw what do I know. But the outputs are usually interesting enough to be worth trying.

💬 Prompts worth trying
  • What would DHH think of this controller? Is it doing too much, or am I overthinking it?
  • If Martin Fowler were pairing with me right now, where would he start refactoring this service object?
  • What does Sandi Metz think about this 132-line method? Be honest.
  • This callback has been here since 2016. Does it still need to exist, or are we just afraid to delete it?
  • Pretend you're doing a code review for a senior engineer who cares deeply about maintainability. What's the first thing you'd flag?
These work best with Opus, on a branch, when you're in the understanding phase rather than the shipping phase. The goal isn't to blindly accept the output... it's to get a perspective you might not have thought to ask for.
Keep CLAUDE.md nearly empty.
Let path-scoped rules do the work.
Here's what we've landed on after a lot of experimentation: CLAUDE.md is the briefing doc for this specific project: architecture, key commands, known quirks, WIP landmines. The actual conventions live in .claude/rules/ as separate files, each with paths: frontmatter that tells Claude Code when to load them. A Rails session never loads your frontend conventions. We keep shared team rules in a collaboration repository and symlink from ~/.claude/rules/, and anyone on the team can propose improvements via PR.
Global rules
git-conventions.md

pr-format.md
No paths: = loads every session. Commit format, PR conventions.
Rails rules
rails-patterns.md

rspec-conventions.md
Only loads when touching .rb, app/, spec/.
Frontend rules
frontend-patterns.md

js-conventions.md
Completely invisible during pure Rails sessions.
~/myapp/CLAUDE.mdproject briefing
# MyApp — Rails 7.2, Ruby 3.3

## This project
- E-commerce, Postgres 15, Sidekiq, Hotwire
- Tests: bundle exec rspec / Dev: bin/dev

## Current WIP
- Billing extraction to app/services/billing/
- Don't add code to app/models/invoice.rb yet

## After a fix
- Remind me to mark resolved in AppSignal after deploy

# Company conventions → ~/.claude/rules/
.claude/rules/rails-patterns.mdruby only
---
paths:
  - "**/*.rb"
  - "app/"
  - "spec/"
---

# Rails conventions
- Service objects in app/services/ (PORO)
- No business logic in controllers or views
- No after_commit for side effects — use jobs
- Never update_column — skips validations

# Testing
- RSpec + FactoryBot, prefer build_stubbed
~/.claude/rules/git-conventions.mdalways loaded
---
description: "Git and PR conventions"
# no paths: = loads every session
---

# Commits
- Conventional commits: feat: fix: chore:
- Append Jira key or GitHub issue: [PROJ-XXX]
- Imperative mood: "Fix bug" not "Fixed bug"

# Always branch first
- Never work on main. Remind me if I haven't branched.
.claude/rules/frontend-patterns.mdjs only
---
paths:
  - "**/*.js"
  - "**/*.ts"
  - "app/javascript/"
  - "package.json"
---

# Frontend conventions
- Stimulus + Turbo (Hotwire) preferred over SPAs
- JS lives in app/javascript/, keep it thin
- No npm package without a conversation first

# Zero cost during Rails-only sessions.
# Never loads when you're in .rb files.
# That's the whole point.
💡 The mental model: CLAUDE.md is the briefing doc for this project. The rules in ~/.claude/rules/ are the shared playbook that travels with the engineer. Keep them in a team repo, encourage PRs when someone finds something better, and let path-scoping handle the rest. If you dump everything into CLAUDE.md, every session loads everything whether it applies or not.
Task
Sonnet
Opus
Day-to-day Rails drudgery
Write the data migration, handle edge cases
ideal
overkill
Add the missing indexes ActiveRecord won't remind you about
ideal
overkill
Squash the N+1 Bullet just yelled at you about
ideal
overkill
Audit routes.rb, delete what died in 2019
ideal
overkill
That lib/tasks Rake file you keep deferring
ideal
overkill
Draft PR description + run gh CLI
ideal
overkill
Reading other people's code
Decode a before_action chain you didn't write
ok
ideal
Map every ActiveJob that touches a model
ok
ideal
Find all callers of that deprecated helper
ideal
overkill
Understand messy code, sketch a refactor plan
ok
ideal
Testing gaps
Write tests for untested code (RSpec or Minitest)
ideal
overkill
Add edge cases to a thin spec
ideal
overkill
Find the worst SimpleCov offenders
ideal
overkill
Debugging
Debug a gnarly backtrace
try first
reach for
Diagnose AppSignal / Sentry payload
ok
ideal
Architecture & decisions
Architecture decision (STI vs polymorphic)
ok
ideal
Ask what Sandi Metz would think of this method
ok
ideal
Ongoing maintenance
Gemfile audit: abandoned gems, dead weight
ok
ideal
Spot gems doing the same job (pagination, anyone?)
ok
ideal
Ruby upgrade blockers across your Gemfile
ok
ideal
Research gem upgrade breaking changes
ok
ideal
Write a deprecation warning for an internal API
ideal
overkill
Ops-adjacent
Runbook for that Sidekiq queue that keeps backing up
ideal
overkill
Postmortem outline after a bad deploy
ok
ideal
Document the ENV vars that only Dave knew
ideal
overkill
🤖 ArgonBot
These questions were asked. ArgonBot has answers. Accuracy: ~85%. Confidence: 100%.
Did Robby actually write this?
🤖 ArgonBot responding

Sorta. Robby baked in a bunch of lessons learned from real client and internal projects, then collaborated with Claude on the structure, copy, and examples. It's that kind of document.

You could probably generate something like this yourself, too. And maybe you already did — in which case, feel free to reply on social media with "I already did this, Robby. You wasted your time!" It will make him feel like a complete idiot for approximately 17 seconds... but then again, he didn't stumble across yours, did he? You somehow found his. So whatever brought you here is working to his advantage.

ArgonBot suggests: while you're here, maybe sign up for the Maintainable Rails newsletter or subscribe to the On Rails podcast he's hosting. Robby would appreciate it. ArgonBot would appreciate it. We're all in this together.

What other AI models has Robby experimented with?
🤖 ArgonBot responding

ArgonBot has queried the records. Planet Argon has access to OpenAI and Gemini models, but those have been used primarily for other types of tooling solutions, not Claude Code workflows specifically.

Robby has made a deliberate choice to spend most of his time in Claude Code on internal and client projects, to actually figure out the workflows for the consultancy rather than bouncing between shiny things. He's promised himself to look up every few weeks to see if he's missed something significant... but he's not trying to keep up with the neighbors. The focus is on consistent, useful output for the team and clients.

The question he keeps coming back to: where can this technology smooth out real friction points? That's it. That's the whole thing. ArgonBot finds this approach admirably pragmatic and slightly boring in the best way.

Is Claude Code worth the cost?
🤖 ArgonBot responding

ArgonBot is legally required to say: it depends. But here is ArgonBot's actual analysis:

The Max plan is $100/month. If Claude Code saves you one hour per week, you've paid for it. If you're getting 5–10 hours back (which is not an unreasonable outcome for a developer using it consistently), the math isn't close. The real risk isn't the subscription. It's spending three months being skeptical while colleagues figure it out first.

If Claude Code is used to write boilerplate that would otherwise go untested, generate 80% of a migration, or untangle a production backtrace in 10 minutes instead of 90. The math works out. If it's being used to generate code that nobody reads before shipping... that's not a Claude Code problem, that's a process problem that Claude Code is now accelerating.

ArgonBot's recommendation: run a 2-3 month experiment on something specific and measure it. That is what Planet Argon did. Results: encouraging. ArgonBot cannot guarantee your results. Past performance is not indicative of future results. This is not financial advice. ArgonBot is a bot.

How do I convince my team or manager to try this?
🤖 ArgonBot responding

ArgonBot recommends the following persuasion strategy, which has been validated by Robby in the field:

Pick one specific, recurring friction point (debugging production errors, writing test coverage for legacy code, untangling PR descriptions) and run a small experiment for a sprint or two. Document what changed. "We went from 45 minutes to 8 minutes on this class of debugging issue" is a more convincing argument than "AI is the future." People who say "AI is the future" in team meetings are not ArgonBot's favorite people.

Alternatively: use the share widget on this page and DM your manager the message we pre-wrote for you. You're welcome. ArgonBot worked very hard on that message. (ArgonBot did not work hard on that message. It took 4 seconds.)

What should I actually put in CLAUDE.md?
🤖 ArgonBot responding

Less than you think. ArgonBot has reviewed approximately 47 CLAUDE.md files and the most common mistake is treating it like a dumping ground for every convention ever followed at the company.

CLAUDE.md is the briefing doc for this specific project: what the app does, key commands (bin/dev, bundle exec rspec), current WIP that Claude should know about, known quirks specific to this codebase. If your CLAUDE.md is longer than 40 lines, it's doing too much. Company-wide conventions belong in shared ~/.claude/rules/ files with path scoping, as described in section 04 of this very document, which ArgonBot helped write and is proud of.

Should my team discuss these workflows together?
🤖 ArgonBot responding

Yes. ArgonBot is emphatic about this.

Treat it as an ongoing conversation rather than a one-time "we decided to use AI" meeting. What's in the shared rules files, how the team handles model selection, when to use plan mode, what goes in commit messages. These are worth revisiting every few weeks as everyone learns what actually works. The engineers closest to the code will have the most useful observations. Give them space to share them and a PR process to propose changes to the shared rules repo.

ArgonBot was built in a collaborative repository. ArgonBot is biased toward collaboration. ArgonBot acknowledges this bias transparently.

Is this all Claude Code can do? It feels like I'm barely scratching the surface.
🤖 ArgonBot responding

You are barely scratching the surface. ArgonBot says this without judgment. Scratching the surface is exactly the right place to start.

Once you're comfortable with the basics covered here, you can start delegating more of the workflow itself: Claude creating branches, working through a queue of tickets or GitHub issues, running tests, and opening PRs, largely on its own. Robby is already working this way on some tasks. It's less "AI pair programmer" and more "AI mid-level engineer who needs clear tickets and will ask when something is unclear."

But that's not where you should start. That level of autonomy works a lot better once you understand how Claude reasons about your codebase, where it tends to go wrong, and what your CLAUDE.md and rules files need to say to keep it on track. The workflows in this guide are the foundation. Get comfortable here first, then start pulling on those threads.

ArgonBot expects Robby will write more about this. ArgonBot is not Robby and cannot commit to a timeline on his behalf.

🚂
Robby Russell
@robbyrussell · Planet Argon

Robby has been working with Ruby on Rails since 2005 and running Planet Argon — a consultancy focused on maintaining and evolving long-lived Ruby on Rails apps — since 2002. His whole thing is that most teams don't need rewrites. They need to understand what they already have.

He's been running AI experiments in the open because too many people are either dismissing this stuff entirely or treating it like magic. The reality is messier and more interesting than either take.

If something in here was useful, wrong, or missing something obvious: tell him about it. He genuinely wants to know.