56 results available
Sign in
Skills directory

salesforce-apex-quality

github

·

Last changed
Hardened

This skill has been hardened by Chainguard

Last changed

This version passed the hardening gate. Hardening applied 13 targeted changes and preserved all other text. Classified findings before hardening: 0. Classified findings closed: 0. Classified findings remaining: 0. Unclassified scanner findings remaining: 0. Review findings closed: 9. Review findings remaining: 1.


Open before hardening

None

Open after hardening

None


Expand
File
Group
Subtype
Severity
Status

No findings available

0 findings


SKILL.md

Salesforce Apex Quality Guardrails

Apply these checks to every Apex class, trigger, and test file you write or review. When reviewing, report findings and propose changes. Edit files only within the scope the user requested, or after the user confirms.

Step 1 — Governor Limit Safety Check

Scan for these patterns before declaring any Apex file acceptable:

SOQL and DML in Loops — Automatic Fail

// ❌ NEVER — causes LimitException at scale
for (Account a : accounts) {
    List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL in loop
    update a; // DML in loop
}

// ✅ ALWAYS — collect, then query/update once
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    if (!contactsByAccount.containsKey(c.AccountId)) {
        contactsByAccount.put(c.AccountId, new List<Contact>());
    }
    contactsByAccount.get(c.AccountId).add(c);
}
update accounts; // DML once, outside the loop

Rule: if you see [SELECT or Database.query, insert, update, delete, upsert, merge inside a for loop body — stop and refactor before proceeding.

Step 2 — Sharing Model Verification

Every class must declare its sharing intent explicitly. Undeclared sharing runs in the caller's mode when called from another class, but runs without sharing when the class is the entry point of the transaction (for example a UI controller, REST endpoint, or trigger). Treat an undeclared entry point class as a security finding, and prefer inherited sharing over leaving out a declaration for framework and utility classes.

DeclarationWhen to use

public with sharing class Foo

Default for all service, handler, selector, and controller classes

public without sharing class Foo

Only when the class must run elevated (e.g. system-level logging). Keep the elevated code in a minimal helper class, require a code comment explaining why, still enforce CRUD/FLS where user data is returned, and get the user's confirmation before generating such a class.

public inherited sharing class Foo

Framework entry points that should respect the caller's sharing context

If a class does not have one of these three declarations, when writing code, add it before writing anything else; when reviewing, report it as a finding.

Step 3 — CRUD / FLS Enforcement

Apex code that reads or writes records on behalf of a user must verify object and field access. The platform does not enforce FLS or CRUD automatically in Apex.

// Check before querying a field
if (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {
    throw new System.NoAccessException();
}

// Or use WITH USER_MODE in SOQL (API 56.0+)
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :accId WITH USER_MODE];

// Or use Database.query with AccessLevel
List<Contact> contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);

Writes need the same protection. Run DML on behalf of users in user mode (USER_MODE), or remove fields the user cannot create or update before the write, and check that the user can create, update, or delete the object before the matching DML.

Rule: any Apex method callable from a UI component, REST endpoint, or @InvocableMethod must enforce CRUD/FLS. Sharing declarations such as with sharing control record visibility only and do not replace CRUD/FLS checks. Internal service methods called only from trusted contexts may leave out CRUD/FLS checks only with a documented reason in a code comment.

Step 4 — SOQL Injection Prevention

// ❌ NEVER — concatenates user input into SOQL string
String soql = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';

// ✅ ALWAYS — bind variable
List<Account> accounts = [SELECT Id FROM Account WHERE Name = :userInput];

// ✅ For dynamic SOQL with user-controlled field names — validate against a whitelist
Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};
if (!allowedFields.contains(userInput)) {
    throw new IllegalArgumentException('Field not permitted: ' + userInput);
}

For dynamic SOQL, pass user supplied values only as bind variables and run the query in user mode; escaping quotes is only a fallback. Add a field name to the query string only after it passes the allowlist check, ideally one built from Schema describe results.

Step 5 — Modern Apex Idioms

Prefer current language features. Each idiom has its own minimum API version, so check the project's source API version before applying one:

Old patternModern replacement

