permset-audit
Skill1 starsChanged 7 months agoMITView on GitHub
Build a permission audit matrix mapping every object/field in the target org to the profiles and permission sets that grant access. Flag orphan permsets, profile-vs-permset CRUD divergence, and fields with no read access from any profile/permset.
In this repository53 other files it gives its agents.
- templates/CLAUDE.mdCLAUDE.md
- adrSkill
- agent-deploySkill
- agent-discoverSkill
- agent-eval-trendSkill
- agent-exchange-listSkill
- agent-specSkill
- agent-testSkill
- agent-vs-flow-vs-apexSkill
- before-vs-after-triggerSkill
- code-reviewSkill
- complexitySkill
- coverage-trendSkill
- dead-codeSkill
- dependency-graphSkill
- deploySkill
- destructive-changesSkill
- diff-deploySkill
- erdSkill
- field-impactSkill
- flaky-test-finderSkill
- flow-auditSkill
- flow-vs-apexSkill
- fls-auditSkill
- gateway-configSkill
- generate-docsSkill
- limit-usageSkill
- lwc-vs-reactSkill
- mcp-tool-vs-restSkill
- notifySkill
- onboardSkill
- org-diffSkill
- org-exploreSkill
- package-versionSkill
- pattern-packSkill
- perf-reviewSkill
- pr-prepareSkill
- queueable-vs-batchSkill
- quick-deploySkill
- react-initSkill
- release-notesSkill
- scratch-orgSkill
- security-scanSkill
- sequence-diagramSkill
- sf-initSkill
- sharing-reviewSkill
- slack-agentSkill
- soql-analyzerSkill
- test-coverageSkill
- test-dataSkill
- test-planSkill
- trust-evalSkill
- trust-layer-auditSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 15
When the assistant sees exit code 77 from sf_cli_query with reason: SOQL_DATA_OBJECT and target: PermissionSetAssignment:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
You are building a permission audit matrix for the target org. The matrix is rows=objects/fields, columns=profile-or-permset, cells=R/CRUD/-. Use it to spot orphan permsets, fields that no one can see, and divergence between similarly named profiles and permsets.
Security: data-with-consent
This skill queries PermissionSetAssignment, which links permission sets to specific user records. That's customer data per the plugin's security model — the metadata-only allowlist blocks it by default, so the skill prompts the user for per-call consent before running.
The other queries used by this skill (Profile, PermissionSet, ObjectPermissions, FieldPermissions) ARE on the metadata allowlist and do not require consent.
When the assistant sees exit code 77 from sf_cli_query with reason: SOQL_DATA_OBJECT and target: PermissionSetAssignment:
[argo/security] CONSENT REQUIRED
Skill: /permset-audit
Org: <alias>
Action: SOQL query against PermissionSetAssignment
Records: ~N (one per active permset assignment in the org)
What enters your conversation context with Claude:
• User Ids (AssigneeId)
• Permission Set Ids (PermissionSetId)
• No field values, no email/name fields are projected
Choose:
[a] Allow once — runs this query (audit log: "permset-audit / soql / PermissionSetAssignment / allow-once")
[d] Deny this part — skip the assignments query; the audit will report orphan permsets without per-user assignments
[c] Cancel — abort the skillOn [a] re-invoke the same sf_cli_query after export ARGO_CONSENT_GRANTED=once. Per the plugin contract this is a single-use override; a follow-up call inside the same skill run requires another consent.
Read Project Config First
source "${CLAUDE_PLUGIN_ROOT}/hooks/lib/config.sh"
source "${CLAUDE_PLUGIN_ROOT}/hooks/lib/sf-cli.sh"
sf_cli_check || exit 2
ORG="$(sf_config_get '.platform.defaultTargetOrg' "$ENV")"
SKILL_NAME="permset-audit"; export SKILL_NAMEInput
$ARGUMENTS:
- (empty) — full audit of all custom objects + selected standard objects (Account, Contact, Case, Opportunity, Lead, User)
<object>— limit to a single sObject API name<object>.<field>— limit to a single field--include-managed— include objects from managed packages (default: skip)--target-org <alias>/--env <name>— standard overrides--ci/--format json|sarif/--out <path>
Steps
1. Pull profiles, permsets, and assignments
sf_cli_query "
SELECT Id, Name, UserType, IsCustom FROM Profile
" "$ORG" > /tmp/permset-audit-profiles.json
sf_cli_query "
SELECT Id, Name, Label, License.Name, Type, IsCustom
FROM PermissionSet
WHERE IsOwnedByProfile = false
" "$ORG" > /tmp/permset-audit-permsets.json
# PermissionSetAssignment links permsets to specific users (customer data) —
# routing through sf_cli_query fires the metadata-allowlist consent prompt here.
sf_cli_query "
SELECT AssigneeId, PermissionSetId
FROM PermissionSetAssignment
WHERE PermissionSet.IsOwnedByProfile = false
" "$ORG" > /tmp/permset-audit-assignments.json2. Pull object permissions
For the in-scope objects:
sf_cli_query "
SELECT ParentId, Parent.Name, Parent.Type, Parent.Profile.Name,
SObjectType, PermissionsRead, PermissionsCreate, PermissionsEdit,
PermissionsDelete, PermissionsViewAllRecords, PermissionsModifyAllRecords
FROM ObjectPermissions
WHERE SObjectType IN ('Account','Contact', ...)
" "$ORG"3. Pull field permissions (per object — paginate)
sf_cli_query "
SELECT ParentId, Parent.Name, Parent.Type, Parent.Profile.Name,
SObjectType, Field, PermissionsRead, PermissionsEdit
FROM FieldPermissions
WHERE SObjectType = 'Account'
" "$ORG"FieldPermissions queries are large; loop per object instead of one giant IN clause.
4. Build the matrix in memory
Pseudocode:
matrix = {} # (sobject, field) -> { permsetId -> "R" / "CRUD" / "-" }
for fp in field_permissions:
key = (fp.sobject, fp.field)
matrix.setdefault(key, {})
cell = matrix[key]
perms = []
if fp.PermissionsEdit: perms.append("U")
if fp.PermissionsRead: perms.append("R")
cell[fp.parent_id] = "".join(perms) or "-"Repeat for object-level perms.
5. Findings
PERM-ORPHAN-PERMSET(warning) — permset not assigned to any user, never granted access to any object/field used by any LWC/Apex in sourcePERM-FIELD-NO-READ(error) — custom field with no profile or permset granting read; nobody can see itPERM-DIVERGENCE(warning) — profile and similarly-named permset both grant access to the same object but with different CRUD masks (likely accidental)PERM-MASTER-DETAIL-NO-CRU(warning) — master object grants edit but child master-detail object doesn't (or vice versa) — likely a misconfigured share path
6. Output
Default Markdown:
# Permission Audit: <ORG>
Run at: ...
Scope: 27 custom objects + 6 standard
## Findings
### Critical (must fix)
- `Account.Risk_Score__c` — no profile or permset grants Read access (PERM-FIELD-NO-READ)
- ...
### High
- Permset `Sales_Old` is unassigned and grants no source-referenced access (PERM-ORPHAN-PERMSET)
- ...
## Coverage matrix excerpt: Account
| Field | Sales User | Sales Manager | Acme_Sales_PS | Acme_Manager_PS |
|-------|------------|----------------|----------------|-------------------|
| Name | R | RU | RU | RU |
| Risk_Score__c | - | - | RU | RU |
| ...
(Full matrix in CI JSON output.)CI JSON shape:
{
"org": "...", "ranAt": "...",
"findings": [{ "ruleId": "PERM-FIELD-NO-READ", "severity": "error", "message": "...", "file": "(metadata)", "line": 1 }],
"matrix": {
"Account": {
"Name": { "Sales User": "R", "Acme_Sales_PS": "RU" },
"Risk_Score__c": { "Acme_Sales_PS": "RU" }
}
}
}SARIF: each finding emitted via ${CLAUDE_PLUGIN_ROOT}/hooks/lib/sarif.sh. The matrix is omitted from SARIF (SARIF is for findings, not data) — emit JSON sidecar at <out>.matrix.json.
7. Exit codes
- 0 — no error-level findings
- 1 — any error-level finding
- 2 — query/setup error
Rules
- No write operations. This is read-only; never deploy or modify perms
- Page large queries. FieldPermissions on a heavy org can return tens of thousands of rows — paginate per sObject
- Excluded by default: managed-package objects (
namespacePrefixset), theUserobject's standard fields, and audit fields (CreatedDate, etc.) - Skip system permsets.
IsOwnedByProfile = truepermsets are profile-derived; we only audit user-managed permsets
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

