slashdev 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,438 @@
1
+ # Testing Patterns — Reference
2
+
3
+ Load this file when writing or reviewing Salesforce test classes.
4
+
5
+ ---
6
+
7
+ ## TestDataFactory
8
+
9
+ A centralized factory class that creates fully-formed test records. Every test class uses this
10
+ instead of building records inline — keeps tests DRY and ensures records pass validation rules.
11
+
12
+ ```apex
13
+ @IsTest
14
+ public class TestDataFactory {
15
+
16
+ public static List<Account> createAccounts(Integer count, Boolean doInsert) {
17
+ List<Account> accounts = new List<Account>();
18
+ for (Integer i = 0; i < count; i++) {
19
+ accounts.add(new Account(
20
+ Name = 'Test Account ' + i,
21
+ Industry = 'Technology',
22
+ BillingStreet = '123 Test St',
23
+ BillingCity = 'San Francisco',
24
+ BillingState = 'CA',
25
+ BillingPostalCode = '94105',
26
+ BillingCountry = 'US'
27
+ ));
28
+ }
29
+ if (doInsert) {
30
+ insert accounts;
31
+ }
32
+ return accounts;
33
+ }
34
+
35
+ public static List<Contact> createContacts(Integer count, Id accountId, Boolean doInsert) {
36
+ List<Contact> contacts = new List<Contact>();
37
+ for (Integer i = 0; i < count; i++) {
38
+ contacts.add(new Contact(
39
+ FirstName = 'Test',
40
+ LastName = 'Contact ' + i,
41
+ Email = 'test.contact' + i + '@example.com',
42
+ AccountId = accountId
43
+ ));
44
+ }
45
+ if (doInsert) {
46
+ insert contacts;
47
+ }
48
+ return contacts;
49
+ }
50
+
51
+ public static List<Opportunity> createOpportunities(Integer count, Id accountId, Boolean doInsert) {
52
+ List<Opportunity> opps = new List<Opportunity>();
53
+ for (Integer i = 0; i < count; i++) {
54
+ opps.add(new Opportunity(
55
+ Name = 'Test Opportunity ' + i,
56
+ AccountId = accountId,
57
+ StageName = 'Prospecting',
58
+ CloseDate = Date.today().addDays(30),
59
+ Amount = 10000
60
+ ));
61
+ }
62
+ if (doInsert) {
63
+ insert opps;
64
+ }
65
+ return opps;
66
+ }
67
+
68
+ public static User createTestUser(String profileName, Boolean doInsert) {
69
+ Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
70
+ String uniqueStr = String.valueOf(DateTime.now().getTime());
71
+ User u = new User(
72
+ FirstName = 'Test',
73
+ LastName = 'User ' + uniqueStr,
74
+ Email = 'testuser' + uniqueStr + '@example.com',
75
+ Username = 'testuser' + uniqueStr + '@example.com.test',
76
+ Alias = 'tuser',
77
+ TimeZoneSidKey = 'America/Los_Angeles',
78
+ LocaleSidKey = 'en_US',
79
+ EmailEncodingKey = 'UTF-8',
80
+ LanguageLocaleKey = 'en_US',
81
+ ProfileId = p.Id
82
+ );
83
+ if (doInsert) {
84
+ insert u;
85
+ }
86
+ return u;
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Design principles
92
+
93
+ - **`doInsert` parameter** — lets callers get records pre-insert for field modification,
94
+ or inserted for immediate use. Both paths are common.
95
+ - **Populate required fields** — factory records must pass all validation rules and required
96
+ field constraints without the caller needing to know which fields are mandatory.
97
+ - **Use unique values** — timestamps or counters in Name/Email to avoid duplicate rule
98
+ violations in bulk tests.
99
+
100
+ ---
101
+
102
+ ## Bulk Testing
103
+
104
+ Every test should insert 200+ records to prove the code under test handles a full trigger
105
+ batch. A test that works with 1 record and fails with 200 has a bulkification bug.
106
+
107
+ ```apex
108
+ @IsTest
109
+ private class AccountTriggerTest {
110
+
111
+ @IsTest
112
+ static void shouldHandleFullBatch() {
113
+ // 200 = one full trigger batch
114
+ List<Account> accounts = TestDataFactory.createAccounts(200, false);
115
+
116
+ Test.startTest();
117
+ insert accounts;
118
+ Test.stopTest();
119
+
120
+ // Assert on outcomes — query fresh to see trigger side-effects
121
+ List<Account> inserted = [
122
+ SELECT Id, Custom_Status__c
123
+ FROM Account
124
+ WHERE Id IN :accounts
125
+ ];
126
+ Assert.areEqual(200, inserted.size(), 'All 200 accounts should be inserted');
127
+ for (Account a : inserted) {
128
+ Assert.areEqual('New', a.Custom_Status__c,
129
+ 'Trigger should set default status to New');
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Why 200?
136
+
137
+ - Triggers fire in batches of up to 200 records (Data Loader, Bulk API, batch Apex).
138
+ - `Test.startTest()` resets governor limits, so the 200-record DML gets a fresh set of limits
139
+ — mimicking real-world trigger execution.
140
+
141
+ ---
142
+
143
+ ## Mocking HTTP Callouts
144
+
145
+ Salesforce blocks real HTTP callouts in tests. Implement `HttpCalloutMock` and register it
146
+ with `Test.setMock`.
147
+
148
+ ### Single-endpoint mock
149
+
150
+ ```apex
151
+ @IsTest
152
+ public class ExternalAccountServiceMock implements HttpCalloutMock {
153
+
154
+ private Integer statusCode;
155
+ private String body;
156
+
157
+ public ExternalAccountServiceMock(Integer statusCode, String body) {
158
+ this.statusCode = statusCode;
159
+ this.body = body;
160
+ }
161
+
162
+ public HttpResponse respond(HttpRequest req) {
163
+ HttpResponse res = new HttpResponse();
164
+ res.setStatusCode(this.statusCode);
165
+ res.setHeader('Content-Type', 'application/json');
166
+ res.setBody(this.body);
167
+ return res;
168
+ }
169
+ }
170
+ ```
171
+
172
+ ### Using the mock in a test
173
+
174
+ ```apex
175
+ @IsTest
176
+ private class ExternalAccountServiceTest {
177
+
178
+ @IsTest
179
+ static void shouldDeserializeSuccessResponse() {
180
+ String mockBody = '{"id": "EXT-001", "name": "Acme Corp", "status": "active"}';
181
+ Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(200, mockBody));
182
+
183
+ Test.startTest();
184
+ ExternalAccountService.ExternalAccount result =
185
+ ExternalAccountService.getAccount('EXT-001');
186
+ Test.stopTest();
187
+
188
+ Assert.isNotNull(result, 'Result should not be null for 200 response');
189
+ Assert.areEqual('EXT-001', result.id, 'ID should match mock response');
190
+ Assert.areEqual('Acme Corp', result.name, 'Name should match mock response');
191
+ }
192
+
193
+ @IsTest
194
+ static void shouldReturnNullFor404() {
195
+ Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(404, ''));
196
+
197
+ Test.startTest();
198
+ ExternalAccountService.ExternalAccount result =
199
+ ExternalAccountService.getAccount('NONEXISTENT');
200
+ Test.stopTest();
201
+
202
+ Assert.isNull(result, 'Should return null for 404');
203
+ }
204
+
205
+ @IsTest
206
+ static void shouldThrowOnServerError() {
207
+ Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(500, 'Internal Server Error'));
208
+
209
+ Test.startTest();
210
+ try {
211
+ ExternalAccountService.getAccount('EXT-001');
212
+ Assert.fail('Should have thrown CalloutException');
213
+ } catch (CalloutException e) {
214
+ Assert.isTrue(e.getMessage().contains('500'), 'Exception should contain status code');
215
+ }
216
+ Test.stopTest();
217
+ }
218
+ }
219
+ ```
220
+
221
+ ### Multi-endpoint mock
222
+
223
+ For classes that call multiple endpoints, route by URL in `respond()`:
224
+
225
+ ```apex
226
+ public HttpResponse respond(HttpRequest req) {
227
+ String endpoint = req.getEndpoint();
228
+ HttpResponse res = new HttpResponse();
229
+ res.setHeader('Content-Type', 'application/json');
230
+
231
+ if (endpoint.contains('/accounts')) {
232
+ res.setStatusCode(200);
233
+ res.setBody('{"id": "ACC-001"}');
234
+ } else if (endpoint.contains('/contacts')) {
235
+ res.setStatusCode(200);
236
+ res.setBody('{"id": "CON-001"}');
237
+ } else {
238
+ res.setStatusCode(404);
239
+ res.setBody('Not found');
240
+ }
241
+ return res;
242
+ }
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Async Testing — Test.startTest() / Test.stopTest()
248
+
249
+ `Test.startTest()` and `Test.stopTest()` do two things:
250
+
251
+ 1. **Reset governor limits** — code between them gets a fresh set of limits, isolating the
252
+ code under test from test setup overhead.
253
+ 2. **Force async execution** — `@future`, `Queueable`, and `Batch Apex` jobs enqueued between
254
+ `startTest`/`stopTest` execute synchronously at `stopTest()`, making them testable.
255
+
256
+ ### Testing a Queueable
257
+
258
+ ```apex
259
+ @IsTest
260
+ private class SyncAccountToERPTest {
261
+
262
+ @IsTest
263
+ static void shouldCallERPEndpoint() {
264
+ Account a = TestDataFactory.createAccounts(1, true)[0];
265
+ Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(200, '{"status":"ok"}'));
266
+
267
+ Test.startTest();
268
+ System.enqueueJob(new SyncAccountToERP(new List<Id>{ a.Id }));
269
+ Test.stopTest(); // Queueable executes here synchronously
270
+
271
+ // Assert on side effects — e.g., a log record, field update, etc.
272
+ }
273
+ }
274
+ ```
275
+
276
+ ### Testing Batch Apex
277
+
278
+ ```apex
279
+ @IsTest
280
+ private class DeactivateStaleLeadsBatchTest {
281
+
282
+ @IsTest
283
+ static void shouldDeactivateStaleLeads() {
284
+ List<Lead> leads = new List<Lead>();
285
+ for (Integer i = 0; i < 200; i++) {
286
+ leads.add(new Lead(
287
+ FirstName = 'Test', LastName = 'Lead ' + i, Company = 'TestCo',
288
+ Status = 'Open'
289
+ ));
290
+ }
291
+ insert leads;
292
+ // Backdate LastActivityDate via Test.setCreatedDate or a field update
293
+ // (LastActivityDate is read-only — use a proxy field or adjust the query for testing)
294
+
295
+ Test.startTest();
296
+ Database.executeBatch(new DeactivateStaleLeadsBatch(), 200);
297
+ Test.stopTest(); // All batch execute() calls run here
298
+
299
+ List<Lead> updated = [SELECT Status FROM Lead WHERE Id IN :leads];
300
+ for (Lead l : updated) {
301
+ Assert.areEqual('Closed - Not Converted', l.Status, 'Stale leads should be deactivated');
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ ---
308
+
309
+ ## System.runAs — Testing Sharing and FLS
310
+
311
+ `System.runAs` executes code in the context of a specific user, enforcing their profile
312
+ permissions, sharing rules, and field-level security. Use it to verify that `with sharing`
313
+ classes and FLS checks work correctly.
314
+
315
+ ```apex
316
+ @IsTest
317
+ private class AccountServiceTest {
318
+
319
+ @IsTest
320
+ static void shouldEnforceSharingRules() {
321
+ // Create a restricted user
322
+ User restrictedUser = TestDataFactory.createTestUser('Standard User', true);
323
+
324
+ // Create an account owned by the running user (not the restricted user)
325
+ Account privateAccount = TestDataFactory.createAccounts(1, true)[0];
326
+
327
+ System.runAs(restrictedUser) {
328
+ Test.startTest();
329
+ List<Account> visible = new AccountService().getVisibleAccounts();
330
+ Test.stopTest();
331
+
332
+ // Restricted user should NOT see the private account
333
+ // (assuming OWD for Account is Private)
334
+ Set<Id> visibleIds = new Set<Id>();
335
+ for (Account a : visible) {
336
+ visibleIds.add(a.Id);
337
+ }
338
+ Assert.isFalse(visibleIds.contains(privateAccount.Id),
339
+ 'Restricted user should not see accounts they do not own');
340
+ }
341
+ }
342
+ }
343
+ ```
344
+
345
+ ### Caveats
346
+
347
+ - `System.runAs` does NOT count against governor limits for the enclosing test.
348
+ - Mixed DML restrictions apply — inserting a User and a standard object in the same
349
+ transaction throws an error. Insert the user first, then use `System.runAs` for the rest.
350
+ - `System.runAs` only works in `@IsTest` methods.
351
+
352
+ ---
353
+
354
+ ## @testSetup — Shared Test Data
355
+
356
+ `@testSetup` runs once per test class and creates records available to all `@IsTest` methods
357
+ in that class. Each test method gets its own copy (rolled back after each method), so tests
358
+ remain independent.
359
+
360
+ ```apex
361
+ @IsTest
362
+ private class OpportunityServiceTest {
363
+
364
+ @testSetup
365
+ static void setupData() {
366
+ List<Account> accounts = TestDataFactory.createAccounts(5, true);
367
+ for (Account a : accounts) {
368
+ TestDataFactory.createOpportunities(10, a.Id, true);
369
+ }
370
+ }
371
+
372
+ @IsTest
373
+ static void shouldCalculateTotalPipeline() {
374
+ Test.startTest();
375
+ Decimal total = OpportunityService.getTotalPipeline();
376
+ Test.stopTest();
377
+
378
+ Assert.areEqual(500000, total, '5 accounts x 10 opps x $10,000 = $500,000');
379
+ }
380
+
381
+ @IsTest
382
+ static void shouldFilterByStage() {
383
+ // This test gets the SAME data as above — fresh copy, not affected by other tests
384
+ List<Opportunity> opps = [SELECT Id FROM Opportunity WHERE StageName = 'Prospecting'];
385
+ Assert.areEqual(50, opps.size(), 'All 50 opps should be in Prospecting stage');
386
+ }
387
+ }
388
+ ```
389
+
390
+ ### Caveats
391
+
392
+ - `@testSetup` cannot be used with `@IsTest(SeeAllData=true)` — they are mutually exclusive.
393
+ - Records created in `@testSetup` are not visible to the `@testSetup` method's own
394
+ `System.runAs` blocks — each `@IsTest` method sees a fresh snapshot.
395
+ - If any `@IsTest` method needs different data, create it within that method — don't
396
+ over-centralize.
397
+
398
+ ---
399
+
400
+ ## Assert Patterns
401
+
402
+ ### Modern Assert class (Spring '22+)
403
+
404
+ Prefer `Assert` over the legacy `System.assert*` methods. The modern class has clearer
405
+ method names and better error messages.
406
+
407
+ ```apex
408
+ // Equality
409
+ Assert.areEqual(expected, actual, 'Descriptive message explaining what went wrong');
410
+ Assert.areNotEqual(unexpected, actual, 'Values should differ because...');
411
+
412
+ // Null checks
413
+ Assert.isNull(value, 'Should be null because...');
414
+ Assert.isNotNull(value, 'Should not be null because...');
415
+
416
+ // Boolean
417
+ Assert.isTrue(condition, 'Condition should be true because...');
418
+ Assert.isFalse(condition, 'Condition should be false because...');
419
+
420
+ // Forced failure (in catch blocks for expected exceptions)
421
+ Assert.fail('Should have thrown an exception');
422
+
423
+ // Instance check
424
+ Assert.isInstanceOfType(obj, Account.class, 'Should be an Account');
425
+ ```
426
+
427
+ ### Rules
428
+
429
+ - **Always include a message parameter.** When a test fails in a CI pipeline, the message
430
+ is the only context the developer has. "Expected 200 but got 0" is useless without
431
+ "All accounts should have been inserted by the bulk trigger."
432
+ - **Assert on specific values, not just non-null.** `Assert.isNotNull(result)` proves the
433
+ query returned something — it doesn't prove it returned the *right* thing.
434
+ - **One logical assertion per test method** when practical. Multiple assertions are fine if
435
+ they verify the same operation's outcome (e.g., checking multiple fields on a record), but
436
+ avoid combining unrelated assertions that make failures ambiguous.
437
+ - **Never use `System.assertEquals` in new code** — it still works but `Assert.areEqual` is
438
+ the modern replacement with consistent naming.
@@ -0,0 +1,219 @@
1
+ ---
2
+ name: secure-deploy
3
+ description: Retrofit a project's CI/CD pipelines, Dockerfiles, and deploy scripts to use Socket Firewall (sfw) for all dependency installs. Use when the user asks to "secure deploy", "set up sfw in CI", "add socket firewall to production", or whenever you're editing a CI workflow / Dockerfile / deploy script that installs dependencies.
4
+ version: "1.0"
5
+ owner: michael@slashdev.io
6
+ ---
7
+
8
+ # Secure Deploy — Retrofit projects to use Socket Firewall in production
9
+
10
+ Slashdev requires every dependency install — local AND production — to be wrapped
11
+ with Socket Firewall (`sfw`). This protects against supply-chain attacks
12
+ (compromised packages, typosquats, dependency confusion) by scanning every package
13
+ against Socket's vulnerability/malware database before it's allowed to install.
14
+
15
+ When a developer asks you to set up production deployment, edit a CI pipeline, write
16
+ a Dockerfile, or deploy a project, **you must ensure every install command uses `sfw`**.
17
+
18
+ This skill walks you through the retrofit systematically.
19
+
20
+ ## When to use this skill
21
+
22
+ Trigger automatically (without being asked) whenever you're about to:
23
+ - Create or edit a `.github/workflows/*.yml` file with an `npm`/`pnpm`/`yarn`/`bun`/`composer`/`pip` install step
24
+ - Create or edit a `Dockerfile` that runs an install command
25
+ - Create or edit `.gitlab-ci.yml`, `bitbucket-pipelines.yml`, `circleci/config.yml`, or any other CI config
26
+ - Create or edit a `deploy.sh`, `Procfile`, or `Makefile` install target
27
+ - Modify the `scripts` section of `package.json` to add an install-related step
28
+
29
+ Trigger explicitly when the user says: "secure deploy", "add sfw to CI", "set up socket firewall in production", "retrofit production for socket firewall", "audit deploy for sfw".
30
+
31
+ ## Step-by-step retrofit playbook
32
+
33
+ ### Step 1 — Discover deployment surface
34
+
35
+ Use Glob to find every file that could install dependencies in CI or production:
36
+
37
+ ```
38
+ .github/workflows/*.yml
39
+ .github/workflows/*.yaml
40
+ .gitlab-ci.yml
41
+ .gitlab/*.yml
42
+ bitbucket-pipelines.yml
43
+ .circleci/config.yml
44
+ azure-pipelines.yml
45
+ Jenkinsfile
46
+ Dockerfile
47
+ Dockerfile.*
48
+ *.dockerfile
49
+ docker-compose*.yml
50
+ Procfile
51
+ deploy.sh
52
+ deploy/*.sh
53
+ scripts/deploy*.sh
54
+ Makefile
55
+ package.json
56
+ ```
57
+
58
+ For each file found, Read it and look for any of these patterns:
59
+
60
+ - `npm install`, `npm i`, `npm ci`, `npm add`
61
+ - `pnpm install`, `pnpm i`, `pnpm add`
62
+ - `yarn`, `yarn install`, `yarn add`
63
+ - `bun install`, `bun i`, `bun add`
64
+ - `composer install`, `composer require`, `composer update`
65
+ - `pip install`, `pip3 install`, `uv install`, `uv sync`, `uv add`
66
+
67
+ Note any line that already starts with `sfw ` — those are already secured and don't
68
+ need to be touched.
69
+
70
+ ### Step 2 — Plan the changes
71
+
72
+ For each install command found, plan one of these rewrites:
73
+
74
+ **a) GitHub Actions workflow YAML**
75
+
76
+ Before:
77
+ ```yaml
78
+ - name: Install dependencies
79
+ run: npm ci
80
+ ```
81
+
82
+ After:
83
+ ```yaml
84
+ - name: Install Socket Firewall
85
+ run: npm install -g @socketsecurity/cli
86
+ - name: Install dependencies
87
+ run: sfw npm ci
88
+ ```
89
+
90
+ If a job has multiple install steps, only one `Install Socket Firewall` step is needed
91
+ per job (it persists across steps because they share the same runner).
92
+
93
+ **b) Dockerfile**
94
+
95
+ Before:
96
+ ```dockerfile
97
+ RUN npm ci
98
+ ```
99
+
100
+ After:
101
+ ```dockerfile
102
+ RUN npm install -g @socketsecurity/cli && sfw npm ci
103
+ ```
104
+
105
+ For Composer:
106
+ ```dockerfile
107
+ RUN npm install -g @socketsecurity/cli && sfw composer install --no-dev --optimize-autoloader
108
+ ```
109
+
110
+ (npm is the simplest install vector for sfw itself; if the base image doesn't have
111
+ Node, install Node first or use the official Socket installer.)
112
+
113
+ **c) Shell deploy scripts (`deploy.sh`, etc.)**
114
+
115
+ Before:
116
+ ```bash
117
+ npm ci --omit=dev
118
+ ```
119
+
120
+ After:
121
+ ```bash
122
+ # Ensure sfw is available
123
+ if ! command -v sfw >/dev/null 2>&1; then
124
+ npm install -g @socketsecurity/cli
125
+ fi
126
+ sfw npm ci --omit=dev
127
+ ```
128
+
129
+ **d) `package.json` scripts**
130
+
131
+ Before:
132
+ ```json
133
+ {
134
+ "scripts": {
135
+ "postinstall": "npm install --prefix ./client"
136
+ }
137
+ }
138
+ ```
139
+
140
+ After:
141
+ ```json
142
+ {
143
+ "scripts": {
144
+ "postinstall": "sfw npm install --prefix ./client"
145
+ }
146
+ }
147
+ ```
148
+
149
+ (Don't add the `npm install -g @socketsecurity/cli` step inside `package.json` — assume
150
+ sfw is already installed wherever this script runs. If it's running in CI, the workflow
151
+ or Dockerfile should have installed sfw at the top.)
152
+
153
+ ### Step 3 — Show the diff and confirm
154
+
155
+ Before writing any changes, show the user a clear summary:
156
+
157
+ > Found N install commands across M files. Proposed changes:
158
+ >
159
+ > 1. `.github/workflows/ci.yml` — wrap `npm ci` in build job (+ add sfw install step)
160
+ > 2. `Dockerfile` — wrap `RUN npm ci` with sfw install
161
+ > 3. `package.json` — wrap postinstall script
162
+ >
163
+ > Apply these changes?
164
+
165
+ Use AskUserQuestion if there's any ambiguity (e.g. multiple jobs in a workflow, unclear
166
+ whether a step is part of the build or local-only). For Dockerfiles especially, ask
167
+ about the base image — if it doesn't have Node, the sfw install command needs to be
168
+ different.
169
+
170
+ ### Step 4 — Apply the changes
171
+
172
+ Use Edit to apply each change. After applying:
173
+
174
+ 1. **Validate YAML/JSON syntax** — for any modified `.yml` or `.json` file, run a quick
175
+ parse check (e.g. `python3 -c 'import yaml,sys; yaml.safe_load(open("path"))'` for
176
+ YAML, or `python3 -m json.tool path` for JSON). Fix any syntax errors.
177
+ 2. **Run a build smoke test if possible** — for Dockerfiles, suggest the user run
178
+ `docker build .` locally. For GitHub Actions, suggest pushing to a feature branch
179
+ and watching the workflow run.
180
+ 3. **Document what was changed** — print a final summary listing every file modified
181
+ and what was added.
182
+
183
+ ### Step 5 — Verify CI doesn't already use a different security tool
184
+
185
+ If during Step 1 you find references to other security scanners (Snyk, Dependabot,
186
+ Trivy, npm audit, etc.), don't remove them — they complement Socket Firewall, they
187
+ don't replace it. Mention to the user: "I found existing security scanning via X. I
188
+ left it in place — Socket Firewall complements it by blocking malicious packages
189
+ *before* they install, while X scans the installed tree."
190
+
191
+ ## Important conventions
192
+
193
+ - **Never silently change install commands.** Always show the diff and get
194
+ confirmation before writing CI/deploy files. These are production-critical files.
195
+ - **Don't add sfw to dev-only `node_modules` install steps in `package.json`** unless
196
+ the user explicitly asks. The PreToolUse hook handles dev-side enforcement
197
+ automatically; only retrofit `package.json` scripts that run in CI/deploy contexts.
198
+ - **Don't try to install Socket Firewall globally on the user's machine** as part of
199
+ this skill. That's the developer's responsibility, and the slashdev hook will warn
200
+ them if it's missing. This skill only modifies project files.
201
+ - **Don't touch `.lock` files** (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`,
202
+ `composer.lock`, `bun.lockb`). They're generated.
203
+ - **Preserve existing flags** — when wrapping `npm ci --omit=dev` with `sfw`, keep
204
+ the `--omit=dev` flag.
205
+
206
+ ## Output format
207
+
208
+ Always end the skill with:
209
+
210
+ ```
211
+ ✓ Secured N install commands across M files:
212
+ - <file>: <change summary>
213
+ - <file>: <change summary>
214
+
215
+ Next steps:
216
+ - Test locally (if applicable)
217
+ - Push to a feature branch and watch CI
218
+ - Confirm sfw is available in your CI environment (the install step is now in place)
219
+ ```