vulkro-sf fix
Suggests fixes for Apex security findings, and (with --ai --write)
applies the ones the deterministic detector re-validates. The command
runs a normal deterministic scan first: that scan is the read-only
baseline and the only source of the exit code. Everything the AI tier
adds on top is advisory.
AI assistance is advisory and local by default. It never changes a deterministic scan result: not a finding, not a severity, not the exit code. The published benchmark is AI-free.
Without --ai, fix reports how many findings are fixable Apex
candidates and does nothing else: no model is called and no file is
touched.
Synopsis
vulkro-sf fix [PATH] [flags]
PATH defaults to the current directory and points at an SFDX project
or a retrieved-metadata folder, exactly as with
vulkro-sf scan.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--ai | bool | off | Turn on the AI fix tier. On its own it is a read-only preview: validated diffs are printed, no file is modified. |
--write | bool | off | Apply only the verified diffs, in place, atomically (temp file + rename), each printed as a diff first. Only meaningful together with --ai (without --ai there are no validated diffs to apply, and the CLI says so). Conflicts with --dry-run. |
--dry-run | bool | off | Make the default read-only preview explicit: a note confirms no file will be modified. Conflicts with --write. |
--ai-model <MODEL> | string | resolved | Model tag override (highest precedence). Overrides VULKRO_AI_MODEL, the saved vulkro-sf ai use selection, and the smart default. |
--ai-url <URL> | string | resolved | AI runtime base url override (OpenAI-compatible). A loopback url is allowed under VULKRO_OFFLINE; a cloud url is refused offline. |
--no-ai | bool | off | Disable the AI layer for this run: only the fixable candidate count is reported. |
--format <fmt> | enum | table | table (human diffs and verdicts) or json (the fixable findings, each carrying an additive ai_fix object when --ai is on). |
What the AI tier fixes
The tier proposes patches only for the mandatory-review Apex classes, where the correct remediation idiom is well defined:
| Finding class | Detector | Remediation idiom the model is asked for |
|---|---|---|
| CRUD / FLS enforcement | apex_crud_fls | Add WITH SECURITY_ENFORCED or WITH USER_MODE to the query, or use user-mode DML / Security.stripInaccessible before the write. |
| Sharing declaration | apex_without_sharing | Convert without sharing to with sharing (or inherited sharing), changing only the class declaration keyword. |
| SOQL injection | apex_soql_injection | Replace string concatenation with a static bracketed query and :bind variables, or wrap unavoidable dynamic input in String.escapeSingleQuotes. |
Findings outside these classes are never sent to a model.
How a fix gets verified
The model proposes; the deterministic detector judges. For each fixable finding, the local model receives a bounded code window (the finding span plus three context lines on each side) and must reply with exactly one fenced replacement block. Any other reply shape is a refusal. Each proposal then goes through the validation loop:
- Stale check. The current file must still byte-match the lines the scan saw. If the source changed, the proposal is refused rather than spliced onto drifted code.
- Parse-validity gate. The patched class must not introduce new syntax errors relative to the original. A patch that breaks parsing could otherwise make the detector find nothing, so a broken edit can never "clear" a finding by making the file unparseable.
- Deterministic re-scan. The patch is applied to the class temporarily and the full deterministic Apex detector suite re-runs over the patched file. The original file is always restored afterwards; validation never leaves your source modified.
A fix is verified only when that fresh deterministic re-scan no longer reports the originating finding, no new finding of any class appears anywhere in the file, and the file still parses. Verification does not claim that your project compiles against org metadata or that tests pass: it claims the scanner's own detectors no longer flag the issue and did not gain a new one.
Anything else is refused, with a reason: the finding is still reported after the patch, the patch cleared it but introduced a new deterministic finding, the patch does not parse, the model reply had no fenced block, or the AI runtime was unreachable. A refusal is a first-class outcome, never an error, and it never changes the exit code.
--ai --write applies only the verified diffs, each printed first,
atomically (temp file + rename). If a file changed between validation
and apply, that fix is safely skipped instead of being spliced onto the
wrong lines.
Example: one verified, one refused
An abbreviated vulkro-sf fix . --ai preview (illustrative):
# [3f9c2a1b] AI fix (advisory) apex_without_sharing (force-app/main/default/classes/AdminTool.cls:1) - verified: finding cleared, no new findings
--- a/force-app/main/default/classes/AdminTool.cls
+++ b/force-app/main/default/classes/AdminTool.cls
@@ -1,4 +1,4 @@
-public without sharing class AdminTool {
+public with sharing class AdminTool {
public void doWork() {
}
}
# [8d41e7c0] AI fix (advisory) apex_soql_injection (force-app/main/default/classes/LeadSearch.cls:12) - refused: the deterministic Apex detector still reports the finding after applying the fix; the model's change did not resolve it. Refusing.
The first proposal converted the sharing declaration; the re-scan
confirmed the finding is gone and nothing new appeared, so it is
eligible for --write. The second proposal did not actually remove the
injection, so the re-scan still flags it and the diff is refused. Both
verdicts come from the deterministic detector, not from the model.
JSON output
--format json emits a stable envelope holding only the fixable
findings:
{
"schema_version": 1,
"generated_by": "vulkro-sf 0.9.0",
"count": 2,
"findings": [ ... ]
}
Under --ai, each finding that received a proposal gains an additive
ai_fix object: the unified diff, the checker_signal, a
validated_by_recheck boolean, a validation block
(finding_cleared, new_findings, rechecked_scope), a
refused_reason on refusal, and the permanent labels advisory,
non_deterministic, and excluded_from_benchmark, so a consumer can
never mistake an AI suggestion for a deterministic result. Without
--ai the JSON is byte-identical to the deterministic output.
Model selection
The model resolves in this order (highest priority first):
- Per-run flags:
--ai-model,--ai-url,--no-ai. - Environment variables:
VULKRO_AI_MODEL,VULKRO_AI_URL,VULKRO_AI_KEY(the key applies to cloud endpoints only and is never persisted). - The saved selection from
vulkro-sf ai use. - The smart default:
qwen2.5-coder:7b(Apache-2.0) on a local Ollama runtime athttp://127.0.0.1:11434/v1, falling back tophi4-mini:3.8b(MIT) on a constrained machine. The default never auto-selectsqwen2.5-coder:3b, whose license is non-commercial.
Run vulkro-sf ai setup once to configure the runtime and model; the
configuration is shared with the core vulkro CLI, so a model set up
with either binary serves both. Under VULKRO_OFFLINE, a loopback
model keeps working (nothing leaves the machine) while any cloud url is
refused. If the AI layer is off or the runtime is unreachable, fix
prints an actionable note and completes with the deterministic report
unaffected.
Exit codes
0- scan completed, no findings.1- scan completed, findings were reported.2- error: bad arguments, IO failure, or internal crash. The error message names the cause and the next step.
The exit code comes from the deterministic scan alone. A verified, refused, or applied AI fix never changes it.
Where to go next
- vulkro-sf scan - the deterministic scan this command builds on.
- vulkro-sf presubmit - the AppExchange pre-submission gate to run after fixes land.