# file:drift

The file:drift collect plugin compares a template against a current file and emits the lines that differ, as a unified diff. Placeholder-shaped substrings are masked out of both sides before comparing, so the same config runs unmodified across every project provisioned from a shared template — no per-project variables map required.

It is the 1.x replacement for the 0.x filediff check. Unlike filediff it does not render the template: see why rendering was rejected.

# Inputs

file:drift is unusual in taking two inputs:

Input Role Accepted formats
input The template (the known-good baseline) raw, string
additional-inputs The current file being checked — exactly one raw, string

Both typically come from file:read or http:fetch.

Exactly one additional input, and its format is checked

Supplying zero or more than one additional-inputs is a collection error. Pointing it at a multi-file fact (e.g. file:lookup, which emits map-bytes) is also rejected rather than silently producing an empty "current" side — which would otherwise breach claiming the whole template had been deleted, the worst failure mode for an audit tool.

# Plugin fields

Field Description Required Default
placeholder-pattern Regex matching placeholders to mask before diffing. No {{\s*(\w+)\s*}}

# Common fields

Field Description Required Default
name The name/identifier of the plugin - this is the yaml key in the config file when defining the fact. Yes -
connection The connection to use for collecting the fact. No ""
input A previous input to use when collecting the fact. No ""
additional-inputs Additional previous inputs to use when collecting the fact. No []

# Return format

list-string — the unified diff lines (3 lines of context), empty when there is no drift. Pair it with the drift analyser, which breaches whenever the list is non-empty.

# Example

collect:
  template:
    http:fetch:
      url: https://raw.githubusercontent.com/client/project-template/main/.github/workflows/ci.yml

  current:
    file:read:
      path: .github/workflows/ci.yml

  ci-drift:
    file:drift:
      input: template
      additional-inputs: [current]

analyse:
  ci-matches-template:
    drift:
      description: CI workflow has not drifted from the project template
      input: ci-drift

Source: examples/file-drift.yml

# Placeholder matching is a wildcard, not a value check

Each placeholder is matched against the current line with a non-greedy (.*?) capture. file:drift therefore confirms that something occupies the placeholder's position — not that the rest of the line is otherwise identical in structure. Consequences worth knowing:

  • A capture can absorb adjacent genuine drift on the same line. Template image: {{ IMG }}:v1 against current image: evil/malware:latest:v1 reports no drift, because the wildcard swallows everything up to the literal :v1. Keep placeholders on their own line, or as an entire value, wherever the drift you care about could plausibly appear.
  • A repeated placeholder is not checked for consistency. RE2 has no backreferences, so a: {{ X }} b: {{ X }} cannot express "these two captures must match" — current a: one b: two reports no drift.
  • An empty substitution counts as substituted, not as drift. Only a placeholder still literally present in the current file — the copy-paste-and-forgot-to-fill-in bug — is flagged as unsubstituted.

These are inherent to wildcard matching without a rendering step, and are the accepted trade-off against a per-project variables map.

# Output rendering

Drift breaches carry a full unified diff via KeyValuesBreach, legible in the pretty, json and junit renderers. The table renderer does not wrap multi-line breach values, so it is unsuitable for drift checks.