Refuse sandboxed consoles, which roll back their own audit trail - #6
Merged
Conversation
grantcox
force-pushed
the
stagility/disable-sandbox
branch
from
August 27, 2026 07:41
ed98233 to
bae64ee
Compare
grantcox
marked this pull request as ready for review
August 27, 2026 07:42
grantcox
force-pushed
the
stagility/session-end-at-exit
branch
from
August 27, 2026 07:44
81fb292 to
71a3226
Compare
`rails console --sandbox` wraps the session in a transaction that is rolled back on exit. Records are enqueued through ActiveJob, so with a DB-backed queue on the primary database — Solid Queue, GoodJob, Delayed Job — the enqueue is inside that transaction and the rollback discards the audit trail along with the operator's changes. Reproduced against a Solid Queue app: the Heroku api:dyno webhook arrived and no console audit records did. A fully interactive console with no record of a single statement is precisely what this gem exists to prevent. Rails already has the gate, so use it rather than building a second delivery path: `config.disable_sandbox = true` is checked in Rails::Console#initialize before the console boots. Setting it from the railtie means installing the gem is what protects an app, instead of every app having to remember. Giving the queue its own database would also close this, but it would make auditing depend silently on queue topology — a later consolidation would reopen the hole with no signal. Specs fork per case, since a Rails application can only be initialized once per process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
grantcox
force-pushed
the
stagility/disable-sandbox
branch
from
August 27, 2026 07:45
bae64ee to
9f84581
Compare
grantcox
marked this pull request as draft
August 27, 2026 07:50
grantcox
marked this pull request as ready for review
August 27, 2026 22:51
becky-ynab
approved these changes
Sep 1, 2026
becky-ynab
left a comment
There was a problem hiding this comment.
I didn't even know about the --sandbox option
Collaborator
Author
I didn't either! I was just looking over the upstream FWIW they already have an issue reported for the configurable reason (basecamp/console1984#132), and for "also cover |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A sandboxed console (
rails console --sandbox) wraps the whole session in a transaction that is rolled back on exit. Audit records are enqueued through ActiveJob, so with a database-backed queue on the primary database — Solid Queue, GoodJob, Delayed Job — the enqueue is inside that transaction, and the rollback discards the audit trail along with the operator's changes.The result is a fully interactive console with no record of a single statement. That is precisely what this gem exists to prevent.
This is console1984#91 reached by a different route. Upstream loses DB-stored audit rows; we store none, but the queue row is the casualty instead — so any host app with a DB-backed ActiveJob adapter on the primary database has this, regardless of where records eventually ship.
Reproduced
Against a Solid Queue app on Heroku: the
api:dynowebhook arrived and no console audit records did — nosession_start, nocommand.production.rbsets:solid_queueanddatabase.ymldefines no separate queue database, so thesolid_queue_jobsrow is written on the primary connection inside the sandbox transaction and discarded on exit.The change
Rails already has the gate, so this uses it rather than building a second delivery path:
Rails::Console#initializechecks it andexit 1s before the console boots. Setting it from the railtie (gated on the samecfg.enabled?as everything else in that block) means installing the gem is what protects an app, rather than every app having to remember a config line.Ordering is sound:
Rails::Command::ConsoleCommand#performcallsboot_application!beforeRails::Console.start, so the railtie'safter_initializeruns first.Alternatives considered
DeliveryJob's class comment rejects — in-dyno HTTP with a retry budget measured in seconds, blocking each statement — and adds a second delivery path that only runs in the rare case.No opt-out is exposed. An unaudited console is the one outcome the gem cannot tolerate.
Tests
Specs boot a real (tiny) Rails application, since the ordering is the point — only a booted app proves the setting lands before
Rails::Consolereads it. Each case forks, because a Rails application can only be initialized once per process. Verified non-vacuous: the enabled case fails when the railtie line is removed.65 examples, 0 failures;
standardrbclean.Companion change
heroku-buildpack-console-guarddenies--sandbox/-sat the wrapper as a backstop (scoped toconsole/c, since-sis rake's silent flag).The two layers cover the same dynos, not different ones: this change activates on
CONSOLE_AUDIT_ENABLED, which that buildpack exports only for one-off, scheduler and release dynos. Its value is independence, not reach — it holds even when the command never reaches the wrapper, and it applies on any host that sets the variable. A console opened withheroku ps:execon a long-running dyno is covered by neither, and remains a documented gap governed outside both projects.🤖 Generated with Claude Code