Fls audit
Static-analyze Apex source for DML and SOQL operations missing CRUD/FLS enforcement. Reports every `insert`/`update`/`delete`/`upsert`/`merge` and dynamic SOQL that doesn't use USER_MODE / SECURITY_ENFORCED / Schema.DescribeSObjectResult checks.From its SKILL.md
npx -y skills add vhmarquez/argo --skill fls-auditAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.2 KB, as published. Nobody here has run it
You are auditing CRUD and Field-Level Security (FLS) enforcement in Apex. Modern Salesforce orgs require WITH USER_MODE (or WITH SECURITY_ENFORCED) on SOQL and as user on DML, or explicit Schema.sObjectType.Foo.fields.bar.isAccessible() checks. This skill flags places where those guards are missing.
Read Project Config First
source "${CLAUDE_PLUGIN_ROOT}/hooks/lib/config.sh"
APEX_SRC="$(sf_config_get '.paths.apexSource' "$ENV")"
TEST_SUFFIX="$(sf_config_get '.naming.apex.testSuffix' "$ENV")"
SHARING_DEFAULT="$(sf_config_get '.platform.sharingDefault' "$ENV")"
Input
$ARGUMENTS:
- (empty) — audit all production Apex (skip test classes)
<ClassName>— audit one class--include-tests— include test classes (rarely useful — test code intentionally bypasses FLS)--ci/--format json|sarif/--out <path>
Steps
1. Walk Apex files
For each .cls and .trigger in ${APEX_SRC}:
- Skip test classes unless
--include-tests - Read the file content
2. Identify DML statements
Pattern (Apex source is straightforward to scan with regex; full AST is overkill):
\b(insert|update|upsert|delete|merge)\s+(?!as\s+(user|system))(?!\bnew\b)\s*([A-Za-z_][\w.]*)
For each match:
- Capture the verb (
insert,update, etc.) and the variable / record being touched - Look for
as useroras systemqualifier - If neither, this is a candidate finding
3. Identify SOQL statements
Pattern:
\[\s*SELECT\s+(.+?)\s+FROM\s+(\w+)
For each match:
- Capture the SELECT clause and FROM target
- Search the same statement for
WITH USER_MODEorWITH SECURITY_ENFORCED - If neither, candidate finding
4. Identify dynamic SOQL
\bDatabase\.(query|getQueryLocator)\s*\(
For each match:
- Inspect the query string composition (look for string concatenation up to N lines back)
- If the query string is built from variables and the call doesn't include
with AccessLevel.USER_MODE, candidate finding
5. Identify Database.* DML
\bDatabase\.(insert|update|upsert|delete|merge)\s*\(
Look for , AccessLevel.USER_MODE or , true /*allOrNothing*/, AccessLevel.USER_MODE parameter.
6. Filter and classify
| Heuristic | Severity | Rule ID |
|---|---|---|
DML without as user/as system/AccessLevel.USER_MODE | error | FLS-DML-NO-CHECK |
Static SOQL without WITH USER_MODE and FROM is custom or sensitive standard object (Account/Contact/Case/User/Lead/Opportunity) | warning | FLS-SOQL-NO-CHECK |
Dynamic SOQL via Database.query without AccessLevel.USER_MODE | error | FLS-DYNAMIC-SOQL-NO-CHECK |
Class declared without sharing | warning | FLS-WITHOUT-SHARING (cross-link to /sharing-review) |
Skip if the line contains a leading comment with // fls-audit:disable (suppression).
7. Output
Default Markdown:
# FLS Audit: <project.name>
Apex classes scanned: 47 (production, excluding tests)
Sharing default: with sharing
Run at: 2026-04-28T13:00:00Z
## Findings
### Critical (DML without enforcement) — 3
- `force-app/.../OrderService.cls:84` — `insert order;` (use `insert as user order;`)
- `force-app/.../OrderApiClient.cls:121` — `update updates;` (use `update as user updates;`)
- ...
### Critical (Dynamic SOQL without enforcement) — 1
- `force-app/.../SearchController.cls:42` — `Database.query(soql)` — pass `AccessLevel.USER_MODE`
### High (Static SOQL on sensitive object without enforcement) — 4
- `force-app/.../AccountReader.cls:18` — `[SELECT Id, Name FROM Account WHERE ...]` — add `WITH USER_MODE`
- ...
## Suppressions in source
- `force-app/.../SystemAdminQueue.cls:55` — `// fls-audit:disable` (justified: scheduled job runs as system)
## How to fix
- DML: append `as user` for caller-context, `as system` for elevated context (justify in comment)
- Static SOQL: append `WITH USER_MODE` to the query
- Dynamic SOQL: pass `with AccessLevel.USER_MODE` as the second arg to `Database.query`
- Manual checks: `Schema.sObjectType.Foo.fields.Bar.isAccessible()` before reading custom-extracted fields
CI mode: emit findings via sarif_emit "argo/fls-audit" "$VERSION".
8. Exit codes
- 0 — no error-level findings
- 1 — any error
- 2 — file-not-found / config error
Rules
- Static analysis only. No org queries; no PMD dependency
- Don't double-fire with /security-scan. PMD's
ApexCRUDViolationcovers some of this. The skills overlap intentionally — both will catch DML issues; this skill adds dynamic-SOQL specifics. CI pipelines should run both - Honor sharing context. A class that's
without sharingis reported as a finding here too (cross-linked to/sharing-review) - Suppressions need justification.
// fls-audit:disableis allowed but the line should also have a// reason: ...comment within 3 lines. If missing, downgrade to a warning telling the user to justify - Don't audit test classes unless
--include-testsis passed — they're allowed to bypass FLS