Here is the pattern almost every agent integration reaches for first. Query Salesforce with a service account that can see everything. Get the rows back. Check who is asking. Remove what they shouldn’t have. Show them the rest.
It feels responsible. There is a filter, it runs on every request, and the output is clean.
It protects almost nothing.
The model saw it
By the time you filter, the data has already been through the model’s context window. That means it can be summarized, reasoned about, counted, compared, and inferred from — all before your filter ever runs.
The failure isn’t usually a leaked field appearing verbatim in the output. It’s subtler:
- The agent says a total that only makes sense if it counted records the user can’t see.
- It says “that’s below average for this segment” — an average computed across private deals.
- It declines to answer in a way that confirms something exists.
None of these trip a filter that looks for forbidden values in the response text. The value never appears. The knowledge does.
The list has to be maintained
The second problem is more ordinary. A post-query filter is a list of things to strip, and lists rot.
Someone adds a field. Someone changes a sharing rule. Someone builds a second agent that queries the same object through slightly different code. The filter in one path gets updated; the filter in the other doesn’t. Nothing fails loudly, because a missing strip rule produces a perfectly well-formed answer.
You end up maintaining a shadow copy of your Salesforce security model in application code, and the two drift.
Enforce in the query
The alternative is to never fetch what the caller isn’t entitled to.
When every record query runs in user mode as the calling user, Salesforce applies the checks it already has, on the query itself:
| Check | Effect |
|---|---|
| Object permissions | No read access, no rows |
| Field-level security | A field the user can’t see is not returned |
| Record sharing | Private records are excluded, including inside related lists |
A field the caller can’t see isn’t fetched and hidden. It’s simply absent, and the agent answers from thinner context — which is the correct behavior.
The practical consequence is worth stating plainly: two users calling the same tool on the same record get different payloads, with no per-user configuration anywhere. You didn’t write that logic. It’s the sharing model you already maintain.
What this changes about review
It also changes what a security review has to cover. Instead of auditing every code path that touches CRM data for a correct and current strip list, you audit the thing you were already auditing — profiles, permission sets, and sharing rules.
There is one thing worth watching: curation is not security. Choosing not to include a field in a curated view keeps it out of the payload, but that’s a design decision, not an access control. A field that must never reach a given user belongs behind field-level security, not behind a curation choice.




