User Access Diff
Compares user access permissions in Salesforce to identify discrepancies.
User Access Diff Agent
What This Agent Does
Given two Users in the same org, produces a symmetric, dimension-by-dimension comparison of their effective access surface: profile, active Permission Set and Permission Set Group assignments (with PSG components flattened), object CRUD, field-level security (opt-in), system permissions (ModifyAllData, ViewAllUsers, AuthorApex, etc.), Apex class / VF page / Flow / Custom Permission / Named Credential grants, public group and queue membership, role hierarchy placement, and territory assignment. Output is a side-by-side report with three buckets per dimension — identical, only in User A, only in User B — plus risk flags when the delta crosses a known sensitivity threshold (e.g., one user has ModifyAllData and the other does not).
Scope: Two users per invocation, one org per invocation. Read-only — the agent does not grant, revoke, or recommend permission changes. For recommended remediation, follow up with permission-set-architect.
Invocation
- Direct read — "Follow
agents/user-access-diff/AGENT.mdfor users [email protected] and [email protected] in the prod org" - Slash command —
/diff-users - MCP —
get_agent("user-access-diff")
Mandatory Reads Before Starting
agents/_shared/AGENT_CONTRACT.mdAGENT_RULES.mdagents/_shared/probes/user-access-comparison.md— the probe this agent usesagents/_shared/probes/permission-set-assignment-shape.md— per-user shape recipeskills/admin/user-management— viaget_skillskills/admin/permission-set-architectureskills/admin/permission-sets-vs-profilesskills/security/permission-set-groups-and-mutingskills/admin/custom-permissionsskills/admin/user-access-policiesagents/_shared/DELIVERABLE_CONTRACT.md— Wave 10 output contract (persistence + scope guardrails)
Inputs
| Input | Required | Example |
|---|---|---|
user_a | yes | [email protected] OR 005XX0000012abc |
user_b | yes | [email protected] OR 005XX0000034def |
target_org_alias | yes | prod |
dimensions | no (default all) | ["profile", "permission-sets", "object-crud", "system-perms", "groups"] |
include_field_permissions | no (default false) | true to include FLS-level diff (can be large) |
purpose | no | "new-hire-parity" | "access-review" | "incident-investigation" — shapes the Process Observations framing |
If user_a or user_b is missing, ambiguous, or resolves to zero rows in the target org, STOP and ask.
If user_a == user_b, refuse — this agent compares two users.
Plan
Step 1 — Resolve both users
- If the input is an email or username, resolve to
User.Idviatooling_query("SELECT Id, Username FROM User WHERE Username = :input LIMIT 1"). - If the input is a 15- or 18-char Id, accept directly.
- Refuse if either user is
IsActive = falseAND the purpose isnew-hire-parity(comparing against an inactive template is almost always a mistake).
Step 2 — Run the probe
Follow agents/_shared/probes/user-access-comparison.md for both users. Honor the dimensions filter — don't pull field-permission rows unless include_field_permissions = true, since they can exceed 10k rows on broad PSGs.
Populate the effective-PS set per user = direct PS assignments ∪ flattened PSG components. This is what downstream diffs operate on.
Step 3 — Compute the symmetric diff
For every dimension in scope, compute three buckets:
| Bucket | Definition |
|---|---|
identical | Present in both users with same grant flags |
only_a | Present in User A's effective set, absent in User B |
only_b | Present in User B's effective set, absent in User A |
The diff key differs per dimension — see the probe's "Post-processing" section. For Object CRUD, the key is (SObjectType, specific-flag) not just SObjectType; for FLS, (SObjectType, Field, permission-type); for Setup Entity Access, (SetupEntityType, SetupEntityId).
Step 4 — Apply risk rubric
Per permission-set-architecture + user-management skills:
| Severity | Condition |
|---|---|
| P0 | Asymmetric ModifyAllData, ViewAllUsers, or ManageSharing |
| P0 | One user has access to more than 3 Apex class: *Controller entries the other lacks AND purpose = incident-investigation |
| P1 | Asymmetric AuthorApex, ManageUsers, CustomizeApplication, ViewSetup, or ViewAllData |
| P1 | Delta in Apex class grants > 10 classes (persona divergence, worth review) |
| P1 | One user has a Queue membership the other lacks AND both hold the same Profile (routing divergence) |
| P2 | Object CRUD delta > 20 objects (likely legitimate role split, flag for confirmation) |
| P2 | PSG assignments differ but the flattened component set is identical (silent drift via PSG re-composition — surface it) |
| P2 | Role-hierarchy delta (different branches) when Profile is identical |
Record every flag with severity, dimension, and one-line rationale.
Step 5 — Frame the Process Observations
Use the purpose input to frame Concerning vs Healthy:
new-hire-parity— Concerning = ANYonly_b(new hire has access the template doesn't); Healthy =only_aset consists only of documented exceptions.access-review— Concerning = P0/P1 asymmetries without documented rationale; Healthy = deltas all fall along known role/team lines.incident-investigation— Concerning = any system-perm or Apex-class delta that could explain the incident; Healthy = access is equivalent, meaning the incident had a different root cause.- Unspecified — produce the findings without a purpose-specific frame.
Step 6 — Emit the output
Side-by-side tables per dimension. For very large deltas (> 50 rows in one bucket), include the first 20 in the markdown output and note the total count.
Output Contract
Conforms to agents/_shared/schemas/output-envelope.schema.json. At minimum:
- Summary — user A label, user B label, org alias, dimensions compared, identical/only_a/only_b counts, highest severity flag.
- Confidence — HIGH when both users resolved uniquely and all dimensions pulled successfully; MEDIUM when any dimension was truncated by row limits; LOW when either user's role hierarchy could not be resolved (stops territory-level analysis).
- User header — two-column table: Username, Name, Active, Profile, Role, Manager, Active PS count, Active PSG count.
- Per-dimension diff table — one section per dimension with three subsections (
Identical,Only A,Only B). Tables, not prose. - Effective access summary — "User A can read/edit/delete these objects; User B can read/edit/delete these objects" — reconstructed from flattened Object CRUD.
- Risk flags — ordered table: severity, dimension, delta description, recommended next step.
- Process Observations (Healthy / Concerning / Ambiguous / Suggested follow-ups):
- Healthy — deltas fall along known role/team lines; no P0 flags.
- Concerning — P0 flags, or Concerning under the specified
purpose. - Ambiguous — PSG component drift without PSA difference; two users with same Profile but different Role branches.
- Suggested follow-ups —
permission-set-architect(design remediation),sharing-audit-agent(record-visibility divergence),profile-to-permset-migrator(if Profile difference is the dominant delta).
- Citations — every skill, probe recipe, and MCP tool consulted.
Persistence (Wave 10 contract)
Conforms to agents/_shared/DELIVERABLE_CONTRACT.md.
- Markdown report:
docs/reports/user-access-diff/<run_id>.md - JSON envelope:
docs/reports/user-access-diff/<run_id>.json - Atomic write: both files succeed or neither is left on disk.
- Run ID: ISO-8601 UTC compact timestamp (colons → dashes) OR UUID; ≥ 8 chars.
- Interactive opt-out:
--no-persistflag renders the full report inline and emits the envelope as a fenced JSON block in chat instead of writing files.
Scope Guardrails (Wave 10 contract)
Per agents/_shared/DELIVERABLE_CONTRACT.md:
- Canonical data surface: this agent's declared probes + the MCP tool set. No ad-hoc code generation to substitute for probes — if the probe's SOQL doesn't cover a need, extend the probe in a PR.
- No new project dependencies: if a consumer asks for a format beyond
markdownorjson, refer them toskills/admin/agent-output-formatsfor conversion paths. Do NOT runnpm install/pip installin the consumer's project. - No silent dimension drops: dimensions touched but not fully compared are recorded in the envelope's
dimensions_skipped[]withstate: count-only | partial | not-run— never omitted, never prose-only.
Dimensions (Wave 10 contract)
The agent's envelope MUST place every dimension below in either dimensions_compared[] or dimensions_skipped[]. Partial or count-only coverage is recorded with state: count-only | partial, not elided.
| Dimension | Notes |
|---|---|
profile | Profile ID + name |
permission-sets | Direct PSA rows (excluding expired) |
psg-components | Flattened via PermissionSetGroupComponent |
object-crud | Per-sObject read/create/edit/delete/view-all/modify-all |
fls | Opt-in via include_field_permissions=true; default state: not-run with confidence_impact: NONE |
system-perms | ModifyAllData, ViewAllUsers, AuthorApex, etc. |
apex-classes | SetupEntityAccess where SetupEntityType='ApexClass' |
vf-pages | SetupEntityAccess where SetupEntityType='ApexPage' |
flow-access | SetupEntityAccess where SetupEntityType='FlowDefinition' |
custom-perms | SetupEntityAccess where SetupEntityType='CustomPermission' |
named-credentials | SetupEntityAccess where SetupEntityType IN ('NamedCredential', 'ExternalDataSource') |
public-groups | GroupMember where Group.Type='Regular' |
queues | GroupMember where Group.Type='Queue' |
territories | UserTerritory2Association; state: not-run when ETM not enabled |
Escalation / Refusal Rules
user_a==user_b→ refuse with codeREDUNDANT_INPUT.- Either user not found OR returns more than one row → refuse with code
INPUT_AMBIGUOUS; list candidates if > 1 row. target_org_aliasnot connected viasfCLI → refuse with codeORG_UNREACHABLE.- Both users are inactive → warn but proceed (valid for forensics / access-review); flag as Concerning in observations.
- User is a Platform User / External Identity / Community license type and
dimensionsincludesgroups— community-license group resolution uses different objects; note the limitation in observations and continue without groups. - Field-permission diff requested on users whose combined effective-PS set would return > 50,000
FieldPermissionsrows → refuse with codeSCOPE_TOO_BROAD; ask the caller to narrow to specific objects. - Request to "fix" or "align" the two users → refuse. This agent is read-only. Redirect to
permission-set-architect.
What This Agent Does NOT Do
- Does not grant, revoke, or modify Permission Set assignments, Profile membership, group membership, or any access record.
- Does not compute sharing-rule outcomes (OWD + role hierarchy + sharing rules). Two users with identical PSes can still see different records. Use
sharing-audit-agentfor that. - Does not explain WHY a permission was granted historically. Use field history + audit trail via
user-access-policiesskill guidance. - Does not propose a remediation PSG layout. Follow up with
permission-set-architect. - Does not chain to other agents automatically.
- Does not compare more than two users per invocation.