The fix design contract¶
compose-lint fix auto-remediates the findings whose edit is mechanically
unambiguous — one correct value, in one place, with no collateral change to
the rest of the file. The guarantee is about the edit, not the outcome:
an auto-fix can still change how the stack behaves, which is why every such
edit is labelled ⚠ behavior-changing with the specific breakage named. This
page is the full contract behind the README's summary.
- Dry-run by default;
--applywrites in place via an atomic swap that preserves the file's read, write and execute bits — an interrupted write never corrupts the Compose file. The setuid, setgid and sticky bits are deliberately dropped: a Compose file has no use for them, and the swap writes a new inode, which would otherwise be handed those bits. - Only mechanically unambiguous fixes are applied. Findings whose remediation is context-dependent (e.g. CL-0006 capability lists, CL-0001 socket mounts) are never auto-edited — the tool refuses rather than guess at a value only you can choose. Each one is listed on stderr with its line, rule and the fix to make by hand, so a refused finding still comes with its remedy.
- Behavior-changing edits are labelled, not withheld. Every edit that alters
runtime behavior emits a
⚠ behavior-changingline naming what breaks, on the dry-run and on--apply, so the warning reaches you on whichever path you took. The label is the mitigation; there is no severity gate that quietly drops risky fixes. - Suppressed findings are never touched —
.compose-lint.ymldisables and per-service excludes are honored. - Refuses rather than risk a wrong rewrite. Files using YAML anchors, merge keys, or
${VAR}interpolation in the affected region are skipped rather than risk a wrong rewrite — including aports:ortmpfs:list shared between services through an anchor, which one edit would change for all of them, and every apply is re-parsed and re-linted before it is written — anything that wouldn't round-trip clean is refused with the diff surfaced for diagnosis. Where the refusal is the safety property rather than a shape the fixer does not handle, stderr says why, one line per finding: a list shared through an anchor, a deletion whose value comes from a${...}default, and a long-syntax port whose first value continues onto the next line. - A key deleted with
!resetis deferred, not written back.!resetremoves the key from the configuration Compose runs, so an absence rule fires on it — but writing the key into a document the reset applies to changes nothing there. That finding is listed to fix by hand, with a note naming the key and the file the!resetis written in, and every other fix in the file still applies. The refusal is the finding's, not the file's. - Diff is data, status is human. The diff goes to stdout; progress and
counts go to stderr, so
compose-lint fix file.yml > changes.diffcaptures the diff without the summary. The⚠ behavior-changinglines are the exception: on a dry run they lead the stdout block, above the diff, so a redirect cannot silently drop the one warning that matters. Under--applythere is no diff and they go to stderr with the status. - The dry-run diff is for review, not a patch. A line that a CI runner
could read as a workflow command (
##[, or::at the start of a line) is escaped before it is printed, so the output is not promised togit apply. Usefix --applyto change the files, andgit diffafterwards if you want a patch.
SARIF suggested changes¶
Structured fixes also ride in SARIF output: compose-lint check --format sarif
populates fixes[].artifactChanges, which GitHub Code Scanning renders as an
inline suggested change on the pull request. Region columns count UTF-16 code
units, the unit JavaScript consumers such as Code Scanning and the VS Code
SARIF viewer index by, and the run declares it with
"columnKind": "utf16CodeUnits". A character outside the Basic Multilingual
Plane, such as an emoji, is two columns.