A CLI that highlights gems you can likely remove from a Ruby project's Gemfile, checks license compliance, detects archived, unmaintained, or deprecated dependencies, and reports vulnerable gems.
GemXray combines eight analyzers to find issues in your Gemfile and lockfile:
| Analyzer | What it detects | Default |
|---|---|---|
unused |
No require, constant reference, gemspec dependency, or Rails autoload signal was found. |
On |
redundant |
Another top-level gem already brings the gem in through Gemfile.lock. |
On |
version |
The gem is already covered by your Ruby or Rails version (default/bundled gem). | On |
license |
Gem license is not in the configured allowed list or is unknown. | On |
archive |
Gem's source repository on GitHub has been archived. | On |
security |
A locked gem version matches a known ruby-advisory-db vulnerability. | On |
deprecated |
RubyGems marks the version yanked, post-install metadata says deprecated, or README says "This gem is deprecated". | On |
unmaintained |
GitHub source repository has had no commits or releases for the configured threshold. | On |
Add the gem to your toolchain:
bundle add gemxray --group development
Or install it directly:
gem install gemxray
If you install the gem globally, replace bundle exec gemxray with gemxray in the examples below.
Generate a starter config:
bundle exec gemxray initScan the current project:
bundle exec gemxray scanUse structured output for CI or scripts:
bundle exec gemxray scan --format json --ci --fail-on danger bundle exec gemxray scan --only unused,version --severity warning bundle exec gemxray scan --only license,archive,security,deprecated,unmaintained
Preview or apply Gemfile cleanup:
bundle exec gemxray clean --dry-run bundle exec gemxray clean bundle exec gemxray clean --auto-fix
Create a cleanup branch and open a pull request:
bundle exec gemxray pr bundle exec gemxray pr --per-gem --no-bundle
Target a different project by passing a Gemfile path:
bundle exec gemxray scan --gemfile path/to/GemfileIf you run gemxray without a command, it behaves as gemxray scan.
| Command | Purpose | Useful options |
|---|---|---|
scan |
Analyze the Gemfile and print findings. | --format, --only, --severity, --ci, --fail-on, --gemfile, --config |
clean |
Remove selected gems from Gemfile. |
--dry-run, --auto-fix, --comment, --[no-]bundle |
pr |
Create a branch, commit the cleanup, and open a GitHub PR. | --per-gem, --[no-]bundle, --comment |
licenses |
List licenses for all gems in the Gemfile. | --format, --gemfile, --config |
init |
Write a starter .gemxray.yml. |
--force |
version |
Print the installed gemxray version. | none |
help |
Print top-level help. | none |
scan, clean, and pr all build the same report first, so the options below change which findings are available to print, remove, or turn into pull requests.
--gemfile PATHSelects the target Gemfile. This also changes the project root used for file edits,bundle install, and git operations, because the project root is derived from the Gemfile directory.--config PATHLoads a specific.gemxray.ymlinstead of the default file in the current working directory.--only unused,redundant,version,license,archive,security,deprecated,unmaintainedRestricts analysis to the listed analyzers. This accepts a comma-separated list. For example,--only unusedmeanscleanandpronly act on unused-gem findings. Using--onlywithlicense,archive,security,deprecated, orunmaintainedenables those analyzers even if they are not enabled in config.--severity info|warning|dangerFilters the report to findings at or above the selected severity. This happens before command-specific behavior, so hidden findings are also excluded fromclean,pr, andscan --ci.--format terminal|json|yamlControls output format forscan.cleanandprcurrently accept the option because they share the same parser, but they do not render the report, so--formathas no visible effect on those commands today.--ciOnly changesscan. When enabled,scanexits with status1if any reported finding matches--fail-onorci_fail_onfrom config.cleanandprcurrently accept the flag but do not use it.--fail-on info|warning|dangerOnly changesscan, and only matters together with--ci. It sets the minimum reported severity that should return exit code1.cleanandprcurrently accept the flag but do not use it.-h,--helpPrints help for the current command and exits with status0.
scan analyzes the target project, formats the resulting report, prints it to standard output, and exits without changing any files.
Behavior:
- It runs the selected analyzers, merges findings per gem, applies severity overrides, filters the report by
--severity, and sorts results by severity and gem name. - With
--format terminal, it prints a human-readable tree. Withjsonoryaml, it prints machine-readable output including summary counts. - Without
--ci, a successful scan exits with status0even if findings exist. - With
--ci, the exit status becomes1when any reported finding reaches--fail-onorci_fail_on.
bundle exec gemxray scan bundle exec gemxray scan --format json --ci --fail-on danger bundle exec gemxray scan --only unused --severity warning
clean runs the same analysis pipeline as scan, then edits the Gemfile based on cleanup-candidate results.
Behavior:
- Without
--auto-fix, it prompts once per cleanup candidate:Remove <gem> (<severity>)? [y/N]:. - Only
yandyesremove the gem. Any other answer skips it. - It edits the full detected source range, so multiline gem declarations are removed as a unit.
- It writes a backup file at
Gemfile.bakbefore saving changes. - If nothing is selected, it prints
No removable gems were selected.and exits with status0.
Command-specific options:
--auto-fix-- Skips prompts and removes every cleanup-candidatedangerfinding automatically.warning,info, security-only findings, deprecated-only findings, and unmaintained-only findings are never auto-removed.--dry-run-- Does not write the Gemfile. Instead, it prints the selected candidates and a preview hunk showing the lines that would be removed or replaced.--comment-- Replaces each removed gem entry with a comment such as# Removed by gemxray: ...instead of deleting the lines outright.--bundle,--no-bundle-- After a real edit,--bundlerunsbundle installin the target project. It is skipped automatically during--dry-runand when no gems were actually removed.
bundle exec gemxray clean bundle exec gemxray clean --auto-fix --severity danger bundle exec gemxray clean --dry-run --comment
pr runs the same analysis pipeline as scan, edits the Gemfile, commits the changes on a new branch, pushes the branch, and opens a GitHub pull request.
Behavior:
- It fails if no cleanup candidates remain after filters are applied.
- It requires the target project to be inside a git repository with a clean worktree before it starts.
- It switches to
github.base_branch, creates a cleanup branch, edits the Gemfile, commits the change, optionally refreshesGemfile.lock, pushes the branch, and opens a PR. - It tries
gh pr createfirst. Ifghis unavailable, it falls back to the GitHub API whenGH_TOKENorGITHUB_TOKENis set. - The PR body includes removed gems, detection reasons, and a short checklist.
Command-specific options:
--per-gem-- Creates one branch and one pull request per cleanup candidate instead of grouping everything into a single cleanup PR.--comment-- Leaves comments in the Gemfile instead of deleting lines, using the same replacement behavior asclean --comment.--bundle,--no-bundle-- Controls whetherprrunsbundle installbefore committing. The default is--bundle(fromgithub.bundle_install: true).
bundle exec gemxray pr bundle exec gemxray pr --per-gem --no-bundle bundle exec gemxray pr --only unused --severity danger
licenses shows the license of every gem declared in the Gemfile.
Behavior:
- It parses the Gemfile and fetches license metadata for each gem, first from the locally installed gemspec and then from the RubyGems API as a fallback.
- With
--format terminal(default), it prints a human-readable table with gem name, version, and license(s). - With
--format jsonor--format yaml, it prints machine-readable output including source and homepage. - Gems with no license metadata are shown as
(unknown).
bundle exec gemxray licenses bundle exec gemxray licenses --format json bundle exec gemxray licenses --gemfile path/to/Gemfile
init writes a starter .gemxray.yml into the current working directory.
- It does not read
--config; it always writes.gemxray.ymlin the directory where you run the command. - If the file already exists, the command fails unless you pass
--force.
bundle exec gemxray init bundle exec gemxray init --force
Prints the installed gemxray version and exits with status 0.
bundle exec gemxray versionPrints the top-level command summary and exits with status 0.
bundle exec gemxray help bundle exec gemxray --help bundle exec gemxray scan --help
| Level | Meaning | Auto-fix target |
|---|---|---|
danger |
High-confidence removal candidate, license violation, known vulnerability, yanked gem version, or unknown license (when deny_unknown is enabled). |
Yes for cleanup candidates; security/deprecated/unmaintained-only findings are not auto-removed |
warning |
Likely removable, archived repository, unmaintained repository, deprecated gem, or unknown license. Worth a quick review. | No |
info |
Informative hint (pinned versions, lower-confidence redundancy). | No |
gemxray reads .gemxray.yml from the working directory unless you pass --config PATH.
The effective config is built in this order:
- Built-in defaults
.gemxray.yml- CLI options for the current run
Later scalar values override earlier ones. Array values (scan_dirs, whitelist, github.labels, github.reviewers) are merged and deduplicated.
version: 1 ci: false ci_fail_on: warning whitelist: - bootsnap - tzinfo-data scan_dirs: - engines/billing/app - engines/billing/lib redundant_depth: 2 overrides: puma: severity: ignore github: base_branch: main labels: - dependencies - cleanup reviewers: [] per_gem: false bundle_install: true license: enabled: false allowed: - MIT - Apache-2.0 - BSD-2-Clause - BSD-3-Clause - ISC - Ruby deny_unknown: false archive: enabled: false github_token_env: GITHUB_TOKEN security: enabled: false advisory_db_path: github_token_env: GITHUB_TOKEN cache_ttl: 86400 deprecated: enabled: false check_readme: true unmaintained: enabled: false threshold_days: 730 github_token_env: GITHUB_TOKEN overrides: {}
| Field | Default | Description |
|---|---|---|
version |
1 |
Schema marker for future compatibility. Currently accepted but does not change behavior. |
gemfile_path |
Gemfile |
Path to the target Gemfile. Expanded from the current working directory. |
format |
terminal |
Output format for scan. Accepted: terminal, json, yaml. |
only |
all | Restricts analysis to listed analyzers: unused, redundant, version, license, archive, security, deprecated, unmaintained. |
severity |
info |
Minimum severity kept in the report. Also limits what clean, pr, and scan --ci can act on. |
ci |
false |
Enables CI-style exit codes for scan. |
ci_fail_on |
warning |
Minimum severity that makes scan --ci exit with status 1. |
auto_fix |
false |
When true, clean removes cleanup-candidate danger findings without prompting. |
dry_run |
false |
When true, clean previews changes without writing the Gemfile. |
comment |
false |
When true, gem entries are replaced with comments instead of being deleted. |
bundle_install |
false |
When true, clean runs bundle install after editing. Does not affect pr. |
whitelist |
[] |
Gem names to skip completely. |
scan_dirs |
[] |
Extra directories added to the built-in scan roots (app, lib, config, db, script, bin, exe, spec, test, tasks). |
redundant_depth |
2 |
Maximum dependency depth for the redundant analyzer in Gemfile.lock. |
overrides |
{} |
Per-gem overrides keyed by gem name. |
Security-only, deprecated-only, and unmaintained-only findings are reported by scan but are not treated as Gemfile cleanup candidates by clean or pr. Update the affected gem, update the parent dependency that brings it in, replace it with a maintained alternative, or remove it manually if it is also unused.
overrides.<gem>.severity accepts ignore, info, warning, or danger.
ignoreskips the gem before analysis (no finding is produced).info,warning,dangerforce the final reported severity after analyzers run.
| Field | Default | Description |
|---|---|---|
github.base_branch |
main |
Base branch that pr checks out before creating the cleanup branch. |
github.labels |
["dependencies", "cleanup"] |
Labels applied to created PRs. Custom labels are added to defaults (arrays are merged). |
github.reviewers |
[] |
Reviewers requested on created PRs. |
github.per_gem |
false |
When true, pr creates one branch and one PR per gem. |
github.bundle_install |
true |
Controls whether pr runs bundle install before committing. |
The license analyzer runs by default. Disable it with --only unused,redundant,version or set license.enabled: false in config.
| Field | Default | Description |
|---|---|---|
license.enabled |
true |
Include the license analyzer in default scans. |
license.allowed |
[] |
SPDX identifiers or license names that are permitted. Matching is case-insensitive and uses fingerprint normalization (e.g. "The MIT License" matches "MIT"). When empty, only unknown-license detection applies. |
license.deny_unknown |
false |
When true, gems with no license metadata are reported as danger instead of warning. |
The archive analyzer runs by default. Disable it with --only unused,redundant,version or set archive.enabled: false in config.
| Field | Default | Description |
|---|---|---|
archive.enabled |
true |
Include the archive analyzer in default scans. |
archive.github_token_env |
GITHUB_TOKEN |
Name of the environment variable containing a GitHub personal access token. The token is used to query the GitHub API for repository archive status. Without a token, public repositories can still be checked but rate limits are stricter. |
archive.overrides |
{} |
Manual gem-to-repository mappings (gem_name: "owner/repo") for gems whose metadata does not point to the correct GitHub repository. |
The security analyzer runs by default. Disable it with --only unused,redundant,version or set security.enabled: false in config.
| Field | Default | Description |
|---|---|---|
security.enabled |
true |
Include the security analyzer in default scans. |
security.advisory_db_path |
empty | Optional path to a local ruby-advisory-db checkout. When omitted, gemxray fetches advisories from rubysec/ruby-advisory-db on GitHub. |
security.github_token_env |
GITHUB_TOKEN |
Name of the environment variable containing a GitHub token for ruby-advisory-db API requests. Public unauthenticated requests still work but have stricter rate limits. |
security.cache_ttl |
86400 |
Seconds to cache fetched advisory data under ~/.gemxray/cache/security. Set to 0 to disable the disk cache. |
The deprecated analyzer runs by default. Disable it with --only unused,redundant,version or set deprecated.enabled: false in config.
| Field | Default | Description |
|---|---|---|
deprecated.enabled |
true |
Include the deprecated analyzer in default scans. |
deprecated.check_readme |
true |
Fetch GitHub README files referenced by RubyGems metadata and flag the gem when README contains This gem is deprecated. |
The unmaintained analyzer runs by default. Disable it with --only unused,redundant,version or set unmaintained.enabled: false in config.
| Field | Default | Description |
|---|---|---|
unmaintained.enabled |
true |
Include the unmaintained analyzer in default scans. |
unmaintained.threshold_days |
730 |
Minimum inactivity period before a repository is reported. The analyzer compares this against the newest GitHub repository push time and latest release publish time. |
unmaintained.github_token_env |
GITHUB_TOKEN |
Name of the environment variable containing a GitHub personal access token. Without a token, public repositories can still be checked but rate limits are stricter. |
unmaintained.overrides |
{} |
Manual gem-to-repository mappings (gem_name: "owner/repo") for gems whose metadata does not point to the correct GitHub repository. |
bundle install
bundle exec rspecRun the executable locally:
ruby exe/gemxray scan --format terminal
Bug reports and pull requests are welcome on GitHub.
The gem is available as open source under the terms of the MIT License.