Skip to main content

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

FlagTypeDefaultDescription
--aibooloffTurn on the AI fix tier. On its own it is a read-only preview: validated diffs are printed, no file is modified.
--writebooloffApply 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-runbooloffMake the default read-only preview explicit: a note confirms no file will be modified. Conflicts with --write.
--ai-model <MODEL>stringresolvedModel tag override (highest precedence). Overrides VULKRO_AI_MODEL, the saved vulkro-sf ai use selection, and the smart default.
--ai-url <URL>stringresolvedAI runtime base url override (OpenAI-compatible). A loopback url is allowed under VULKRO_OFFLINE; a cloud url is refused offline.
--no-aibooloffDisable the AI layer for this run: only the fixable candidate count is reported.
--format <fmt>enumtabletable (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 classDetectorRemediation idiom the model is asked for
CRUD / FLS enforcementapex_crud_flsAdd WITH SECURITY_ENFORCED or WITH USER_MODE to the query, or use user-mode DML / Security.stripInaccessible before the write.
Sharing declarationapex_without_sharingConvert without sharing to with sharing (or inherited sharing), changing only the class declaration keyword.
SOQL injectionapex_soql_injectionReplace 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:

  1. 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.
  2. 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.
  3. 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):

  1. Per-run flags: --ai-model, --ai-url, --no-ai.
  2. Environment variables: VULKRO_AI_MODEL, VULKRO_AI_URL, VULKRO_AI_KEY (the key applies to cloud endpoints only and is never persisted).
  3. The saved selection from vulkro-sf ai use.
  4. The smart default: qwen2.5-coder:7b (Apache-2.0) on a local Ollama runtime at http://127.0.0.1:11434/v1, falling back to phi4-mini:3.8b (MIT) on a constrained machine. The default never auto-selects qwen2.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