DEV Community

CopperSunDev
CopperSunDev

Posted on • Originally published at coppersun.dev

Ruff Won't Catch Security Bugs Unless You Ask It To

Ruff replaced flake8, isort, pyupgrade, and half a dozen other tools in most Python projects. It's fast, easy to configure, and covers an enormous surface area. What it doesn't cover by default: security.

Run ruff check . on a fresh project and you get style checks, import ordering, unused variables, and a set of correctness rules. You don't get SQL injection detection. You don't get hardcoded secret scanning. You don't get command injection checks. Those rules exist in Ruff — they're just not on unless you turn them on.

What Ruff's Default Ruleset Covers

BrassCoders names Ruff's default scope precisely because the gap is easy to miss. Ruff's default enabled rules include E (pycodestyle errors), W (pycodestyle warnings), and F (Pyflakes) — style enforcement, import cleanup, and correctness checks like undefined names and unused imports. These are the rules that catch a misspelled variable name, an f-string that references a nonexistent variable, or a wildcard import that pollutes the namespace.

Security rules live under a different prefix. Ruff's S ruleset is a port of flake8-bandit, which is itself derived from Bandit — the Python security linter from PyCQA. S608 checks for SQL injection via string formatting. S603 checks for subprocess calls without shell-injection protection. S506 checks for yaml.load without a safe loader. None of these are in Ruff's default select. A developer who installed Ruff to modernize their linting stack and left the configuration at defaults has zero security coverage from Ruff, regardless of how many other rules pass.

The Ruff documentation lists every rule and its default status. The S-prefix rules are all opt-in.

How to Enable Security Checks in Ruff

BrassCoders bundles Bandit's full ruleset internally — if you want Bandit-equivalent coverage inside your existing Ruff pipeline, you can enable it with a two-line config change. In ruff.toml:

[lint]
select = ["E", "F", "W", "S"]
Enter fullscreen mode Exit fullscreen mode

Or on the command line: ruff check --select S .

The S ruleset covers the most common security anti-patterns: SQL injection, command injection, unsafe deserialization, hardcoded secrets, weak cryptography, and a handful of others that Bandit has been catching for years. If you're running Ruff without S rules and you have Python security concerns, enabling them is the right first step.

One caveat worth naming: Ruff's S rules are a subset of Bandit's full ruleset. Bandit standalone runs a broader set of checks and gets updated as new patterns emerge. For most codebases, Ruff's S coverage is sufficient for the common cases. For teams who want Bandit's full coverage, BrassCoders bundles Bandit as one of its 12 scanners — running brasscoders scan replaces the separate Bandit invocation and adds the additional scanners on top.

What Even --select S Won't Find

Here's where Ruff and Bandit share a gap. In BrassCoders's published benchmark against 12 AI-generated Python files with planted bugs, Bandit caught 6 of 12. The 6 it caught were all security bugs: SQL injection, command injection, unsafe deserialization, hardcoded credentials. The 6 it missed included 4 performance anti-patterns and 2 correctness bugs.

The performance anti-patterns are the signal. AI coding assistants produce a specific set of performance bugs that don't appear often in human-written code:

  • O(N²) string concatenation — building a string inside a loop with += instead of join()
  • list.insert(0, item) for prepending — O(N) per call, should be collections.deque
  • Nested loops used as joins on large datasets — where a set lookup or dictionary would run in O(1)
  • Unbounded polling intervals — while True: check_something() with no sleep or backoff

These patterns appear in AI-generated code because the model optimizes for the happy path. The code works on small inputs. It satisfies the prompt. The performance implication only surfaces at scale, and the model doesn't model scale unless the prompt asks for it explicitly.

Neither Ruff nor Bandit has rules for these patterns. They weren't a meaningful failure mode in human-written code. BrassCoders caught 11 of 12 in the same benchmark — the 5 additional bugs beyond Bandit's 6 included all 4 AI-coder performance anti-patterns and one phantom import.

The phantom import is its own category. AI coding assistants occasionally reference libraries that don't exist, or reference real library names with invented APIs. A linter that checks only code style can't detect a package that's hallucinated — it would need to verify the import against a registry or a known package list. BrassCoders's AI-pattern scanner does this. Ruff's F401 (unused import) catches a different case: a package that's imported but never used in the file. That's not the same as a package that's used in the file but doesn't exist on PyPI.

The Full Stack: Ruff, BrassCoders, and Where They Divide

The right mental model: Ruff and BrassCoders cover different layers of the same codebase.

Ruff runs in your editor and pre-commit hook. It's synchronous and near-instant. Its job is to enforce style, catch trivial errors before they reach review, and keep imports clean. Enable the S ruleset and it adds Bandit-equivalent security checks to that same fast pass.

BrassCoders runs in CI, after the commit. It takes 20–60 seconds on a typical project. It runs 12 scanners — including Bandit's full ruleset — and applies the additional AI-pattern, performance, and secret-format detectors on top. Its output lands in .brass/ai_instructions.yaml, where Claude Code or Cursor picks it up and triages each finding.

They're not competing for the same slot. Ruff's fast feedback loop catches the obvious stuff while you're writing it. BrassCoders's CI gate catches what slips through — the performance anti-pattern that only shows under load, the phantom import that passes a style check, the hardcoded token that an AI assistant added as a placeholder and you forgot to replace.

Both belong in the pipeline. Neither replaces the other.

pip install brasscoders
brasscoders scan .
Enter fullscreen mode Exit fullscreen mode

The scan runs offline by default. Nothing leaves your machine. The AI-pattern rules, Bandit integration, and secret detectors are all in the free OSS core — no subscription needed to close the gap Ruff leaves open.

If you're already running Ruff, add select = ["S"] today and add brasscoders scan to your CI pipeline. The setup takes about 30 minutes end to end. The benchmark data on what each tool covers — and what each misses — is reproducible against the published corpus.

Top comments (0)