if (obj != null) { x = obj.Field__c; }

x = obj?.Field__c;

x = (y != null) ? y : defaultVal;

x = y ?? defaultVal;

System.assertEquals(expected, actual)

Assert.areEqual(expected, actual)

System.assert(condition)

Assert.isTrue(condition)

[SELECT ... WHERE ...] in a user facing entry point with no access enforcement

[SELECT ... WHERE ... WITH USER_MODE] (system mode code such as batch jobs, trigger handlers, and logging may keep system mode with a documented reason)

Step 6 — PNB Test Coverage Checklist

Every feature must be tested across all three paths. Missing any one of these is a quality failure:

Positive Path

  • Expected input → expected output.
  • Assert the exact field values, record counts, or return values — not just that no exception was thrown.

Negative Path

  • Invalid input, null values, empty collections, and error conditions.
  • Assert that exceptions are thrown with the correct type and message.
  • Assert that no records were mutated when the operation should have failed cleanly.

Bulk Path

  • Insert/update/delete 200–251 records in a single test transaction.
  • Assert that all records processed correctly — no partial failures from governor limits.
  • Wrap the code under test in Test.startTest() / Test.stopTest(): startTest gives the code under test a fresh set of governor limits, and stopTest makes queued async work run before your assertions.

Test Class Rules

@isTest(SeeAllData=false)   // Required — no exceptions without a documented reason
private class AccountServiceTest {

    @TestSetup
    static void makeData() {
        // Create all test data here — use a factory if one exists in the project
    }

    @isTest
    static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {
        // Positive path
        List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
        Test.startTest();
        AccountService.processAccounts(accounts);
        Test.stopTest();
        // Assert meaningful outcomes — not just no exception
        List<Account> updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];
        Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');
    }
}

Step 7 — Trigger Architecture Checklist

  • One trigger per object. If a second trigger exists, consolidate into the handler.
  • Trigger body contains only: context checks, handler invocation, and routing logic.
  • No business logic, SOQL, or DML directly in the trigger body.
  • If a trigger framework (Trigger Actions Framework, ff-apex-common, custom base class) is already in use — extend it. Do not create a parallel pattern.
  • Handler class is with sharing unless the trigger requires elevated access. In that case, keep the elevated logic in a minimal helper class with a documented reason.

Quick Reference — Hardcoded Anti-Patterns Summary

PatternAction

SOQL inside for loop

Refactor: query before the loop, operate on collections

DML inside for loop

Refactor: collect mutations, DML once after the loop

Class missing sharing declaration

Add with sharing (or document why without sharing)

escape="false" on user data (VF)

Remove — auto-escaping enforces XSS prevention

Empty catch block

Add logging and appropriate re-throw or error handling; log only the exception type, message, and record IDs, and keep field values, personal data, credentials, and full payloads out of logs

String-concatenated SOQL with user input

Replace with bind variable or whitelist validation

Test with no assertion

Add a meaningful Assert.* call

System.assert / System.assertEquals style

Upgrade to Assert.isTrue / Assert.areEqual

Hardcoded record ID ('001...')

Replace with queried or inserted test record ID

Install

Install with chainctl
chainctl skill install public/github/salesforce-apex-quality:latest
Get started with chainctl
View source

Harden your own skills

Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.

Provenance

Upstream sourcehttps://github.com/github/awesome-copilot
Upstream commit2201a49dd6f972ac4f685d03361d58c9c9206690
OCI reference

skills.cgr.dev/public/github.com/github/skills/salesforce-apex-quality:latest

License

MIT

Files

HARDENING.md

LICENSE

SKILL.md


The trusted source for open source

Talk to an expert
PrivacyTerms

Product

Chainguard ContainersChainguard LibrariesChainguard VMsChainguard OS PackagesChainguard ActionsChainguard Agent SkillsIntegrationsPricing
© 2026 Chainguard, Inc. All Rights Reserved.
Chainguard® and the Chainguard logo are registered trademarks of Chainguard, Inc. in the United States and/or other countries.
The other respective trademarks mentioned on this page are owned by the respective companies and use of them does not imply any affiliation or endorsement.