Apex With / Without / Inherited Sharing Decision
Activate when an author is creating a new Apex class, refactoring one, or
reviewing a PR and the question is the narrow one: what sharing keyword
goes on this class right now? This is a code-author decision, distinct
from the org-level sharing-mechanism choice in
standards/decision-trees/sharing-selection.md.
Before Starting
Gather this context before deciding:
- Entry point. Is the class invoked from an LWC
@AuraEnabledmethod, a REST@HttpGet, a batchDatabase.executeBatch, a trigger handler, a Flow@InvocableMethod, or chained from another Apex class? - Record visibility intent. Should the user see only what their sharing rules permit, or does the operation legitimately need to read and write records they cannot normally see (e.g., audit aggregation, approval routing, compliance scrubbing)?
- Caller context. If this class will be called from other Apex,
remember that
with/without sharingis inherited from the calling class — your keyword may be irrelevant if awithout sharingcaller invokes you, unless you explicitly declarewith sharing. - Annotations on the class.
@AuraEnabled,@RemoteAction, and@RestResourceclasses are user-facing entry points and must default towith sharingunless you have a documented elevation reason.
Core Concepts
The three keyword choices
with sharing / without sharing / inherited sharing differ on whose sharing rules apply to direct SOQL queries. Field-Level Security and CRUD are a separate enforcement concern (WITH USER_MODE, Security.stripInaccessible; WITH SECURITY_ENFORCED only on classes at API ≤ 66.0 — it is removed in 67.0 and does not compile there) and are not controlled by these keywords.
| Keyword | Sharing applied to direct SOQL | Default for what entry points | When to use |
|---|---|---|---|
with sharing |
Running user's sharing rules enforced; invisible records filtered out | @AuraEnabled, @RemoteAction, @RestResource (must default here) |
Any user-facing entry point unless you have a documented elevation reason |
without sharing |
Running user's sharing rules ignored for this class's own queries | None — never a safe default | Audit aggregation, approval routing, compliance scrubbing — deliberate elevation only |
inherited sharing |
Adopts the caller's mode; defaults to with sharing when called directly from Lightning/REST/Aura |
Reusable utilities, selectors, base service classes | Shared service/selector code that should respect whatever the entry point demanded |
without sharing is a record-visibility keyword, not a blanket system context. It does not turn off FLS or CRUD — and at API 67.0+ database operations enforce the running user's FLS and object permissions by default with no keyword at all, so an elevation that genuinely needs them off states it per statement (WITH SYSTEM_MODE, AccessLevel.SYSTEM_MODE).
Sharing is inherited through method calls
A class that explicitly declares with sharing keeps its own mode no
matter who calls it. What a bare callee does depends on its own
apiVersion (see "Bare class behavior" below): at API ≤ 66.0 the
outermost class on the call stack governs it, so a with sharing
controller makes the bare service run with sharing while a without sharing controller makes the same service run without sharing. At
API 67.0+ the bare callee no longer follows the caller down — it runs
with sharing on its own. Either way the callee's behavior is decided
somewhere other than its source, so always declare an explicit keyword on
shared service / selector layers.
Where inherited sharing fits
Reusable utilities, selectors, and base service classes that should
respect whatever the entry point demanded. inherited sharing is the
explicit way to say "I do not want to override the caller's choice, but I
also do not want to be ambiguous about it." It also produces a safe
default (with sharing) when called from Lightning / REST entry points —
unlike a bare class with no keyword.
Bare class (no keyword) behavior — version-gated
The bare-class default inverted in Summer '26. The gate is the
apiVersion in the class's own .cls-meta.xml, not the org's
release — a Summer '26 org runs a class pinned to 58.0 with the old
behavior.
- API 67.0+ — a class with no keyword runs
with sharing, regardless of who called it. The rule reaches across a call chain: once any class in the chain is saved at 67.0+, the chain from there runswith sharing— a bare 67.0 callee is not pulled down towithout sharingby awithout sharingcaller. - API ≤ 66.0 — a bare class is not equivalent to
with sharing; it ran effectively aswithout sharing, except when called from a Lightning context (@AuraEnabled,@RemoteAction), which has run itwith sharingsince API v34.
Never ship a bare class at either version — pick one of the three
keywords explicitly so reviewers can audit intent, and so an apiVersion
bump cannot silently flip a class that needs elevation into enforcing
sharing. Canonical version table:
agents/_shared/AGENT_CONTRACT.md
§ Apex security idiom by API version.
Interaction with WITH USER_MODE
WITH USER_MODE on a SOQL query (and Database.queryWithBinds(..., AccessLevel.USER_MODE), plus DML overloads with AccessLevel.USER_MODE)
enforces both sharing rules and FLS/CRUD for that single statement,
regardless of the class-level keyword. This means a without sharing
class can run a single query in user mode without changing the rest of
its behavior — useful when 95% of a system-context job needs one
user-scoped lookup. Conversely, WITH SYSTEM_MODE on a query inside a
with sharing class elevates only that statement. Class keyword sets the
default; WITH USER_MODE / WITH SYSTEM_MODE is the per-statement
override.
Always justify without sharing
Repo convention: any without sharing class must be preceded by a
// reason: comment explaining what user-invisible data is being
accessed and why the elevation is required. The checker enforces this.
Common Patterns
Pattern: AuraEnabled controller
When to use: any @AuraEnabled Apex class invoked from LWC / Aura.
How it works: declare with sharing. Let the user's record
visibility govern. If a single operation legitimately needs elevation
(e.g., loading a configuration record outside the user's perimeter),
factor it into a separate without sharing helper with a // reason:
comment, or use WITH SYSTEM_MODE on that one query.
Why not the alternative: without sharing on an @AuraEnabled class
is a frequent insecure-direct-object-reference vector — the LWC may pass
arbitrary IDs and the controller will return them all.
Pattern: Domain / service / selector layer
When to use: classes called from other Apex, never directly by a user-facing entry point.
How it works: declare inherited sharing so the entry point's
choice flows through. Document the assumption at the top of the class:
"caller-governed; ensure entry point declares with sharing for
user-scoped operations."
Why not the alternative: a bare class is ambiguous; an explicit
with sharing overrides legitimate batch / system contexts that need
elevation.
Pattern: Batchable / Schedulable / Queueable system jobs
When to use: asynchronous jobs that operate across all records regardless of user perimeter (data scrubs, aggregation, retention sweeps).
How it works: without sharing with a // reason: comment
explaining why system context is required. Add a unit test that asserts
the job processes records the running user cannot see.
Why not the alternative: with sharing on a batch run by an
integration user can silently miss records and cause incomplete jobs.
Decision Guidance
| Scenario | Keyword | Reason |
|---|---|---|
@AuraEnabled controller for LWC |
with sharing |
User invoked it; respect their visibility |
@RestResource exposed to a community / partner |
with sharing |
External caller authenticates as a user |
| Reusable selector / service / domain class | inherited sharing |
Caller chooses; you remain neutral |
| Batch / Schedulable system job | without sharing + // reason: |
Cross-perimeter aggregation |
| Trigger handler | without sharing (typical) |
The trigger body runs in system mode at every API version — 67.0 does not change that; the handler's keyword governs only the handler's own queries |
| Site / guest user controller | with sharing (mandatory for guest) |
Guest perimeter must not be elevated |
| Managed-package internal class | without sharing (Salesforce-enforced) |
Subscriber's keyword cannot override package |
One-off elevated query inside a with sharing class |
keep class with sharing, use WITH SYSTEM_MODE per-query |
Minimum-blast-radius elevation |
Recommended Workflow
When this skill activates, the agent runs these steps in order:
- Identify entry-point category for every class in scope (controller, REST, batch, trigger handler, service, selector, utility, guest-user controller).
- Match category to the Decision Guidance table above and propose the default keyword for each class.
- Trace inherited-sharing risk — for any class without an explicit keyword, list every caller and confirm none of them are
without sharingin a way that would silently elevate this code. - Justify every
without sharingwith a single-line// reason:comment immediately above the class declaration; reject the change if the reason is generic ("performance", "convenience"). - Plan per-query overrides — if 90%+ of the class is one mode but one query needs the other, do not flip the class keyword; use
WITH USER_MODE/WITH SYSTEM_MODEon that statement. - Run
python3 skills/apex/apex-with-without-sharing-decision/scripts/check_apex_with_without_sharing_decision.py --manifest-dir <path>to flag missing keywords on@AuraEnabledclasses and unjustifiedwithout sharing. - Add a Review Checklist citation to the PR confirming the keyword choice was deliberate and the
// reason:comment exists where required.
Review Checklist
- Every class in scope has an explicit sharing keyword (no bare classes) — at API 67.0+ the bare default is
with sharing, which is safe but hides intent and flips on anapiVersionchange - All
@AuraEnabledand@RestResourceclasses arewith sharing - Every
without sharingclass has a// reason:comment above it - Reusable service / selector classes use
inherited sharing - No
with sharingclass silently calls awithout sharinghelper that exposes user-invisible data back to the UI - Per-query
WITH USER_MODE/WITH SYSTEM_MODEused where the class default is wrong for one statement - Trigger handlers explicitly declare
without sharing(orwith sharingif user-mode behavior is intended) — never bare - Test class exists that proves the chosen keyword by running as a low-privilege user
Salesforce-Specific Gotchas
- Sharing is inherited through method calls. A
with sharingcontroller that calls awithout sharinghelper runs that helper's own queries with the running user's sharing rules ignored — the explicit keyword wins over the caller at every API version. Reviewers must trace the call tree, not just the entry point. - Managed-package classes always run
without sharingregardless of subscriber. When a subscriber org calls a managed-package@AuraEnabledclass, the package's declared keyword is enforced; the subscriber cannot tighten it. - Triggers run in system mode at every API version. The 67.0
default-mode change does not reach the trigger body: it bypasses
sharing, FLS, and object permissions, and a
.triggerfile cannot carry a class-level sharing keyword. Per-statement enforcement still works inside a trigger (WITH USER_MODEon its SOQL,as user/AccessLevel.USER_MODEon its DML) — but the default is system mode, so delegate to a handler class and declare an explicit keyword there. - Aggregate queries (
SUM,COUNT,AVG) respect class sharing. Awith sharingclass runningSELECT COUNT() FROM Opportunityonly counts opportunities the user can see — surprising for dashboards. WITH USER_MODEenforces FLS/CRUD too, not just sharing. Adding it to a query inside awithout sharingclass can suddenly start throwingQueryExceptionif the user lacks field-level read on a selected field.
Output Artifacts
| Artifact | Description |
|---|---|
| Keyword recommendation per class | Explicit with / without / inherited sharing choice with rationale |
// reason: comment text |
One-line justification for every without sharing class |
| Per-query override list | Statements that need WITH USER_MODE / WITH SYSTEM_MODE |
| Test class scaffold | Runs the code as a minimum-permission user to prove keyword behavior |
Related Skills
apex/apex-fls-crud-enforcement— sharing is record-level; FLS/CRUD is field/object-level. Both must be considered.apex/apex-aura-enabled-security— controller-specific security review including sharing keywordstandards/decision-trees/sharing-selection.md— org-level sharing mechanisms (OWD, role hierarchy, sharing rules) — read this when the question is broader than "what keyword goes on this class"