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 an llms.txt index of this site, a one-file llms-full.txt corpus, and a Markdown twin for every HTML page (same path, .md suffix, advertised as rel="alternate"). The deep guides under docs/guides/ are published on this site from that same tree — do not treat raw GitHub as a second canon.
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: