---
title: Audition
description: Auditions Ruby scripts, gems, Rack apps, and Rails applications for Ractor-readiness—static analysis of isolation violations plus dynamic in-Ractor probing, with explanations and fixes.
github: https://github.com/yaroslav/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](https://github.com/Shopify/rubydex), Shopify's Ruby
indexer, and the target is also loaded in a sandboxed subprocess
to observe real `Ractor::IsolationError`s 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](https://github.com/yaroslav/audition/blob/main/docs/rails_core_best_practices.md).

- **Three probes, one verdict.** Per-file Prism AST checks,
  whole-program semantic analysis powered by
  [rubydex](https://github.com/Shopify/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](https://github.com/yaroslav/audition/blob/main/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.

```console
$ 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:

```console
$ 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.

> **Warning:** The entire codebase was written by Claude Fable 5 (Anthropic).
> It has a thorough spec suite and was validated against real
> gems, but no human has reviewed every line. Be wary; read before
> you trust, especially `--fix` rewrites.

## Installation

```console
gem install audition
```

Or in a Gemfile:

```ruby
gem "audition", require: false
```

## Usage

```console
audition worker.rb            # a script: static + run inside Ractor
audition my_gem               # an installed gem, by name
audition path/to/gem-checkout # a gem working copy (*.gemspec)
audition path/to/rack-app     # a config.ru directory
audition path/to/rails-root   # a Rails application
audition lib                  # any directory, static-only
audition Gemfile.lock         # sweep every gem in the bundle
audition path/to/app --deps   # same, from the app root
```

Useful flags:

| Flag | Effect |
| --- | --- |
| `--deps` | sweep the target's Gemfile.lock gem by gem |
| `--write-baseline` / `--no-baseline` | record / ignore known findings |
| `--fix` | apply safe corrections, then re-check |
| `--fix-unsafe` | also apply semantics-affecting corrections |
| `--dry-run` | with a fix flag: preview edits, change nothing |
| `--format json` | machine-readable report for CI |
| `--format github` | GitHub Actions annotations on PR diffs |
| `--compare old.json` | delta vs a previous report: fixed/introduced |
| `--static-only` / `--dynamic-only` | pick one probe layer |
| `--fail-on warning` | stricter CI gate (default: error) |
| `--capabilities` | table of what this Ruby allows in Ractors |
| `--timeout 60` | dynamic probe budget in seconds |
| `--plain` | no colors or hyperlinks (also via NO_COLOR, pipes) |

Exit codes: `0` clean, `1` findings at or above the `--fail-on`
threshold (or a failed dynamic probe), `2` usage error.

## Adopting incrementally

Nobody goes from 150 findings to zero in one commit. Three tools
keep the gate useful from day one:

**Baseline.** Record today's findings, then fail CI only on new
ones:

```console
audition . --write-baseline    # writes .audition-baseline.json
audition .                     # exit 0; summary shows "N baselined"
```

The ledger stores per-check-per-file counts, so line drift never
invalidates it. `--no-baseline` shows everything again.

**Inline pragmas.** Silence a single line, rubocop-style:

```ruby
$legacy_flag = true # audition:disable global-variables
risky_call          # audition:disable
```

**Project config.** `.audition.yml` at the target root
(CLI flags always win):

```yaml
fail_on: warning
timeout: 60
exclude:
  - legacy/**
  - db/schema.rb
checks:
  disable:
    - at-exit
```

## What it catches

Static, with file:line precision:

- **Global variables**, with a verified allowlist: `$stdout`, `$~`,
  `$!`, `$VERBOSE` writes and friends stay legal.
- **Class variables**, resolved on the rubydex graph.
- **Class-level instance variables**, unified across the class
  body, `def self.`, and `class << self`, across files; the classic
  `@cache ||= {}` and `return @x if defined?(@x)` memoizations.
- **Constants that are not deeply shareable**: bare mutable
  literals, interpolated strings, and the subtle shallow freeze
  (`[[1], [2]].freeze` still raises; audition explains why).
  Honors `# frozen_string_literal:` and
  `# shareable_constant_value:` magic comments.
- **Sync primitives and Procs in constants** (Mutex, Queue,
  lambdas), including `Hash.new { }` default procs, which stay
  unshareable even after `.freeze`.
- **Registry-style constant mutation** (`RENDERERS << key`,
  `LOOKUP[k] = v`) and **`define_method` with a literal block**
  (the method carries an unshareable Proc); both patterns and
  their fixes come from the Rails core ractorization study.
- **Runtime require and autoload** (serializes all Ractors through
  the main-Ractor proxy).
- **`Ractor.new` blocks capturing outer locals** (the ArgumentError
  at creation time), resolved through Prism's exact scope depths.
- **Hostile or removed APIs**: `Ractor.yield`/`take` (gone in 4.0),
  ActiveSupport `class_attribute`/`cattr_*`/`mattr_*`,
  `include Singleton`, `fork`, `ObjectSpace._id2ref`, ENV mutation.

Dynamic, on the live object graph:

- Runs scripts inside a real Ractor (via `load`, which is not
  proxied) and reports the actual exception.
- Requires a library, then sweeps every constant it introduced with
  `Ractor.shareable?`, and inspects every class and module for
  class-level ivars and class variables, with
  `const_source_location` attribution.
- Boots `config.ru` and serves one GET / entirely inside a Ractor,
  the per-worker model of Ractor web servers; then hammers it from
  4 Ractors x 25 requests to surface failures that only appear
  under concurrency.
- Boots Rails (`config/environment.rb`), eager-loads, and sweeps
  the application's namespaces.

## Agent skill

This repository ships a `ractor-readiness` skill that teaches
coding agents (Claude Code and friends) the full audition
workflow: audit, fix tiers, suite-parity verification, and
incremental adoption. It lives in
[skills/ractor-readiness/SKILL.md](https://github.com/yaroslav/audition/blob/main/skills/ractor-readiness/SKILL.md).

Install into Claude Code as a plugin:

```
/plugin marketplace add yaroslav/audition
/plugin install audition@audition
```

Or install the skill with the
[skills CLI](https://github.com/vercel-labs/skills):

```console
$ npx skills add yaroslav/audition
```

## Extending

Checks are written in a small declarative DSL and can be registered
from outside the gem:

```ruby
class NoSleep < Audition::Static::Checks::Base
  check_name "no-sleep"

  explain :sleepy,
          severity: :warning,
          message: "sleep inside potential Ractor code",
          why: "Blocking one Ractor blocks its whole OS thread.",
          fix: "Prefer Ractor::Port#receive with a timeout."

  on :call_node do |node|
    flag(node, :sleepy) if node.name == :sleep && !node.receiver
  end
end

Audition::Static::Checks.register(NoSleep)
```

`on` generates the Prism visitor and always continues traversal;
`explain` entries are a message catalog with `%{placeholders}`.

## Development

```console
bundle install
bundle exec rake spec       # RSpec suite
bundle exec rake standard   # standardrb lint
lefthook install            # pre-commit lint hook
bundle exec exe/audition --capabilities
```

Static scanning is Ractor-parallel on large targets (one worker
per core, minus one for the main Ractor); audition's own `lib/`
passes `audition lib` clean.

The design notes in `docs/design.md` include the empirically
verified Ruby 4.0 Ractor semantics table that the checks are
calibrated against.

## Acknowledgements

The whole-program checks stand on
[rubydex](https://github.com/Shopify/rubydex), Shopify's
high-performance static analysis suite for Ruby: audition feeds
every file into its graph and reads state ownership back out.
Thanks to its authors, in particular the top five contributors:
[Alexandre Terrasa](https://github.com/Morriar),
[Vinicius Stock](https://github.com/vinistock),
[Alex Rocha](https://github.com/alexcrocha),
[Stan Lo](https://github.com/st0012), and
[Soutaro Matsumoto](https://github.com/soutaro).

## Assisted by

Claude Fable 5.

## License

MIT. See LICENSE.txt.
