New Ory Agent Security is now live! Claim your complimentary test drive. Get Started!

Skip to main content

Strict mode for Ory Permissions

What is strict mode?​

Strict mode makes the Ory Permissions engine treat your OPL as the single source of truth during every check. Without strict mode, the engine doesn't use your OPL declarations to filter which tuples it follows — it may follow subject-set pointers that your OPL doesn't specify.

Strict mode is enabled by default for all new Ory Network projects created from July 2026 onwards — if you created your project after that date, strict mode is already on and you don't need to configure anything. For older projects, strict mode is disabled by default and can be enabled under Permissions → Namespace & rules in the Ory Console.

Why enable strict mode?​

Strict mode improves both performance and correctness:

  • Fewer queries. Ory Keto skips evaluation steps that are impossible given your schema — following undeclared subject-set pointer types, and direct tuple checks on permits rules.
  • No stale grants. Tuples that reference relations removed from your OPL no longer grant access.
  • Explicit errors when a check can't answer. Ory Permissions enforces depth and width limits to prevent unbounded graph traversal, and some permissions have no answer at all. In non-strict mode, either case silently returns { "allowed": false } — identical to a legitimate denial. In strict mode, the engine returns an explicit error so you can tell the two apart.
ScenarioNon-strictStrict
A single check can't answer{ "allowed": false }An error naming the reason
A batch check entry can't answer{ "allowed": false }{ "allowed": false, "error": "<reason>" }

Over REST, that error is 422 Unprocessable Entity and the reason is in the response body. Over gRPC, it is FAILED_PRECONDITION carrying the same reason. The reason names the cause: max_depth_exceeded or max_width_exceeded when the search hit a limit, and negation_cycle when the permission negates its own recursion.

Ory Network enforces fixed depth and width limits that cannot be changed in the console. If you hit a limit, contact Ory support to discuss your use case.

Permissions that negate their own recursion​

A permission defined as the negation of itself has no answer, in any mode. For example, a document is visible when its parent is not visible, over a folder that is its own parent:

class Doc implements Namespace {
related: {
parent: Doc[]
}
permits = {
visible: (ctx: Context) => !this.related.parent.traverse((p) => p.permits.visible(ctx)),
}
}

Whichever answer the engine assumed, the definition produces the opposite, so no answer satisfies it. A check like this reports negation_cycle, and never returns allowed: true. Only the part of the check that loops is affected: if the permission can also be granted another way, the check answers from that instead.

A loop in your relationships is not itself a problem. A group that contains itself, or a folder that is its own ancestor, resolves normally — only a ! inside the loop makes the permission unanswerable. To fix it, change your permission model so the negated permission no longer depends on itself.

Patterns that break in strict mode​

These patterns work in non-strict mode but break after enabling strict mode.

Tuples written with a subject ID instead of a subject set​

If your application uses the subject_id API field to write tuples or perform checks — for example writing File:readme#viewers@user_5 with no namespace — strict mode returns an explicit error at check time. Subject IDs have no connection to your OPL, so the engine cannot validate them. In non-strict mode this produces a silent allowed: false, indistinguishable from a legitimate denial. In strict mode you get an error immediately, because strict mode requires all tuples to be consistent with your OPL.

This requires a migration: see Migrating from subject IDs to subject sets.

Subject-set tuples for undeclared types​

This covers any tuple that points to a subject-set type your OPL doesn't declare for that relation.

Example: viewers is declared as User[], but a tuple pointing to a Group subject-set was written:

class File implements Namespace {
related: {
viewers: User[] // only Users allowed
}
}

Writing a tuple like this — which assigns a Group subject-set to the viewers relation — will be ignored in strict mode:

keto relation-tuple create Group:engineering#members viewers File:readme

Declare the type in OPL to keep it working:

viewers: (User | SubjectSet<Group, "members">)[]

The same applies in reverse: if viewers is declared as SubjectSet<Group, "members">[] but a direct user tuple was written:

File:readme#viewers@User:alice

Strict mode ignores it because User is not a declared type for that relation.

Traversal through undeclared object types​

In strict mode, traverse follows only tuples pointing directly to an object of a type declared on the traversed relation. The subject must not include a relation.

For example:

class User implements Namespace {}

class Group implements Namespace {
related: {
members: User[]
}
}

class File implements Namespace {
related: {
groups: Group[]
}
permits = {
view: (ctx: Context) => this.related.groups.traverse((g) => g.related.members.includes(ctx.subject)),
}
}
TupleFollowed by groups.traverse(...)
File:readme#groups@Group:engineeringYes
File:readme#groups@Other:engineeringNo
File:readme#groups@Group:engineering#membersNo

Strict mode ignores nonmatching tuples during permission checks; this restriction is not enforced when writing tuples.

Tuples written directly against permit relations​

Example: canView is a computed permit, but a tuple was written against it directly:

class File implements Namespace {
related: {
editors: User[]
viewers: User[]
}
permits = {
canView: (ctx: Context) => this.related.editors.includes(ctx.subject) || this.related.viewers.includes(ctx.subject),
}
}
File:readme#canView@User:alice

Strict mode skips direct tuple checks on permits rules. Write tuples against editors or viewers instead.

Stale tuples from a renamed or removed relation​

If you renamed or removed a relation in OPL but didn't clean up the old tuples, in rare setups, Ory Keto in non-strict mode still follows them. Strict mode ignores them immediately.

How to check if you're ready​

Audit two things before enabling:

  1. Tuple writes — every relation you write tuples against should exist in your OPL, and the subject type should match what the relation declares. For example, if your application writes:

    Document:readme#editors@User:alice

    check that the Document namespace in your OPL declares an editors relation, and that it accepts User as a subject type:

    class Document implements Namespace {
    related: {
    editors: User[]
    }
    }
  2. Check requests — every relation you check should be defined in your OPL. For example, if your application calls:

    is User:alice allowed to editors on Document:readme

    verify that editors is declared in the Document namespace.

If both are consistent with your OPL, enabling strict mode produces identical results to non-strict mode — with faster permission checks.

See the Ory Permission Language guide.

Enabling and disabling​

Go to Permissions → Namespace & rules in the Ory Console. Toggle Strict mode on or off and save. The change takes effect immediately — no restart required, and no data is modified.