console_think_twice

Gem Version CI

Confirm before you destroy. console_think_twice intercepts destructive Active Record calls made from a console and asks first, so a stray User.destroy_all in production costs you a keystroke instead of a database.

>> User.first.destroy!

This will permanently destroy User #1 in production.
Cascades to: sessions, comments
Confirm? (y/N) n
ConsoleThinkTwice::Aborted: Aborted. Nothing was destroyed.

>> User.destroy_all

This will permanently destroy 200 Users in production.
Cascades to: sessions, comments
Confirm? (y/N) y
=> [#<User id: 1, ...>, ...]

>> User.first.destroy!(force: true)
=> #<User id: 2, ...>

Installation

Add it to your Gemfile:

gem "console_think_twice"

Then bundle install. Or:

bundle add console_think_twice

In a Rails app that is all — a railtie installs the guard from the console hook, which runs for rails console only. Web requests, background jobs and rails runner are untouched, and the gem never patches anything until a console actually boots.

Outside Rails, call ConsoleThinkTwice.install! once your models are loaded.

What is guarded

Call Prompt
record.destroy, record.destroy! This will permanently destroy User #1.
record.delete This will permanently delete, skipping callbacks, User #1.
Model.destroy_all, relation.destroy_all, company.users.destroy_all This will permanently destroy 200 Users.
Model.delete_all, relation.delete_all This will permanently delete, skipping callbacks, 200 Users.
company.users.destroy(user, other) This will permanently destroy 2 Users.

Model.destroy(id), destroy_by and delete_by route through those methods, so they are covered too.

Anything other than y/yes raises ConsoleThinkTwice::Aborted, which also stops a loop like users.each(&:destroy!) on the first refusal rather than asking 200 times.

Every guarded method takes force: true to skip the prompt.

One prompt per call

A dependent: :destroy cascade destroys child records, and destroy_all destroys its records one at a time — both would otherwise prompt again for every record. Nested prompts are suppressed for the duration of a confirmed call, so you are asked exactly once.

What is not guarded

update_all, upsert_all and anything run through connection.execute are untouched, as is collection.delete(record) — which unlinks the record rather than destroying it, unless the association says dependent: :destroy.

Active Record also destroys records itself, as one step of a call that is about something else: a record marked _destroy by accepts_nested_attributes_for, or the old record a has_one assignment discards. Those do not prompt. They are already covered by whatever you typed to set them off, and they run inside that call's transaction — stopping to ask there would hold the transaction, and its locks, open until somebody answered. A destroy you type yourself is always asked about, including inside transaction { ... } and in a rails console --sandbox.

Nobody to ask

When the input stream is not a terminal — piped input, a script fed to rails console — there is no one to answer, so the guard refuses the call instead of assuming yes. Pass force: true for destructive calls that are meant to run unattended.

Configuration

ConsoleThinkTwice.configure do |config|
  config.enabled = Rails.env.production?      # default: true, unless CONSOLE_THINK_TWICE is 0/false/no/off
  config.label = "production (europe)"        # shown in the prompt; defaults to Rails.env,
                                              # set nil or false to leave it out
  config.affirmative_answers = %w[y yes si]   # default: %w[y yes]
  config.interactive = true                   # default: auto-detected from config.input.tty?
  config.input = $stdin
  config.output = $stdout
end

Set CONSOLE_THINK_TWICE=0 in the environment to turn the guard off without touching code. ConsoleThinkTwice.disable! turns it off for the rest of the current session and ConsoleThinkTwice.enable! turns it back on; setting config.enabled takes effect straight away too, without reinstalling.

Development

bundle install
bundle exec rake        # specs and linter
bundle exec rspec
bundle exec standardrb

The guard reads Active Record's own call stack to tell your calls from its, so the suite is run against every supported Active Record, not just the newest. Delete Gemfile.lock, then:

ACTIVERECORD_VERSION=7.0 bundle install && ACTIVERECORD_VERSION=7.0 bundle exec rspec

License

MIT. See LICENSE.