| name | Upgrade Fix |
| description | 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. |
| argument-hint | next | ruby:X.Y.Z [rails:X.Y] [scope:path] |
| allowed-tools | Read, Edit, Bash, Glob, Grep, TodoWrite |
| version | 0.4.0 |
Upgrade Fix
Apply all upgrade changes end-to-end: version pins, gem dependencies, code fixes, Rails config (if Rails), iterative RSpec green, iterative RuboCop green.
Argument Parsing
Two modes:
Explicit mode:
ruby:X.Y.Z — required target Ruby version
rails:X.Y — optional target Rails version
scope: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]'.
Step 0: Load custom rules (if present)
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:
- Schema errors abort fix before any file changes (suggest running
/ruby-upgrade-toolkit:rules validate).
- Apply the "Preflight (credentials)" check for the rules that match this
phase (via the engine's "Rule resolution" section). Missing credential
env vars for matched rules abort fix with the preflight error template.
- Remember the filtered rule set for use in Steps 2–8.
The engine's "Apply ordering" section governs interleaving of rules with
built-in steps. Specifically, within this phase:
- Run
phase-inject rules (timing: before) matched to this phase.
- Execute built-in Steps 2–5 (version pins, gem resolution, Ruby code
fixes, Rails fixes).
- After built-in apply steps and before Step 6 (RSpec), run
code-transform rules matched to this phase, in declared order.
- Run
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).
- Run
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.
Step 1b: Resolve target in next mode
Skip 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.
Step 1: Detect Current Versions
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.
Step 2: Update Ruby Version Pins
- Update
.ruby-version to the exact target Ruby version (e.g. 3.3.1).
- Update the
ruby directive in Gemfile to "~> X.Y" (minor version pin).
- If
.tool-versions exists, update the Ruby line.
ruby -v
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.Z or rvm use X.Y.Z) then re-run this command."
Step 3: Fix Gem Incompatibilities
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>
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:
bundle update <gem_name> 2>&1 | tail -10
bundle install 2>&1 | tail -5
For Ruby 3.4+, add any missing stdlib gems to Gemfile that were found during audit:
gem "base64"
gem "csv"
gem "bigdecimal"
gem "ostruct"
Do not bulk-update all gems. Update one gem at a time until bundle install exits 0.
Step 4: Apply Ruby Version-Specific Code Fixes
If scope: is given, restrict file searches to that path. Otherwise search app/, lib/.
2.7 → 3.0: Keyword argument separation
For each method identified with a keyword argument mismatch:
Pattern A — hash passed where keywords expected:
options = { timeout: 30 }
connect(options)
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:
process(**{key: "value"})
process(**user_opts)
process({key: "value"})
process(user_opts)
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
Any version: YAML.load → YAML.safe_load
grep -rEn "YAML\.load\b|Psych\.load\b" ${SCOPE:-app/ lib/ config/} --include="*.rb"
For each occurrence, replace:
data = YAML.load(content)
data = YAML.safe_load(content)
data = YAML.safe_load(content, permitted_classes: [Date, Symbol, MyClass])
→ 3.4: it block parameter conflict (warnings in 3.2–3.3, breaks in 3.4)
Only run this section when upgrading to Ruby 3.4.
grep -rEn "^\s*it\." ${SCOPE:-app/ lib/} --include="*.rb" 2>/dev/null | grep -v "^[[:space:]]*#"
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).
3.3 → 3.4: stdlib gem removals
If gems were added to Gemfile in Step 3, verify they load correctly:
bundle exec ruby -e "require 'base64'; puts 'ok'" 2>&1
Step 5: Apply Rails Fixes (if rails: argument given)
Skip this entire section if no rails: argument was provided.
5a. Update Rails gem
Update the rails pin in Gemfile to '~> X.Y', then:
bundle update rails 2>&1 | tail -10
5b. Run bin/rails app:update
THOR_MERGE=cat bundle exec rails app:update 2>&1
Review each generated diff. Apply changes that:
- Add new configuration options
- Update deprecated option names
- Remove options that no longer exist
Do NOT blindly accept diffs that override intentional customizations.
5c. Update framework defaults
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:
5d. Apply Rails deprecation fixes
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.
5e. Migrate Turbolinks → Turbo (Rails 7+ only)
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:
- Add
gem "turbo-rails" to Gemfile, bundle install, run bin/rails turbo:install
- Remove
gem "turbolinks" from Gemfile, bundle install
- Update JavaScript event listeners (
turbolinks:load → turbo:load)
- Update data attributes (
data-turbolinks → data-turbo)
- Run RSpec to catch any view/JS integration regressions
5f. Update RuboCop target version
In .rubocop.yml, ensure AllCops.TargetRubyVersion matches the target Ruby minor version:
AllCops:
TargetRubyVersion: X.Y
NewCops: enable
Step 6: Iterative RSpec Loop
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:
- Read the error and backtrace.
- Read the failing spec and the source file it tests.
- Determine: Ruby breaking change, gem API change, Rails change, or pre-existing bug?
- Apply the minimal fix to the source file (not the spec, unless the spec itself uses a deprecated pattern).
- Verify the fix:
bundle exec rspec spec/path/to/failing_spec.rb --no-color 2>&1 | tail -5
- Repeat until the full suite is green.
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.
Step 7: Iterative RuboCop Loop
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:
- Read the file and the offense message.
- Apply the minimal fix using Edit.
- Verify:
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
Step 8: Verify Phase and Prompt for Commit
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 update
RUBY_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 touched
LOAD_DEFAULTS_NEW — target Rails minor written to config/application.rb (if applicable)
8a. Run verification
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:
- Run each
timing: before gate matching this phase.
- Run the built-in status gates (RSpec, deprecations, RuboCop, Zeitwerk).
- Run each
timing: after gate matching this phase.
For each gate: run the command, capture exit code + last 40 lines of
output.
- Exit 0 → pass; record as
[gate-id] PASS.
- Non-zero +
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.
- Non-zero +
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.
8b. Decide by tier
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.
8c. Build the proposed commit message
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.
8d. Prompt the user
Print the proposed message exactly as it will be committed, then ask:
━━━ Phase verification: [GREEN|YELLOW] ━━━
[message above]
Commit this now? [yes / edit / no]
- yes — stage all tracked changes (
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.
- edit — accept a revised message from the user, then commit with that text. Preserve the "chore(upgrade)" prefix unless the user deliberately overrides it.
- no — skip the commit. Print: "Commit skipped — your working tree is dirty. Run
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).
8e. After committing
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.
Step 9: Produce Summary
## 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]