Skip to content

Lint

StageFreight's built-in linters — content hygiene, dependency freshness, secret detection, merge-conflict markers, Unicode/bidi defenses, file size, and more — run as a pipeline gate. They are cache-aware and parallel, and by default scan only what changed.

lint:
  level: changed              # "changed" (delta-only) or "full"
  exclude:
    - "vendor/**"
    - "*.generated.go"
  modules:
    secrets: { enabled: true }
    freshness:
      enabled: true
      cache_ttl: 300      # module options are inline — no `options:` wrapper

--level (or --all for full) overrides lint.level, which overrides the changed default. Individual modules can be toggled in config or with --module / --no-module.

Modules

Content-only modules produce deterministic output and are cached forever by content hash. The freshness and vulnerabilities modules depend on external state (registries, the OSV database) and use TTL-based caching.

Module Default Detects
tabs enabled Tab characters
secrets enabled Committed secrets (gitleaks)
conflicts enabled Unresolved merge-conflict markers
filesize enabled Files over a size threshold (default 500 KB)
linecount disabled Files over a line-count threshold (default 1000)
links disabled Markdown links that don't resolve: internal, anchors, external (see below)
unicode enabled Dangerous Unicode (see below)
yaml enabled YAML syntax errors
lineendings enabled Inconsistent line endings
gofmt enabled Go files not gofmt-formatted (--fix-safe formats them)
freshness enabled Out-of-date dependencies + correlated CVEs (TTL-cached)
vulnerabilities enabled Advisories against lockfiles (TTL-cached; osv is a deprecated alias)

Unicode — supply-chain defense

The unicode module defends against trojan-source attacks, invisible-text obfuscation, and control-byte smuggling. BiDi overrides, zero-width characters, tag characters, confusable whitespace, and invalid UTF-8 are flagged; ASCII control bytes are gated by a path-scoped allowlist.

    unicode:
      detect_control_ascii: true
      allow_control_ascii_in_paths: ["src/output/banner_art.go"]
      allow_control_ascii: [27]        # ESC only

Freshness & vulnerabilities

freshness checks dependency versions against upstream registries and correlates them against the OSV database. vulnerabilities emits one finding per canonical advisory (RuleID = the advisory ID, e.g. GHSA-…/CVE-…), unifying the OSV-API correlation and a per-lockfile osv-scanner pass so the same CVE never surfaces twice — even when an ecosystem splits its manifest and lockfile into separate files (e.g. npm's package.json vs package-lock.json). Vulnerability options are shared with freshness unless placed directly under the vulnerabilities module.

A pinned osv-scanner that fails to resolve hard-fails the gate; an unpinned/unavailable scanner silently skips the osv-scanner leg (the OSV-API leg still runs).

The links module checks that every markdown link resolves, so a moved or renamed page can't silently break the pages that point at it. It is a whole-repo check (a move breaks links in files that did not themselves change), so it always sees the entire repo — even under --level changed, where other modules scan only the delta, and under --baseline, where it is diffed against the whole base tree so a link this change actually broke still blocks while pre-existing link debt does not. It is opt-in: off unless a config enables it, which keeps it off large repos that never ask for it.

Three link classes, each with its own severity (error blocks, warn reports, off skips). Enabling the module is purely local and additive: the two filesystem-only classes default to warn (they report, never break a build), and external defaults to off so turning the module on makes no network call. Escalate a class to error to make it block; opt external in to warn/error when you want reachability checked:

Class Default Checks
internal warn Repo-relative (beside the file) and wiki-root (/a/b, resolved against root as <path>.md) targets exist
anchors warn #section matches a heading (or explicit {#id} / HTML id/name) in the target page
external off http(s) URLs are reachable (opt-in — a network fetch per URL)
    links:
      enabled: true
      internal: error           # error | warn | off — escalate the filesystem-certain class
      # anchors rides the warn default (report, never block); external is off unless set
      root: ""                  # wiki root for absolute links (default: repo root)
      include: ["docs/**"]      # optional scope; default is every markdown file
      exclude: ["**/vendor/**"]
      external_skip: ["https://staging.example/*"]   # URL globs left unchecked when external is on
      external_timeout_ms: 5000

mailto:/tel: and other non-fetchable schemes are left alone, and fenced code blocks are skipped. Internal targets resolve with the usual wiki conventions: a bare path matches <path>.md/.markdown, a directory index, or an existing directory. Findings carry a stable RuleID (unresolved-link / unresolved-anchor / external-link) with the link as the anchor, so --baseline flags only newly-introduced breakage and a repo with existing link debt is not lit up wall-to-wall on day one.

Only internal is fully deterministic — a target file either exists or it doesn't — which is why it is the class worth escalating to error. anchors depends on heading-slug matching (done GitHub-style, which a given renderer may compute differently), so it defaults to warn and should be escalated to error only once validated against the surface that actually renders the docs. external depends on the network, so it stays off until opted in.

Cache TTL contract

Each module declares its caching behavior, which the engine honors uniformly:

CacheTTL() Engine behavior Expiry
> 0 Cache with TTL Expires when now - CachedAt > TTL
== 0 Cache forever No expiry check
< 0 Never cache Skips Get + Put
not implemented Cache forever No expiry check

Reference

lint

Linting configuration. Controls scan mode, module toggles, and per-module options. 9 modules: tabs, secrets, conflicts, filesize, linecount, unicode, yaml, lineendings, freshness.

lint:
  preset: <string>
  level: <string>   # one of: changed, full · required
  cache_dir: <string>   # required
  target_branch: <string>   # required
  exclude: [<string>]   # required
  modules: {}   # required
  provenance:
    generated: [<string>]
    vendored: [<string>]
  remediation:
    trailing_whitespace: false   # default ON under --fix-safe
    final_newline: false   # default ON under --fix-safe
  cache:
    max_age: <string>   # evict entries not hit in this duration (e.g. "7d")
    max_size: <string>   # evict oldest entries when cache exceeds this (e.g. "100MB")
  fail_on: <string>   # FailOn is the DIAGNOSTIC-IMPORTANCE threshold at or above which a lint finding blocks the build…