Keela ð
Your Ruby codebase's forensic investigator.
Like her namesake â the famous springer spaniel who helped solve cases by finding microscopic traces that eluded forensic teams â Keela sniffs out the dead code that grep missed.
ð 6 strategies: methods, scopes, constants, delegates, attrs, i18n
ðŊ Baseline mode â only bark at NEW dead code
ð JSON output for CI pipelines
Why Remove Unused Code?
Dead code isn't harmless â it's actively costly:
- Cognitive overhead: Developers read and try to understand code that doesn't matter, slowing down onboarding and feature work
- CI minutes: Tests for unused methods still run, burning compute time on every pipeline
- False confidence: Test coverage metrics include dead code, masking gaps in the code that actually runs
- Refactoring friction: Unused code creates dependencies that make refactoring harder ("wait, is this called somewhere?")
- Security surface: More code means more potential vulnerabilities, even in paths users never hit
Most codebases accumulate dead code gradually â a feature flag that's always on, a method replaced but never deleted, a scope that lost its last caller. Keela helps you find it and clean it up.
Installation
gem install keela
Or add to your Gemfile:
gem 'keela', group: :development
Quick Start
# First time: generate a baseline of current unused code
keela --update-baseline
# This creates .keela_baseline.yml in your project root
# From now on, just run:
keela
# Keela will fail if:
# - NEW unused code is detected (someone added dead code)
# - Previously unused code was REMOVED (time to update the baseline!)
How It Works
Keela operates in two modes:
Baseline Mode (Default)
If a .keela_baseline.yml file exists, Keela compares the current scan against it:
| Scenario | Result |
|---|---|
| No changes from baseline | â Pass (silent, exit 0) |
| NEW unused code detected | â Fail (shows new items) |
| Code REMOVED from baseline | â Fail (prompts to update baseline) |
This lets you gradually pay down tech debt while preventing new dead code from sneaking in.
The baseline file is organized by strategy:
# .keela_baseline.yml
methods:
app/models/user.rb:
- legacy_method
- old_callback
app/helpers/application_helper.rb:
- unused_helper
scopes:
app/models/user.rb:
- inactive
- archived
constants:
app/models/user.rb:
- OLD_STATUS
Note: The baseline file only stores names, not line numbers. This prevents false positives when code moves within a file.
Report Mode
If no baseline exists (or you use --report), Keela shows all unused code:
keela --report
What "Unused" Means
Keela defines "unused" as unused in production code â not just unused anywhere.
By default, Keela scans app/, lib/, and config/ directories. It intentionally
excludes spec/ and test/ directories because:
-
Tests aren't usage â Code that only exists to be tested isn't providing application value. If you delete the unused code, you delete its tests too.
-
Tests can cover dead code â A method with 100% test coverage can still be dead if nothing in the application calls it.
-
Cleaner signal â Including test files would hide genuinely unused code behind "but it has tests!" false negatives.
If Keela flags something that's only used in tests, consider whether the code
(and its tests) can be removed entirely. If the code is intentionally test-only
(e.g., test helpers defined in app/), add it to your exclusion file.
To include test directories in usage scanning (not recommended), use --include:
keela --include 'test/**/*.rb' --include 'spec/**/*.rb'
Command Line Options
# Scan for all unused code (methods, scopes, constants, delegations, attributes)
keela
# Scan for specific types
keela --type methods
keela --type scopes
# Combine multiple types
keela --type methods,scopes,constants
# Force report mode (ignore baseline)
keela --report
# Update the baseline file
keela --update-baseline
# Use a custom baseline path
keela --baseline config/unused_baseline.yml
# Specify excluded items file
keela --excluded config/keela_excluded.yml
# Custom file extensions
keela --extensions rb,rake,haml
# Exclude files matching patterns
keela --exclude 'vendor/**/*' --exclude 'tmp/**/*'
# Include additional directories (adds to defaults)
keela --include 'engines/**/*.rb' --include 'custom/**/*.rb'
# Use a custom config file
keela --config path/to/keela.yml
# Output as JSON (for CI integrations)
keela --format json
# Suppress all output (useful for CI and scripting, rely on exit code)
keela --quiet
keela -q
# Show verbose debugging output (files scanned, patterns used)
keela --verbose
# Show source location (file:line) for each unused item
keela --source-location
# Validate exclusion file entries (find stale exclusions)
keela --test-exclusions
# Show version
keela --version
Detection Strategies
Keela detects several types of unused code:
| Strategy | Detects | Example |
|---|---|---|
| methods | Unused method definitions | def unused_method |
| scopes | Unused ActiveRecord scopes | scope :unused_scope, -> { } |
| constants | Unused constants | UNUSED_CONSTANT = 'value' |
| delegations | Unused delegate declarations | delegate :unused, to: :target |
| attributes | Unused attr_* declarations | attr_accessor :unused_attr |
| i18n_keys | Unused translation keys | en.users.unused_key in locale YAML |
Run all strategies (default) or target specific ones with --type.
Note: The i18n_keys strategy is not included in --type all because it requires scanning YAML locale files. Run it explicitly with --type i18n_keys.
I18n Keys (Beta)
The i18n_keys strategy is beta and may produce false positives. It cannot detect:
- Lazy lookup -
t('.title')in views resolves based on the view path - Dynamic keys -
t("users.#{action}.title")with interpolated segments - Model translations -
User.human_attribute_name(:email)andUser.model_name.human - Pluralization siblings - If
one:is used,other:may appear unused
Review results carefully and use the exclusion file for known false positives.
Configuration File
Create a keela.yml or .keela.yml in your project root:
# keela.yml
extensions:
- rb
- haml
- erb
exclude_patterns:
- "vendor/**/*"
- "tmp/**/*"
include_patterns:
- "engines/**/*.%<ext>s"
excluded_path: ".keela_excluded.yml"
baseline_path: ".keela_baseline.yml"
# Per-strategy configuration
strategies:
methods:
definition_paths:
- app/helpers
- app/models
- lib/
Keela automatically loads keela.yml or .keela.yml from the current directory. Use --config to specify a different path.
Customizing Definition Paths Per Strategy
By default, each strategy looks for definitions in specific directories (e.g., methods looks in app/helpers and app/models). You can customize this per-strategy:
# keela.yml
strategies:
methods:
definition_paths:
- app/helpers
- app/models
- lib/
- ee/app/models
scopes:
definition_paths:
- app/models
- ee/app/models
This is useful when your project has code in non-standard locations (like lib/ or enterprise edition directories) that you want Keela to check for unused definitions.
Customizing Which Files to Scan
There are two approaches:
1. Tweak the defaults with --include and --exclude (or include_patterns/exclude_patterns in config):
# Add engines/ to the default app/, lib/, config/ directories
keela --include 'engines/**/*.rb'
# Exclude vendor files from scanning
keela --exclude 'vendor/**/*'
2. Full control with directory_patterns - replaces the defaults entirely:
# keela.yml - scan ONLY these directories
directory_patterns:
- "src/**/*.%<ext>s"
- "custom/**/*.%<ext>s"
Use directory_patterns when you need complete control. Use --include/--exclude when you just want to tweak the defaults.
Note: Mixing both approaches raises a ConfigurationError. Choose one or the other.
Available options:
| Key | Description | Default |
|---|---|---|
directory_patterns |
Glob patterns for files to scan (replaces defaults) | app/, lib/, config/ |
extensions |
File extensions to scan | rb, haml, erb |
include_patterns |
Additional patterns to scan (added to defaults) | [] |
exclude_patterns |
Patterns for files to exclude | [] |
excluded_path |
Path to YAML file of excluded items | nil |
baseline_path |
Path to baseline YAML file | .keela_baseline.yml |
required_directory |
Directory that must exist for scanning to proceed | nil |
CI Integration
Keela is designed for CI pipelines. Add it to catch dead code before it merges:
# .gitlab-ci.yml
unused_code:
script:
- bundle exec keela
rules:
- if: $CI_MERGE_REQUEST_IID
# .github/workflows/ci.yml
- name: Check for unused code
run: bundle exec keela
The workflow:
- Initial setup: Run
keela --update-baselineand commit.keela_baseline.yml - CI runs:
keelacompares against baseline, fails on new dead code - After cleanup: Run
keela --update-baselineto update the baseline
Structured Output Formats
Use --format for machine-readable output:
keela --format json --report # JSON output
keela --format toon --report # TOON output (token-efficient for LLMs)
JSON
{
"strategies": ["methods", "scopes"],
"unused": {
"methods": {
"app/models/user.rb": ["unused_method", "old_helper"]
},
"scopes": {
"app/models/post.rb": ["inactive"]
}
},
"summary": {
"total": 3,
"by_strategy": {
"methods": 2,
"scopes": 1
}
}
}
TOON
TOON (Token-Oriented Object Notation) is a compact format optimized for LLM prompts, using ~40-50% fewer tokens than JSON:
strategies[2]: methods,scopes
unused:
methods:
"app/models/user.rb"[2]: unused_method,old_helper
scopes:
"app/models/post.rb"[1]: inactive
summary:
total: 3
by_strategy:
methods: 2
scopes: 1
These formats are useful for integrating with other tools, generating reports, processing results programmatically, or including in LLM context windows.
Exclusion File
Some code appears unused but is actually called dynamically. Exclude it using the strategy-aware format (recommended):
# .keela/excluded.yml (strategy-aware format)
methods:
app/models/user.rb:
- legacy_method: "Called via metaprogramming"
- callback_method: "Used as ActiveRecord callback"
app/helpers/application_helper.rb:
- helper_method: "Called from views dynamically"
scopes:
app/models/user.rb:
- active: "Called dynamically via send()"
This format mirrors the baseline file structure and allows you to exclude the same name in one strategy while flagging it in another. For example, if you have both scope :active and def active in the same file, you can exclude them independently.
Legacy Flat Format
The flat format is still supported for backward compatibility:
# .keela_excluded.yml (legacy flat format)
app/models/user.rb:
- legacy_method: "Called via metaprogramming"
- callback_method: "Used as ActiveRecord callback"
Note: With the flat format, an exclusion applies to ALL strategies. If you exclude active, both the method and scope named active will be excluded.
Usage
keela --excluded .keela/excluded.yml
Or configure in keela.yml:
excluded_path: ".keela/excluded.yml"
Ruby API
require 'keela'
# Configure Keela
Keela.configure do |config|
config.extensions = %w[rb haml erb]
config.directory_patterns = %w[
app/**/*.%<ext>s
lib/**/*.%<ext>s
]
config.include_patterns = %w[engines/**/*.%<ext>s]
config.exclude_patterns = %w[vendor/**/* tmp/**/*]
config.excluded_path = '.keela_excluded.yml'
config.baseline_path = '.keela_baseline.yml'
end
# Run a scan
strategy = Keela::Strategies::Methods.new
scanner = Keela::Scanner.new(strategy: strategy)
success = scanner.run
# Access results
scanner.unused_collection # Hash of file => [unused_names]
scanner.new_unused # Items not in baseline
scanner.removed # Items in baseline but no longer unused
Custom Strategies
Detect other patterns by creating your own strategy:
class CallbackStrategy < Keela::Strategy
def name
"callbacks"
end
def definition_file_pattern
%r{app/models}
end
def extract_definition(line)
# Match: before_save :do_something
line =~ /(?:before|after|around)_\w+\s+:(\w+)/ ? Regexp.last_match(1) : nil
end
def usage_regex(name)
/def #{Regexp.quote(name)}\b/
end
def skip_comments?
true
end
end
scanner = Keela::Scanner.new(strategy: CallbackStrategy.new)
scanner.run(force_report: true)
About the Name
Keela was a famous English Springer Spaniel known as the "CSI dog." She could detect microscopic traces of blood that other forensic methods missed, helping solve cases that had gone cold for decades. Her nose was so sensitive she found evidence that luminol and DNA testing couldn't.
Like her namesake, this gem finds the dead code that grep and your IDE missed.
Good girl, Keela. ðĶī
License
MIT License. See LICENSE.txt.