For AI agents (and the people driving them)
Increasingly the “reader” of authorization docs is an AI coding agent. These prompts encode the foot-guns this documentation already knows about, so an agent gets the integration right on the first try. Copy, adjust the names, paste.
There is also an llms.txt index of this site, and every page here links to its canonical in-repo source — point your agent at those for depth.
Install CurrentScope in this app
Add the current_scope gem to this Rails app (Rails >= 8.1, < 9 required).
1. Add `gem "current_scope"` to the Gemfile, then run:
bin/rails generate current_scope:install
bin/rails current_scope:install:migrations && bin/rails db:migrate
2. In ApplicationController, include CurrentScope::Context and
CurrentScope::Guard (in that order). Authentication must be wired
BEFORE these concerns run — Context reads current_user when its
callback fires, so an auth callback registered after these includes
means the gate denies before authentication happens.
3. The gate is fail-closed and covers EVERY action. You MUST add
`skip_before_action :current_scope_check!` to controllers where
authorization does not apply — sign-in/sessions (or nobody can log in),
webhooks, health checks. A skipped controller is unprotected by the
permission gate: keep or add the app's own auth there. If the app has
act-as/impersonation, ALSO add
`skip_before_action :current_scope_mutation_guard!` on sign-in,
sign-out, and the stop-impersonation action — that guard is a separate
callback that survives the gate skip, and without its skip an
impersonating admin cannot sign out or stop impersonating.
4. If this app already has users and traffic, set
`config.enforcement = :report` in the initializer BEFORE deploying,
run the test suite, then run `bin/rails current_scope:report` and seed
the roles it names. Flip back to :enforce only when re-exercising the
app adds no NEW access.would_deny rows (the report reads the
append-only ledger — historical rows never clear) and any
access.sod_blind_spot rows are resolved. Report mode must not be the
final state.
If config.sod_actions is set, ALSO clear any access.sod_initiator_missing
rows before flipping. Those are not denials — they are 500s. An SoD action
reaching a model with no current_scope_initiator RAISES
CurrentScope::ConfigurationError, in :report mode exactly as in :enforce,
so granting will not clear them. Best done before enabling SoD at all:
the engine logs a preflight warning naming the ones it can see (at boot in
production; on the first request in development, whose routes are lazy),
`bin/rails current_scope:report` lists them, and
`CurrentScope::SodPreflight.scan` returns them programmatically: a Result
with .rows ([permission, model_class] pairs), .inspected, .in_scope and
.skipped. An EMPTY .rows is NOT an all-clear on its own — check
`result.blind?`, which is true when a check failed (.degraded?) or when
nothing was read at all (.inspected == 0), because the scan only sees
controllers declaring current_scope_model.
5. Bootstrap the first admin (the management UI only admits full-access
subjects): `bin/rails current_scope:grant SUBJECT_ID=<id>` — or, in
seeds, `CurrentScope.grant!(user)` (it creates the default
Owner/Member roles if missing; Member starts with zero permissions).
6. Declare record hooks as PRIVATE controller methods. For member actions:
`def current_scope_record = (set_thing if request.path_parameters[:id])`
— key off request.path_parameters, never params, so a ?id= query string
cannot smuggle a record into collection actions. Use the route's actual
member key: a route declared with a custom param (e.g. :slug) must
check request.path_parameters[:slug], or the hook returns nil and the
SoD veto is silently skipped on that action.
7. Run the full test suite. Expect controller tests to 403 until grants are
seeded — use the test helpers (grant_role!/grant_scoped_role!) from
current_scope/test_helpers, not stubs.
## Carry role definitions between environments
```text
Export and diff are safe for an agent to run. Import and rollback are not
autonomous in production.
1. Export live roles:
bin/rails current_scope:definitions:export FILE=config/current_scope/roles.yml
2. Commit the YAML. Diff against another environment:
bin/rails current_scope:definitions:diff FILE=config/current_scope/roles.yml
3. A human reviews the printed diff. The document is desired state for role
names, descriptions, full_access, and permission keys. It does not carry
assignments.
4. A human applies, never an agent on its own:
bin/rails current_scope:definitions:import FILE=config/current_scope/roles.yml CONFIRM=1 ACTOR_ID=<id>
Production always needs CONFIRM=1. A populated non-production database
does too. Do not set CONFIRM=1 unless a human asked in that turn.
5. Rollback reads the snapshot written beside the document, not FILE=:
bin/rails current_scope:definitions:rollback SNAPSHOT=config/current_scope/roles.yml.pre.yml CONFIRM=1 ACTOR_ID=<id>
Before finishing, read docs/SECURITY-CHECKLIST.md in the gem repo (https://github.com/davidteren/current_scope/blob/main/docs/SECURITY-CHECKLIST.md) and verify each item that applies.
## Declare subject identity and attach Owner
```text
Declare how a subject is identified so grants can be found across
environments. This is config.subject_identity, NOT config.subject_label.
Label is display-only and fail-soft. Identity is load-bearing: duplicate
keys raise, and resolve never inserts a subject.
1. Pick the identity:
- one column: config.subject_identity = :email
- composite: config.subject_identity = [:name, :email]
- split across tables: bin/rails generate current_scope:identity
then set config.subject_identity = CurrentScopeSubjectIdentity.new
and fill identify / resolve.
2. Check uniqueness against live rows:
bin/rails current_scope:identity:check
The task exits non-zero and names the colliding keys; it does not raise.
A collision raises ConfigurationError at BOOT, and identity:setup stops
before it writes. Do not first-row-win.
3. Dry-run a grant (writes nothing):
bin/rails current_scope:identity:setup SUBJECT=you@example.com
SUBJECT is whatever your identify returns. For a composite it is a YAML
sequence: SUBJECT='[Ada, you@example.com]'.
ROLE= defaults to Owner (full_access). Admin, if you create it, is not
full_access unless you set that yourself.
IDENTITY=email is an OVERRIDE for one run, for the case where
config.subject_identity is not written yet. Do not add it once step 1
is in the initializer: it replaces the configured identity, so under a
composite or an object identity it audits and grants by the wrong key,
and it removes the create_placeholder! factory step 5 needs.
4. Write only after the dry-run looks right:
bin/rails current_scope:identity:setup SUBJECT=you@example.com WRITE=1
That calls CurrentScope.grant! (ledger source: bootstrap). WRITE=1 also
creates the Role row if it does not exist, and seeds the default Owner
and Member roles when ROLE is unset. The dry-run plan names both.
5. Missing subject: never invent one in production. Outside production,
PLACEHOLDER=1 needs a create_placeholder! factory on the identity
object from `bin/rails generate current_scope:identity`. With one, a
dry-run prints the would-create line; without one the task stops and
says PLACEHOLDER=1 has no factory. Create the marked row with
PLACEHOLDER=1 WRITE=1. Production + PLACEHOLDER=1 is refused.
Hard stop: never invent a production subject.
Enable separation of duties on an approve flow
Enable CurrentScope separation of duties so an initiator can never approve
their own record. SoD is OFF by default — config.sod_actions = [] means the
veto never runs.
1. In config/initializers/current_scope.rb set
`config.sod_actions = %w[approve]` (action NAMES, not full keys).
2. On the model, define `def current_scope_initiator = <initiator assoc>`.
If an SoD action reaches a record whose class lacks this hook, the
resolver raises ConfigurationError — that is intended (fail loud).
3. CRITICAL: the controller's `current_scope_record` hook MUST return the
record on the SoD member action. If it returns nil there, the veto is
silently SKIPPED and the initiator can approve. This is the load-bearing
control — verify it, don't assume it.
4. Write the verification test and run it:
- grant the initiator a role that ticks approve
- POST the approve action as the initiator
- assert response :forbidden AND
response.headers["X-Current-Scope-Reason"] == "sod_veto"
If the reason is "no_grant" instead, SoD is not examining this action.
5. Do NOT add bypass logic unless explicitly asked. If a conditional
self-approval is a real requirement, use the engine's break-glass
(allow_sod_bypass) — never a hand-rolled branch, because break-glass
records the sod.bypassed audit event a hand-rolled branch forgets.
That recording requires config.audit enabled (the default); if an
unaudited bypass must be impossible, set config.audit = :strict.
Debug a CurrentScope 403
A request is being denied. Diagnose it from the engine's own signals; do not
guess or bypass the gate.
1. Read the X-Current-Scope-Reason response header (or the INFO log line
"[CurrentScope] denied <key> (<reason>) → 403"). Map the reason:
- no_grant → nothing grants this controller#action key; check
the role grid / seed the grant
- sod_veto → the subject initiated this record; that denial is
the anti-fraud control working — do not "fix" it
- model_undeclared → the controller needs `def current_scope_model = <Model>`
- model_invalid → current_scope_model returned something that is not
a concrete ActiveRecord class
- impersonation_gate→ non-GET/HEAD while impersonating; sessions are
read-only by design
- not_full_access → the management UI; only full-access subjects enter
2. In development/test, check the log for the engine's nudges (nil SoD
record, inert scoped grant, cross-controller key derivation) — each
names its one-line fix.
3. If there is no header and no log line, a host rescue_from may have
replaced the denial handler, or the controller never included Guard —
run `bin/rails current_scope:ungated` to check the gated surface.
Migration tooling (shipped)
Migrating from Pundit, CanCanCan, or Action Policy? The current-scope-migrate Claude Code skill is complete (#45): deterministic rule inventories for all three systems, decision report, parity harness (per-system old-answer replay), reviewable role-backfill migrations (enum column or rolify), and safe mechanical call-site rewrites behind an explicit --write. The manual path remains the adoption guide.
Planned agent surfaces
These are tracked but not shipped — do not prompt an agent to use them yet: