The setup
LumenPay's main monolith — 400,000 lines of Rails 5.2, started in 2018 — had to land on Rails 7.1 before a security mandate cut off support. Five engineers, a hairy migration spec, and an immovable deadline three weeks out.
Week 1, Day 1 — Bound the work
The senior engineer opened a fresh worktree and gave Claude Code its marching orders:
> Read config/application.rb and Gemfile.
> Produce a migration checklist for Rails 5.2 → 7.1, grouped by file.
> Note the deprecated APIs we depend on and which gems block the upgrade.Eight minutes later: a 73-item checklist with file paths and risk ratings. The team triaged in person, marked items "Claude can do this", "needs a human", or "needs a human + Claude as a pair."
Day 2 — Commit the safety baseline
Before letting Claude Code touch production code paths, the platform engineer committed a project-scoped .claude/settings.json:
{
"permissions": {
"deny": [
"git push --force*",
"git push origin main*",
"bundle exec rake db:drop*",
"rm -rf*"
],
"allow": ["bundle install", "bundle exec rspec*", "git diff*"]
},
"hooks": {
"PreToolUse": [".claude/hooks/check-no-secrets.sh"]
}
}Every engineer's session inherited these rules. Nobody could accidentally force-push or wipe a database from a Claude Code session.
Day 3 — A /migrate-rails slash command
The pattern repeated across files: read the file, apply the documented Rails 7 changes, run the spec for that area, commit on green. They committed it once:
# .claude/commands/migrate-rails.md
For the file path I provide:
1. Read it.
2. Apply Rails 7.1 migration changes per the Rails Upgrade Guide.
3. Run the spec for this area only (`bundle exec rspec spec/<corresponding>`).
4. If green, stage the file and write a commit message starting with "rails 7:".
5. If red, summarize the failures and STOP.The slash command turned 11 days of repetitive prompting into 11 days of /migrate-rails app/models/payment.rb calls.
Day 5 — Log every file edit to Slack
Engineering leadership wanted visibility without micromanaging. A PostToolUse hook posted every Edit and Write to a #rails-upgrade Slack channel:
TOOL=$(jq -r '.tool_name' <<<"$INPUT")
FILE=$(jq -r '.tool_input.file_path' <<<"$INPUT")
if [[ "$TOOL" == "Edit" || "$TOOL" == "Write" ]]; then
curl -s -X POST "$SLACK_WEBHOOK" -d "{\"text\": \":memo: $FILE\"}"
fiChannel got noisy fast and that was fine. When something exploded the next morning, the on-call could scroll back and see exactly which file had been touched at 6:14 PM.
Day 8 — A "payments debugging" skill
LumenPay's payments code has tribal knowledge — which webhooks retry, which counter resets nightly, the exact log lines that mean an authorization failed silently. The team encoded this as a Skill:
.claude/skills/payments-debug/
SKILL.md
reference/error-codes.md
reference/known-failure-modes.md
scripts/tail-payment-logs.shThe skill auto-loaded whenever the conversation involved a payment ID. Junior engineers got expert behavior without typing a paragraph of setup every time.
Day 11 — Code-review agent in CI
The team built a small code-review agent on the Agent SDK and ran it on every Rails upgrade PR:
session = Session(
system=REVIEW_PROMPT_RAILS_7,
tools=[Tool("read_file"), Tool("git_diff")],
)
review = await session.run(f"Review PR #{pr_number}.")
post_to_github_review(review)It caught two deprecated patterns the human reviewers had missed across 60 PRs.
What stuck
- Bound the work first. Don't unleash Claude Code on "do the migration" — feed it a checklist.
- Commit safety baselines as soon as the harness touches a sensitive repo.
- Slash commands turn repetitive prompts into single-token invocations.
- Hooks are the right place for org-wide observability.
- Skills are how teams encode tribal knowledge so juniors inherit it.
- The Agent SDK lets you embed the same review behavior inside CI.
