Two Linter Frameworks, One Project File, Both Actually Running

My Emacs config loads two separate linter frameworks in the same file, both fully configured, both actually running: flymake and flycheck. They're not doing the same job for different languages, and neither one is dead configuration left over from a migration that didn't finish. Both are live, and figuring out which one is actually authoritative for a given diagnostic in a given buffer took more digging than I expected going in.

What's actually set up for each

core/project.el configures flymake first, with a custom minor mode wrapped around one of its settings:

  • (use-package flymake
  • :ensure t
  • :custom
  • (flymake-show-diagnostics-at-end-of-line nil)
  • (flymake-indicator-type 'margins)
  • (flymake-margin-indicators-string
  • `((error "!" compilation-error)
  • (warning "?" compilation-warning)
  • (note "i" compilation-info)))
  • :init
  • (define-minor-mode my/diagnostic-at-eol
  • "Minor mode to show flymake diagnostic at eol."
  • :init-value nil :global nil :lighter nil
  • (if my/diagnostic-at-eol
  • (setq flymake-show-diagnostics-at-end-of-line 'short)
  • (setq flymake-show-diagnostics-at-end-of-line nil))
  • (flymake-mode -1)
  • (flymake-mode 1)))

my/diagnostic-at-eol is a hand-written toggle: flip it on, and Flymake starts showing the full diagnostic text at the end of the line instead of just a margin glyph; flip it off, and it goes back to a bare indicator character in the margin defined a few lines above. The last two lines — disabling and immediately re-enabling flymake-mode — exist because Flymake reads that display setting when the mode starts up, not continuously, so toggling the minor mode is what forces it to notice the variable just changed.

A few lines later, in the same file:

  • (use-package flycheck
  • :ensure t
  • :config (counsel-projectile-mode))
  • (global-flycheck-mode)

That's a real, active, global mode — not a leftover stub. global-flycheck-mode runs unconditionally at load time, turning Flycheck on in every buffer where it has a checker available.

Two systems, not one displaced by the other

Flymake and Flycheck solve the same basic problem — running a syntax or lint checker against the buffer and showing you where it complains — but they're architecturally different enough that having both active isn't automatically redundant. Flymake is built into Emacs core and is what most LSP clients, including lsp-mode in this same config, wire their diagnostics into directly; a language server's errors and warnings generally arrive through Flymake's backend API. Flycheck is a separate package with its own large ecosystem of checker definitions, often for tools or a syntax-checking style Flymake's built-in backends don't cover, and it doesn't automatically consume LSP diagnostics the way Flymake does unless something explicitly bridges them.

Which means, in a buffer where both are active, you can genuinely be looking at output from two independent checking pipelines at once — an LSP server's diagnostics surfacing through Flymake's margin indicators, and Flycheck running its own separate checker on the exact same buffer, potentially finding overlapping or entirely different things, with its own indicator convention layered on top. Neither one silently defers to the other; there's no single "authoritative" answer to which framework a given warning is coming from without checking the indicator style or the message wording against what each one is actually configured to show.

What's actually worth checking, if you inherit a setup like this

The honest answer to "why both" isn't a single clean design decision — it's that they entered the config to cover different needs at different times and neither one made the other worth removing, since each is doing something the other one either can't do at all or doesn't do by default. The custom my/diagnostic-at-eol toggle is real, deliberate work spent specifically on Flymake's presentation, which only makes sense to have written if Flymake's own diagnostics were expected to be the ones a person is actually reading day to day — a reasonable signal, even without a comment saying so outright, of which of the two is meant to be the primary one in practice, with the other filling in wherever the primary doesn't reach.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.