Skip to content

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; --apply writes 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-changing line 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.yml disables 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 a ports: or tmpfs: 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 !reset is deferred, not written back. !reset removes 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 !reset is 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.diff captures the diff without the summary. The ⚠ behavior-changing lines 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 --apply there 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 to git apply. Use fix --apply to change the files, and git diff afterwards 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.