The project is in a healthy, maintained state
Compares the leaf key paths declared by each of your Rails encrypted credential files and fails when they disagree. Paths only, never values, so rotating a secret is not drift and no secret reaches an error message. Hooked where Rails already checks for pending migrations: a development middleware, the test suite boot, and a rake task.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

CredentialParity

Catch Rails credential drift between environments before it reaches production.

CI Gem Version

Rails ships nothing that compares your credential files to each other. Because the whole file is encrypted rather than just the values, a key added to three of four files and misspelled in the fourth is invisible in a diff, and credentials.dig returns nil for it rather than complaining. The first sign of trouble is the environment that missed it failing after a deploy, found by whoever ran the deploy rather than whoever made the change.

Credential keys are out of sync between environments.

  staging is missing vendor.indexing_api.private_key, which production defines
  staging defines vendor.indexing.private_key, which production does not

Environment Roles

Every environment is either a mirror of the reference or a subset of it. The defaults describe the shape almost every Rails app already has:

Environment Role May omit a key May add its own key
production reference n/a n/a
staging mirror No No
development subset Yes No
test subset Yes No

The asymmetry is the point. A flat "all files must be identical" rule is wrong on its face, because development and test legitimately go without credentials for services only the deployed environments talk to. Letting them be subsets means no allowlist of exceptions to maintain, and no allowlist to go stale.

Requirements

Ruby 3.2 or newer, and ActiveSupport 7.0 or newer.

Installation

gem "credential_parity"

Then add the check to your test suite, next to the pending migration check that is already there:

# spec/rails_helper.rb
ActiveRecord::Migration.maintain_test_schema!
require "credential_parity/rspec"

On Minitest, call it directly:

# test/test_helper.rb
CredentialParity.check!

That is all. The development middleware installs itself.

Usage

In development

A middleware raises on page load when the files stop agreeing, the same way a pending migration does. It wraps an ActiveSupport::FileUpdateChecker, so the files are only decrypted again after one of them actually changes and the per request cost is a stat call. Correct the file and reload; no restart needed.

In your test suite

require "credential_parity/rspec" at boot, next to maintain_test_schema!. This is the hook that catches the person who introduced the drift, because adding a credential is always followed by running something.

From the command line

bundle exec rake credential_parity:verify

Prints the environments it compared, or exits non-zero listing every violation. Useful in a deploy step, or when you want an answer without booting a server.

Anywhere else

CredentialParity.check!       # raises DriftError, or returns nil
CredentialParity.violations   # array of Violation, empty when clean
CredentialParity.checkable?   # false when the keys are not all present

Configuration

All configuration is optional. CredentialParity works out of the box on an app with the usual four environments.

# config/initializers/credential_parity.rb
CredentialParity.configure do |config|
  # The environment every other one is compared against (default: :production)
  config.reference = :production

  # Must declare exactly the same key paths as the reference (default: [:staging])
  config.mirrors = [:staging, :demo]

  # May omit keys, may never add their own (default: [:development, :test])
  config.subsets = [:development, :test]

  # Where the .yml.enc and .key files live
  # (default: Rails.root.join("config/credentials"))
  config.directory = Rails.root.join("config/credentials")

  # Environments the page load middleware is installed in (default: [:development])
  config.middleware_environments = [:development]
end

An app with no staging environment sets config.mirrors = [].

How the comparison works

Leaf key paths are compared, never values. Two consequences follow:

  • Rotating a secret in one environment is not drift. Every environment is supposed to hold a different value for the same key.
  • No secret can reach an error message. Only path names are ever read out, which matters because that message gets pasted into terminals and chat.

Recursing to leaf paths, rather than comparing top level keys, is what catches the nested case. If production declares vendor.indexing_api.private_key and every other environment declares vendor.indexing.private_key, all four still declare a top level vendor, so a shallow comparison sees nothing wrong.

A declared key with nothing under it, some_key: or some_key: {}, counts as a leaf. It is still a key one environment declares and another does not.

When it does not run

The check needs every environment's key, so it runs on developer machines, which is the only place all of those keys exist, and quietly does nothing anywhere else. On CI and on deployed hosts only one environment's key is present, so there is nothing to compare and nothing fails.

rails credentials:edit is deliberately never gated. It is the command used to repair drift, so a check that blocked it would lock you out of the fix. The scoping is copied from ActiveRecord::Migration::CheckPending, which Rails runs on web requests and at test boot but not for generators, console, or runner.

Set SKIP_CREDENTIAL_PARITY_CHECK=1 to bypass it entirely.

Error Handling

All errors inherit from CredentialParity::Error, so you can catch everything with one rescue or handle specific cases:

begin
  CredentialParity.check!
rescue CredentialParity::Error => e
  # Catch any credential_parity error
end

Specific error classes:

Error When
CredentialParity::DriftError The environments disagree (message lists every path and where it is missing or extra)
CredentialParity::ConfigurationError No credentials directory could be resolved, or a violation carried an unknown kind

Deployment Notes

CI

Nothing to do. CI holds at most one environment's key, usually through RAILS_MASTER_KEY, so the check reports itself unable to run and your build is unaffected. Adding rake credential_parity:verify to CI is harmless but will skip for the same reason.

Deployed hosts

Same story. A production dyno has production.key and nothing else, so parity cannot be evaluated there. If you want a deploy time gate, the thing to check on a deployed host is that every credential your code reads is present, which is a different question from whether the environments agree.

Contributing

Bug reports and pull requests are welcome on GitHub.

  1. Fork the repo
  2. Create your feature branch (git checkout -b my-feature)
  3. Make your changes with tests
  4. Ensure all tests pass (bundle exec rake test)
  5. Commit and push
  6. Open a pull request

Testing

# Install dependencies
bundle install

# Run the full test suite
bundle exec rake test

# Run tests against a specific ActiveSupport version
bundle exec appraisal activesupport-7.0 rake test
bundle exec appraisal activesupport-8.0 rake test

# Run all appraisals
bundle exec appraisal rake test

Available appraisals: activesupport-7.0, activesupport-7.1, activesupport-7.2, activesupport-8.0.

Tests generate their own encrypted fixtures in a tmpdir, so no real credential file is ever decrypted and no external services are needed.

License

Copyright (c) 2026 Velocity Labs, LLC. Released under the MIT License.