I had the comment half typed. An Apex class with no sharing declaration at the top, and my cursor sitting under it, ready to write: this class has no declaration, so it runs without sharing — please add with sharing.

I did not send it, and the reason was not caution. It was twenty years of teaching — somewhere in that habit is a small alarm that goes off when I am about to say confidently something I have only ever read. So instead of sending the comment, I opened a Developer Edition org and tested it.

It was wrong. And once I had started testing, two more things I was certain of turned out to be wrong as well. This post is about all three, and about the modest method that found them, which you can run yourself in an afternoon.

The claim nearly everyone repeats

The claim goes like this: Apex classes come in three flavours — with sharing, without sharing, and no declaration — and the third one behaves like the second. Leave it off, and record-level security is off.

It is stated in that shape in an enormous amount of writing about Salesforce, including by people who know far more than I do. It is also easy to believe, because it has the right feel: security is opt-in, so no opt-in means no security.

What actually happens is different. A class with no sharing declaration inherits its caller’s sharing context. It does not choose a posture of its own; it takes on whichever posture the code that invoked it was already running under.

Called from a with sharing class, the undeclared class enforces sharing. Called from a without sharing class, it does not. The same file behaves differently depending on who called it — which means you cannot read that class in isolation and know what it does.

An undeclared class is not insecure by default. It is undecided by default — and the decision is made by whoever calls it.

Why I insist on testing a claim like this

I want to be careful here, because “the docs are wrong” is a tiresome genre and that is not my point. My point is smaller: a claim you have only read is not a claim you know.

Sharing behaviour is unusually hard to reason about from prose, because the answer depends on a context living outside the file you are reading. That is exactly the kind of claim that gets summarised, re-summarised, repeated in a blog post and then in a code-review comment, until it has travelled a long way from anyone who ever watched it run. So I stopped reading and built the smallest experiment that would distinguish the possibilities.

The shape of an honest experiment

Everything here I learned from marking practical work, not from software: if a pupil’s experiment cannot fail, it has proved nothing. Each of my checks had three parts.

A positive control. A case you are confident should succeed. If it fails, your test rig is broken and every other result is noise. Mine was a plain query as an administrator: records come back, no surprises.

A negative control. A case you are confident should be blocked. If it succeeds, the rig is not actually enforcing anything, and a green result means nothing. Mine was a restricted user reading a record that sharing genuinely denied them.

A discriminator. The one case whose outcome you cannot predict, arranged so that the competing explanations give different answers. This is the whole experiment; the controls exist only to make its result trustworthy. For the sharing question: an undeclared class called once from a with sharing caller and once from a without sharing caller. If “undeclared means without sharing” were true, both runs would return the same rows. They did not.

Three cases each, run by hand in a real Developer Edition org, against a real restricted user. Not a mock. Not a reasoning exercise. The org gets the final word.

Surprise two: ‘without sharing’ stops meaning what you think

The second thing I got wrong is stranger, and I would not have found it if I had stopped at the first.

Apex’s default execution mode changed at API version 67.0. Below it, plain operations run in system mode, with field-level security and object permissions unenforced; at 67.0 and above they default to user mode. That version is stamped on each file, so it is a property of the class, not the org.

Here is the part that caught me. At v67, that user-mode default overrides even an explicit without sharing for plain operations. The developer wrote the declaration. The declaration is right there in the source. And it no longer produces the effect they were relying on.

I raise this not to alarm you about upgrades, but because of what it does to reading code. A without sharing at the top of a file has for years been a strong signal to a reviewer: this class deliberately reaches past record security, and someone had better have a reason. That signal now depends on a number in a metadata file. Same word, same position, different consequence.

The third thing: two axes, not one

The third assumption was the most embarrassing, because I have taught the correction and still made the mistake in my own head.

Salesforce security answers two independent questions:

  • Sharing decides which records you get — org-wide defaults, role hierarchy, sharing rules.
  • Field-level security and CRUD decide which fields and objects you may touch at all.

These are separate axes, and nothing in the platform makes one imply the other. A class declared with sharing will happily read a field the running user is not permitted to see, because the sharing declaration was never asked about fields. It answered a different question, correctly.

The tell, if you want one, is in the grammar. with sharing sits on the class. WITH USER_MODE sits on the statement. Different scope, different subject, different question.

What I do now

Three habits came out of that afternoon, and none of them are clever:

  1. Declare sharing on every class. Not because undeclared is insecure, but because undeclared is ambiguous, and ambiguity is what a reviewer cannot check. Say what you mean, so the file can be read alone.
  2. Say the mode on the statement. WITH USER_MODE on SOQL and AccessLevel.USER_MODE on the Database methods, so enforcement does not depend on which version is stamped on the file. (Security.stripInaccessible is a different tool, not a substitute: it strips fields from records you have already read.)
  3. Test the claim I am about to repeat — especially to someone junior, in writing, in a review.

Your next step

Take the belief about Salesforce you would defend most confidently in a meeting — the one you have never actually watched happen — and spend an afternoon trying to break it in a scratch org. Write the positive control, write the negative control, then build the one case that separates the explanations.

Three of mine did not survive contact with the org. Finding that out alone, on a Tuesday, cost me an afternoon and a little pride. Finding it out from an auditor would have cost considerably more.

Mustafa Aksu

Salesforce developer & ISV builder focused on Revenue Cloud, Agentforce, and Data Cloud. I write from real, shipped work.