Audition
Point it at a Ruby script, a gem, a Rack app, or a Rails root and it
tells you whether that code can run inside Ractors, why it cannot,
and how to fix it. Unlike a linter, audition does not stop at
pattern-matching your source: whole-program analysis is powered by
rubydex, Shopify's Ruby
indexer, and the target is also loaded in a sandboxed subprocess
to observe real Ractor::IsolationErrors on the live object
graph. Some of the checks and fixes were trained on how Rails
core itself is being ractorized; see the
pattern study.

- Three probes, one verdict. Per-file Prism AST checks,
whole-program semantic analysis powered by
rubydex (Shopify's code
graph; class-level state is resolved across files and
reopenings), and dynamic in-Ractor execution of the actual
target.
- Explains, not just flags. Every finding carries a
why
(which rule of the Ractor model it violates) and a fix
(what to write instead).
--fix like RuboCop, in two tiers. Safe corrections:
.freeze on string constants, Ractor.make_shareable(...) for
mutable and shallow-frozen containers and Proc constants, and
boot-time hoisting of method-body requires. --fix-unsafe adds
semantics-affecting rewrites: magic-comment insertion,
freeze-on-memoize for class-level memoization (both @x ||=
and return @x if defined?(@x) idioms keep their caching, the
memoized value becomes shareable, Rails-core style;
Ractor.store_if_absent only for block initializers and
invalidated caches), autoload to require, and write-once
globals/class variables to frozen constants. --dry-run
previews everything as a diff.
- Dependency-aware. Runtime findings are attributed to their
source via
const_source_location; when your own code is clean
but a dependency is not, the verdict is a distinct blocked
state, so globalid is not blamed for ActiveSupport's state.
- Dogfooding. The scanner for Ractor compatibility is built
using Ractors: static analysis fans out across CPU cores on
Ractor workers, and audition passes its own audit.
- Trained on Rails core. Several checks and fix suggestions
come straight from studying the Rails ractorization effort
(about 75 substantive commits):
Hash.new default procs,
in-place mutation of registry constants, closure-carrying
define_method, copy-on-write rewrites. The findings are
documented in
docs/rails_core_best_practices.md.
- Terminal-native output. Colors, glyphs, and OSC 8 hyperlinks;
path:line is clickable in supporting terminals. JSON output for
CI.
$ audition worker.rb
* audition 0.1.0 ruby 4.0.6 · script at .
worker.rb
x raises inside a Ractor: Ractor::IsolationError: can not
access global variable $jobs from non-main Ractor
why: The script ran fine on the main Ractor but failed under
Ractor.new; the static findings usually pinpoint the line.
x worker.rb:1 write to global variable $jobs
why: Non-main Ractors cannot access global variables; this
raises Ractor::IsolationError the moment the line executes
in a Ractor (verified on Ruby 4.0).
fix: Pass the value into the Ractor explicitly
(Ractor.new(value) { |v| ... }) or over a Ractor::Port; for
per-Ractor state use Ractor.current[:key].
x worker.rb:4 read of global variable $jobs
...
dynamic probes
x script probe failed (details above)
summary: 3 errors
verdict: x not ractor-ready
$ echo $?
1
And the whole-bundle view:
$ audition Gemfile.lock --static-only
╭───────────────┬─────────┬───────────┬────────┬──────────┬─────────╮
│ gem │ version │ verdict │ errors │ warnings │ fixable │
├───────────────┼─────────┼───────────┼────────┼──────────┼─────────┤
│ activesupport │ 8.1.0 │ not ready │ 157 │ 97 │ 77 │
│ i18n │ 1.14.7 │ not ready │ 48 │ 40 │ 45 │
│ mail │ 2.9.1 │ not ready │ 27 │ 4 │ 13 │
│ rack │ 3.2.6 │ not ready │ 23 │ 45 │ 60 │
│ ... │ │ │ │ │ │
╰───────────────┴─────────┴───────────┴────────┴──────────┴─────────╯
0 of 11 gems ractor-ready
Requires Ruby 4.0 or newer, strictly: the tool targets the modern
Ractor API (Ractor::Port, Ractor#value, main-Ractor require
proxying) and its verified semantics.