secure-code-agent docs
Work orders and remediation

Work orders — the first-class output

Status: v0.13.2 — 2026-10-30. Triage tiers and outcome verification.

The score is second class. It is worth something as a trend and very little as a number, and the thing that actually improves a repository is a bounded, specific instruction an agent can act on and a way to check afterwards that it did.

This page is the whole loop: audit → work order → fix → verify → trend.


1. Audit

secure-code-agent .

Writes two files by default:

file what it is
secure-code-report.md what the audit found, for a person
secure-code-remediation-prompt.md the work order, for an agent

Both are written without a flag. The report describes your code; the work order changes it, and the one that changes it used to be reachable only by passing --prompt-output — so the artifact that grades you was always on and the artifact that helps you was off unless you knew to ask.

Set any outputs.*_path to null to turn that file off. SARIF, JSON and the PR comment stay off until asked, because they are for other systems to read rather than for the person at the terminal.


2. The work order

Three tiers, each with its own instructions, because a flat list gives a command injection and a variable-name match the same billing.

§FIX — patch these

The scanner is confident, and the rule has not been measured producing noise. Each finding carries file and line, the offending snippet, its CWE, OWASP, ASVS and SSDF anchoring, and a concrete suggested fix.

§REVIEW — confirm before changing

Either the scanner reported low confidence, or the rule is on the measured low-precision list. The reason is stated per finding, so the agent is not being told "be careful" without being told why.

A justified suppression is a successful outcome for this tier, not a failure. That is the point of having it: a noisy rule is demoted, never dropped.

Bandit's B105 scored zero useful hits out of 22 across the calibration corpus — EMAIL_HOST_PASSWORD = "", PASSWORD_FIELD = "password", django-insecure- — and still caught a planted DB_PASSWORD = "SuperSecret123!" in a synthetic vulnerable repository. Dropping it would lose the credential. Leaving it unlabelled buries the credential under noise. So it is labelled, and the value is judged as well as the rule: twelve characters with a digit and an uppercase letter promotes a finding back to §FIX. Checked against all 22 corpus values, it promotes none of them.

§ACCEPT — test tree and documentation

Summarised rather than listed, grouped by rule, with a drafted .scignore.yaml entry. The decision here is per rule, not per line.

Listing these individually produced a 541KB, 15,390-line work order when this tool audited itself — past most context windows and useless to a person. The same content is now 111 lines.

§FIX is capped at 12 patch blocks; §REVIEW is one line per finding, capped at 12; the §ACCEPT table lists 8 rules. Quoted code is trimmed to 3 lines. Every cap says what it left out, with a pointer to the JSON report where everything lives — capping is fine, capping silently is the absence-of-evidence failure this whole project exists to prevent.

The caps were 40 per tier, which fixed the 15,390-line number and not the principle: Django still produced 1,954 lines and PyGoat 1,602. The bulk was not the findings but the context around them — Django's twelve §FIX blocks carried 212 lines inside code fences, because scanners return whole-function context and nothing trimmed it. A work order is a prompt someone pastes into an agent, not a backlog; ordinary repositories now land at 85–145 lines.


3. Fix

Hand the work order to any agent. The hard constraints and patch protocol travel with it — see remediation.md.


4. Verify

secure-code-agent . --verify-against secure-code-report.json

Re-audits and compares against the run that produced the order.

outcome meaning
fixed reported before, gone now, and the code actually changed
still open reported before and after
silenced gone, but only because a suppression or an inline marker now covers it
introduced did not exist before this work
deferred test tree and documentation — reported, never required

Exit code is 0 only if the run passes: nothing regressed, and either work was done or there was none to do. A verification step that always passes verifies nothing.

Two distinctions worth stating, because both were wrong at first:

"Improved" and "passed" are different questions. They were one property, and with nothing to fix, nothing was fixed, so a clean repository exited 1 forever — a CI step that could never go green again, which is a good way to teach people to delete the CI step. improved stays strict for reporting; passed is the gate.

Silencing is not fixing. Hard constraint 6 forbids # nosec, # noqa and friends, and forbidding is not detecting: Bandit honours # nosec itself, so the finding stops arriving and simply reads as repaired. Measured on a fixture, two real findings silenced that way reported "improved: 2 fixed" and exited 0. Verification now reads the source back around the reported line and calls it what it is.


5. Trend

Every run appends one line to .secure-code/history.jsonl and prints the movement:

trend: 3.90 (B+) — up 3.86 from 0.04 (F), 3 scored runs

Append-only. A malformed line costs one run rather than the history, and unknown keys from a newer version are ignored so an old reader survives a new writer. Findings are deliberately not stored: they would grow without bound, duplicate the reports, and — since findings quote source — turn the history into a second copy of the repository.

A run whose coverage was too thin to grade records score: null, and the trend skips it rather than plotting a collapse to zero that never happened.


In CI

- name: Audit
  run: secure-code-agent . --json-output report.json --fail-on-gate

# ... agent applies the work order ...

- name: Prove it helped
  run: secure-code-agent . --verify-against report.json

The second step fails the build if the agent silenced findings, introduced new ones, or left the order undone.