From ruby-upgrade-toolkit
Use when the user runs /ruby-upgrade-toolkit:fix or asks to apply upgrade fixes, bump Ruby/Rails versions, fix deprecations, fix RSpec failures after upgrading, or fix RuboCop issues. Accepts `next` (read target from the task list) OR explicit ruby:X.Y.Z, optional rails:X.Y, and optional scope:path arguments. Applies all changes, iterates until green, verifies, prompts for commit, and ticks off the matching task.
npx claudepluginhub dhruvasagar/ruby-upgrade-toolkit --plugin ruby-upgrade-toolkitThis skill is limited to using the following tools:
Apply all upgrade changes end-to-end: version pins, gem dependencies, code fixes, Rails config (if Rails), iterative RSpec green, iterative RuboCop green.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Apply all upgrade changes end-to-end: version pins, gem dependencies, code fixes, Rails config (if Rails), iterative RSpec green, iterative RuboCop green.
Two modes:
Explicit mode:
ruby:X.Y.Z — required target Ruby versionrails:X.Y — optional target Rails versionscope:path — optional; restricts code-level fixes to this file or directory. Gem and version pin changes always apply project-wide.Next mode:
next (bare keyword, no value) — resolve the target from the task list created by /plan. See Step 1b below.next is mutually exclusive with ruby:/rails:. If both are given, surface an error and stop. If neither is given, surface: Provide either 'next' (requires a plan) or 'ruby:X.Y.Z [rails:X.Y]'.
Apply the "Load" and "Schema check" sections of
$CLAUDE_PLUGIN_ROOT/skills/rules/references/rules-engine.md.
If .ruby-upgrade-toolkit/rules.yml is absent, set RULES_LOADED = false.
Fix behaves byte-identically to a version without this feature — skip every
rules-specific substep below.
If rules are loaded:
/ruby-upgrade-toolkit:rules validate).The engine's "Apply ordering" section governs interleaving of rules with built-in steps. Specifically, within this phase:
phase-inject rules (timing: before) matched to this phase.code-transform rules matched to this phase, in declared order.gem-swap rules matched to this phase: edit Gemfile (remove from,
add each to[]), run bundle install, apply the swap's
code_transforms[]. Any private-source to[] entry must have its
source block written as the Gemfile source ... do ... end block; the
tool reads credentials from the environment at bundle time (never echoes
them).phase-inject rules (timing: after) matched to this phase.Apply the swap's code_transforms[] using the same literal-or-regex
machinery as standalone code-transform rules.
next modeSkip this step if the user passed explicit ruby:/rails: arguments.
Call TodoWrite to list pending tasks. Find the first pending task whose subject matches one of these regex patterns:
^Phase \d+ — Ruby (\d+\.\d+\.\d+): apply \+ verify \+ commit$ → extract Ruby patch version; set TARGET_RUBY = <match>, no Rails arg.^Phase \d+ — Rails (\d+\.\d+): apply \+ verify \+ commit$ → extract Rails minor; set TARGET_RAILS = <match>. Read current Ruby (ruby -v) and use it verbatim as TARGET_RUBY.^Final — Infra checks → this task is not fix-actionable. Print: Next task is 'Final — Infra checks'. Run /upgrade to execute it, or tick it off manually after you've updated CI/CD and Dockerfiles. Exit.If no pending task matches, print:
No pending fix tasks in the task list.
Run /ruby-upgrade-toolkit:plan ruby:X.Y.Z [rails:X.Y] to generate one,
or pass explicit args: /ruby-upgrade-toolkit:fix ruby:X.Y.Z [rails:X.Y]
and exit.
Record the task ID — Step 8e will tick it off after a successful commit.
ruby -v 2>/dev/null || true
cat .ruby-version 2>/dev/null
grep "^ruby " Gemfile 2>/dev/null
bundle exec rails -v 2>/dev/null || true
Read Gemfile and Gemfile.lock.
.ruby-version to the exact target Ruby version (e.g. 3.3.1).ruby directive in Gemfile to "~> X.Y" (minor version pin)..tool-versions exists, update the Ruby line.# Confirm new Ruby is installed (user must have done this — if not, stop and instruct them)
ruby -v
# Expected output starts with: ruby X.Y.Z
If the active Ruby is not the target version, stop and tell the user:
"Please install Ruby X.Y.Z and activate it (
rbenv use X.Y.Zorrvm use X.Y.Z) then re-run this command."
bundle install 2>&1
Parse the output using these error patterns:
Error: "requires Ruby version"
Gem X requires Ruby >= A.B.C, current is X.Y.Z
Action: The gem has a minimum Ruby version constraint that is too high for the current Ruby OR too low for the target. Run: bundle update <gem_name>. If the latest version still requires a higher Ruby than available, the gem must be replaced (see compatibility matrix).
Error: "Could not find compatible versions"
Could not find compatible versions for gem '<name>':
In snapshot (Gemfile.lock): <name> = X.Y.Z was resolved to X.Y.Z, which depends on ...
Action: This is a cascading constraint conflict. Run:
bundle update <name_of_conflicting_gem> 2>&1 | tail -15
If the conflict involves Rails itself, ensure the Gemfile rails pin was updated in Step 2 first.
Error: "an error occurred while installing X" (native extension)
An error occurred while installing nokogiri (1.x.y), and Bundler cannot continue.
Action: The gem's native extension needs to be recompiled for the new Ruby. Run:
gem pristine <gem_name>
# if not cached, reinstall:
gem install <gem_name>
Error: "rake aborted" or build failure during install
rake aborted!
LoadError: cannot load such file -- <stdlib_lib>
Action: A gem's Rakefile requires a stdlib gem that is now separate in Ruby 3.4. Add the gem to Gemfile first, then retry bundle install.
General approach — update one gem at a time:
# Conservative single-gem update
bundle update <gem_name> 2>&1 | tail -10
# Verify no new conflicts introduced
bundle install 2>&1 | tail -5
For Ruby 3.4+, add any missing stdlib gems to Gemfile that were found during audit:
# Ruby 3.4 stdlib removals — now separate gems
gem "base64" # if used via require 'base64'
gem "csv" # if used via require 'csv'
gem "bigdecimal" # if used via require 'bigdecimal'
gem "ostruct" # if used via require 'ostruct'
Do not bulk-update all gems. Update one gem at a time until bundle install exits 0.
If scope: is given, restrict file searches to that path. Otherwise search app/, lib/.
For each method identified with a keyword argument mismatch:
Pattern A — hash passed where keywords expected:
# BEFORE: method signature uses **kwargs or keyword params
# Caller passes a plain hash as last positional arg
options = { timeout: 30 }
connect(options) # ArgumentError in Ruby 3.0
# AFTER: double-splat at call site
connect(**options)
Pattern B — keywords passed to method expecting positional hash:
Detection — find **hash at call sites where the method accepts options = {}:
grep -rEn "\*\*options|\*\*opts|\*\*params|\*\*kwargs" ${SCOPE:-app/ lib/} --include="*.rb" | grep -v "def "
For each match, read the method definition it is calling. If the method signature is def method_name(options = {}) or def method_name(opts = {}) (positional hash, NOT **), this is Pattern B:
# BEFORE: double-splat on a method that takes options = {}
process(**{key: "value"}) # ArgumentError in Ruby 3.0
process(**user_opts) # ArgumentError in Ruby 3.0 if process(opts={})
# AFTER: wrap literal hash in braces (positional), keep variable as-is
process({key: "value"}) # passes hash as positional argument
process(user_opts) # remove ** — pass hash directly as positional arg
If the method definition is NOT available (external gem method), check the gem's changelog for Ruby 3.0 compatibility. Do not apply Pattern B fix to external gem call sites — update the gem instead.
Read each affected file, identify which pattern applies, and apply the minimal fix. Run the file's tests after each file:
bundle exec rspec spec/path/to/file_spec.rb --no-color 2>&1 | tail -5
grep -rEn "YAML\.load\b|Psych\.load\b" ${SCOPE:-app/ lib/ config/} --include="*.rb"
For each occurrence, replace:
# BEFORE
data = YAML.load(content)
# AFTER (no custom classes needed)
data = YAML.safe_load(content)
# AFTER (custom classes needed — use permitted_classes)
data = YAML.safe_load(content, permitted_classes: [Date, Symbol, MyClass])
it block parameter conflict (warnings in 3.2–3.3, breaks in 3.4)Only run this section when upgrading to Ruby 3.4.
# Find `it` used as a block variable (receiver or assignment target)
grep -rEn "^\s*it\." ${SCOPE:-app/ lib/} --include="*.rb" 2>/dev/null | grep -v "^[[:space:]]*#"
# Broader search using Ruby for correct word-boundary detection (grep -P not available on macOS)
export SCOPE_DIR="${SCOPE:-app lib}"
ruby -r find -e '
Find.find(*ENV["SCOPE_DIR"].split) do |f|
next unless f.end_with?(".rb") && File.file?(f)
File.readlines(f).each_with_index do |line, i|
next if line =~ /^\s*#/
next if line =~ /\bit\s+['"'"'\"]/
next if line =~ /\b(bit|commit|submit|permit|limit|edit|visit|digit|habit|orbit)\b/
puts "#{f}:#{i+1}:#{line.chomp}" if line =~ /(?<![a-z_0-9])it(?![a-z_0-9?!])/
end
end
' 2>/dev/null
For each match, read 5 lines of context around it to confirm it is truly a block parameter variable (not an RSpec test call). When confirmed, rename it to a descriptive name based on what the block iterates over (e.g., it in items.each { it.save } → item).
If gems were added to Gemfile in Step 3, verify they load correctly:
bundle exec ruby -e "require 'base64'; puts 'ok'" 2>&1
rails: argument given)Skip this entire section if no rails: argument was provided.
Update the rails pin in Gemfile to '~> X.Y', then:
bundle update rails 2>&1 | tail -10
THOR_MERGE=cat bundle exec rails app:update 2>&1
Review each generated diff. Apply changes that:
Do NOT blindly accept diffs that override intentional customizations.
In config/application.rb, update:
config.load_defaults [TARGET_RAILS_VERSION]
Create a stub initializer to re-enable any defaults that break the test suite:
# config/initializers/new_framework_defaults_X_Y.rb
# Re-enable old defaults here as the app is updated to handle new ones
# Rails.application.config.old_default = old_value
If $CLAUDE_PLUGIN_ROOT/skills/rails-upgrade-guide/references/fix-patterns.md exists, read it for the full pattern table.
Apply these safe auto-fixes across the scope (or whole app/ if no scope):
| Pattern | Fix |
|---|---|
.update_attributes( | → .update( |
before_filter | → before_action |
after_filter | → after_action |
around_filter | → around_action |
redirect_to :back | → redirect_back(fallback_location: root_path) |
render text: | → render plain: |
require_dependency | remove the line |
find_by_<column>( | → find_by(<column>: |
enum status: [ | → enum :status, [ |
enum status: { | → enum :status, { |
response.success? | → response.successful? |
scope :name, where( | → scope :name, -> { where( + closing } |
For each file modified, read it first, apply the fixes precisely (only the deprecated patterns, nothing else), then run the file's tests:
bundle exec rspec spec/corresponding/file_spec.rb --no-color 2>&1 | tail -5
For has_and_belongs_to_many: present the join-model migration plan (create model, replace association, add id column) and wait for explicit user confirmation.
For open redirects (redirect_to params[...]) — these require security judgment, not just a syntax fix. For each occurrence:
grep -rn "redirect_to.*params\[" app/controllers/ --include="*.rb"
Read the surrounding code and present to the user:
Found open redirect candidate:
File: app/controllers/sessions_controller.rb:47
Code: redirect_to params[:return_to]
Security risk: if params[:return_to] is not validated, an attacker can craft
a URL like /login?return_to=https://evil.com to redirect users off-site.
Options:
A) Safe redirect (recommended): only allow relative paths
# Note: must exclude '//' prefix — browsers treat '//evil.com' as protocol-relative
path = params[:return_to].presence
redirect_to(path&.start_with?('/') && !path.start_with?('//') ? path : root_path)
B) Allowlist redirect: only allow specific domains
# Define ALLOWED_HOSTS in config/application.rb or ApplicationController:
# ALLOWED_HOSTS = %w[app.example.com staging.example.com].freeze
allowed_host = URI.parse(params[:return_to].to_s).host rescue nil
redirect_to(allowed_host && ALLOWED_HOSTS.include?(allowed_host) ? params[:return_to] : root_path)
C) Keep as-is (intentional external redirect — document why)
Which option? [A/B/C]
Apply the chosen fix. Do not auto-apply open redirect fixes without this confirmation.
Skip if not upgrading to Rails 7 or later.
grep -n "turbolinks" Gemfile 2>/dev/null | grep -v "^\s*#"
If found, follow $CLAUDE_PLUGIN_ROOT/skills/rails-upgrade-guide/references/turbo-stimulus-guide.md for the complete migration procedure. Key steps:
gem "turbo-rails" to Gemfile, bundle install, run bin/rails turbo:installgem "turbolinks" from Gemfile, bundle installturbolinks:load → turbo:load)data-turbolinks → data-turbo)In .rubocop.yml, ensure AllCops.TargetRubyVersion matches the target Ruby minor version:
AllCops:
TargetRubyVersion: X.Y
NewCops: enable
Before starting the fix loop, establish the baseline failure count. Use the failure count from the audit report if one was produced in this session. Otherwise, capture it now using the "Test suite — failure count" block in $CLAUDE_PLUGIN_ROOT/skills/rails-upgrade-guide/references/verification-suite.md.
Keep BASELINE_FAILURES in context throughout the iterative loop. When verifying and summarising later, report only failures above this baseline as upgrade-introduced regressions. If the baseline already had failures, document them separately as pre-existing and do not attempt to fix them unless the user explicitly asks.
Run the full test-suite command from the same reference (section "Test suite — full run") to get the current failure list:
For each failure:
bundle exec rspec spec/path/to/failing_spec.rb --no-color 2>&1 | tail -5
If a failure is caused by an incompatible gem version, update that gem:
bundle update <gem_name>
bundle exec rspec spec/path/to/failing_spec.rb --no-color 2>&1 | tail -5
If a failure cannot be traced to the upgrade (pre-existing bug), document it in the summary and do not attempt to fix it.
Use the "RuboCop — auto-correct loop" block in $CLAUDE_PLUGIN_ROOT/skills/rails-upgrade-guide/references/verification-suite.md — it covers the safe (-a) and unsafe (-A) auto-correct passes. After both passes, inspect remaining offenses with the "RuboCop — offense count (JSON)" block from the same reference.
For remaining offenses, fix each one manually:
bundle exec rubocop path/to/file.rb 2>&1
If RuboCop itself is outdated for the new Ruby:
bundle update rubocop rubocop-rails rubocop-rspec rubocop-performance
Throughout Steps 2–7, keep a running record of what was changed:
VERSION_PINS_CHANGED — set of files modified (.ruby-version, Gemfile, .tool-versions)GEM_UPDATES — list of {gem, from, to} entries from each bundle updateRUBY_FIXES — per-category counts (kwarg sites, YAML.load occurrences, it renames, stdlib additions)RAILS_FIXES — per-pattern counts (update_attributes, before_filter, etc.) and list of config files touchedLOAD_DEFAULTS_NEW — target Rails minor written to config/application.rb (if applicable)Load $CLAUDE_PLUGIN_ROOT/skills/status/SKILL.md and run it to produce the readiness tier (GREEN / YELLOW / RED). Compare test-suite failures against BASELINE_FAILURES — only new failures count as regressions.
If RULES_LOADED is true, interleave verification-gate rules with the
built-in gates per the engine's "Apply ordering" (verify) section:
timing: before gate matching this phase.timing: after gate matching this phase.For each gate: run the command, capture exit code + last 40 lines of
output.
[gate-id] PASS.required: true → tier becomes RED regardless of built-in
gates. Print the gate's id and last-40-lines output under a
⛔ Required gate failed: <id> header. Skip to 8b's RED branch.required: false → mark as advisory FAILED, continue. Does
not affect the tier. Recorded for the commit message.Record outcomes in RULE_GATE_RESULTS for Step 8c.
RED (new failures above baseline):
Print the full RSpec failure output for each new failure, then exit:
⛔ Phase did not reach GREEN. New failures: [N] (baseline was [BASELINE]).
Not committing — inspect the failures above and rerun /ruby-upgrade-toolkit:fix
when ready, or investigate manually.
Do NOT prompt for commit. Do NOT create a commit. Exit.
GREEN (tests pass at baseline, 0 new failures, 0 RuboCop offenses, 0 deprecations) or YELLOW (tests pass at baseline but warnings or offenses remain):
Proceed to the commit prompt.
Compose a message from the tracked changes:
chore(upgrade): ruby [TARGET_RUBY][ + rails TARGET_RAILS] phase
Version pins:
- .ruby-version: [OLD] → [TARGET_RUBY]
- Gemfile ruby directive: "~> X.Y"
- [.tool-versions: updated] (only if changed)
Gem updates:
- [gem]: [from] → [to]
- ...
Ruby code changes:
- Keyword argument fixes: [N] sites across [N] files
- YAML.load → YAML.safe_load: [N] occurrences
- [`it` variable renames: N] (omit if 0)
- [stdlib gems added: list] (omit if none)
Rails changes: (omit whole section if no rails: arg)
- config.load_defaults: [OLD] → [NEW]
- Deprecation fixes: [N] patterns, [N] occurrences
- [Open-redirect decisions: N (option A/B/C per site)]
- [Turbolinks → Turbo: migrated]
Custom rules applied: (omit whole section if RULES_LOADED is false OR no rules fired this phase)
- [<rule-id>] <one-line effect summary — file counts, gem diffs, gate result>
- ...
Verification:
- RSpec: [PASSING] examples, [F] failures ([BASELINE] pre-existing, 0 new)
- RuboCop: [N] offenses
- Deprecation warnings: [N]
- [Custom gate <id>: PASS (0 warnings)] (one line per rule gate, required)
- [Custom gate <id>: ADVISORY FAILED (N issues)] (omit required/advisory label if none)
- Tier: [GREEN|YELLOW]
Omit any section whose tracked list is empty.
Print the proposed message exactly as it will be committed, then ask:
━━━ Phase verification: [GREEN|YELLOW] ━━━
[message above]
Commit this now? [yes / edit / no]
git add -A scoped to files touched in Steps 2–5, never force) and run git commit -m "$(cat <<'EOF'...EOF)" with the message above.git commit -am '...' when ready."Never use --no-verify, --amend, or --force. If a pre-commit hook fails, surface the error and return to the prompt (treat as an edit opportunity).
Print a one-line confirmation with the short SHA. If this invocation resolved its target from the task list (i.e. the user passed next, or the explicit args happen to match a pending task's subject), mark that task complete via TodoWrite. Then produce the summary below.
## Upgrade Fix Summary
Date: [date]
Ruby: [old] → [new]
Rails: [old] → [new] (or "not upgraded")
Scope: [full project / path/to/scope]
Commit: [short SHA, or "skipped" if user chose no]
### Version Pins Updated
- .ruby-version: [old] → [new]
- Gemfile ruby directive: updated
- [.tool-versions: updated / not present]
### Gem Updates
| Gem | Old Version | New Version |
|-----|-------------|-------------|
### Ruby Code Changes
- Keyword argument fixes: [N] files, [N] occurrences
- YAML.load → safe_load: [N] occurrences
- [it variable renames: N]
- [stdlib gem additions: list]
### Rails Changes (if applicable)
- Deprecation fixes: [N] files, [N] patterns fixed
- Config updates: [list files changed]
- Framework defaults: load_defaults updated to [X.Y]
### RSpec
- Before: [N] failures
- After: [N] failures (should be 0)
### RuboCop
- Before: [N] offenses
- After: [N] offenses (should be 0)
### Manual Action Required
- CI/CD files still referencing old Ruby: [list paths]
- Dockerfiles still using old base image: [list paths]
- Pre-existing RSpec failures (not upgrade-related): [N]
- Complex patterns deferred for user review: [list]