Muting permission sets are the duct tape of Salesforce access control. They look like surgery and behave like a tourniquet — useful in an emergency, dangerous as a permanent solution.
If your Permission Set Group has more than two mutings, you do not have a permissions model. You have an apology in YAML.
The shape of the mistake
A team builds a Permission Set Group for “Sales Manager”. They drop in Sales_Core, Sales_Pipeline_Edit, Forecast_View, and Reports_Admin. Reports_Admin happens to include Modify All Data on three objects the manager should not modify.
Instead of splitting Reports_Admin, they add a muting permission set that removes Modify All Data on those three objects. Ship it.
Six months later there are eleven groups, each with two or three mutings, and an external auditor asks: “Show me everyone who has edit on Opportunity__c.”
The answer requires walking every group, every component set, every muting set, computing the effective permission, and praying you got it right.
Why muting is fundamentally wrong as a primary tool
Three reasons:
- Mutings are subtractive over an additive model. Salesforce permissions are unionized. Mutings invert the polarity. Your team has to reason in two directions at once.
- Audit tooling is weak. The Setup UI shows the group’s effective permissions, but the export tools (Metadata API, Salesforce CLI) give you the raw components and you have to compute the diff yourself.
- They hide intent. A muting set with “Removes_ModifyAll_From_Reports_Admin” tells you what but not why. Six months from now nobody remembers and nobody dares delete it.
The right pattern: vertical slices, not horizontal layers
Permission Sets should be vertical — they grant a coherent business capability end to end. They should NOT be horizontal layers like “all read”, “all edit”, “all admin”.
Bad (horizontal):
Account_ReadAccount_EditAccount_Admin
Good (vertical):
Customer_Onboarding_Specialist(edit on Account, read on Contract, run flow X)Renewal_Manager(edit on Opportunity stages 5-7, edit on Quote, run flow Y)Territory_Planner(read on Account, edit on Territory2Model)
A vertical slice is small enough that you never need to mute anything inside it. A group is just the union of slices a job requires.
Refactoring an existing mess
Step 1: inventory. Pull every PSG and its components.
sf data query --query "
SELECT PermissionSetGroup.DeveloperName,
PermissionSet.Name,
PermissionSet.IsCustom,
PermissionSet.Type
FROM PermissionSetGroupComponent
ORDER BY PermissionSetGroup.DeveloperName
" --target-org prod --result-format csv > psg-inventory.csv
Step 2: find your muting sets.
SELECT Id, Name, Description, IsCustom
FROM PermissionSet
WHERE Type = 'Muting'
Each muting represents a vertical slice you failed to draw.
Step 3: for each muting, identify what the muted PSG users actually need from the component set being muted. That subset becomes a new, smaller vertical slice. Replace the muting with the new slice.
Step 4: delete the muting and the now-unused larger set. Run the access changes through a sandbox with at least one identity per affected job role.
When muting IS the right tool
Two cases, both narrow:
- Temporary suppression during phased rollout. You’re enabling a feature for 90% of users but holding back 10% for legal review. A muting set on the holdback group is a clean way to express this. It is meant to disappear in six weeks.
- Emergency revocation. A vendor permission set has dangerous defaults and you need to clamp it before the next change window. Muting buys you time while you build the proper split.
Both cases need a RemoveBy__c custom field convention and a scheduled flow that flags mutings past their removal date.
Apex check for governance
Schedule this once a week. It surfaces mutings older than 90 days.
public class MutingPermissionAudit implements Schedulable {
public void execute(SchedulableContext sc) {
List<PermissionSet> stale = [
SELECT Id, Name, CreatedDate, Description
FROM PermissionSet
WHERE Type = 'Muting'
AND CreatedDate < :Datetime.now().addDays(-90)
];
if (stale.isEmpty()) return;
// Post to ops Chatter group or platform event
List<PermissionAudit__e> events = new List<PermissionAudit__e>();
for (PermissionSet ps : stale) {
events.add(new PermissionAudit__e(
Category__c = 'StaleMuting',
Detail__c = ps.Name + ' created ' + ps.CreatedDate.format()
));
}
EventBus.publish(events);
}
}
Related reading
If your team is also fighting field-level access on the same objects, the field-level security 2026 patterns piece covers the FLS side of this same refactor.
UX note
On the User record page, add a “Computed Permissions” component that resolves the union-minus-mutings to a flat list. The Setup UI requires three clicks to get this. Putting it one click away from the record stops admins from guessing.
Bottom line
- Mutings are emergency tools, not architecture.
- Vertical permission slices eliminate the need for mutings in 90% of cases.
- If a group has more than two mutings, it needs a split, not another muting.
- Schedule a stale-muting audit; treat any muting older than 90 days as tech debt.
- Build a computed-permissions view on the User record so admins see truth, not configuration.