Straitjacket

Rules

Every built-in rule, what it flags, the file types it runs on, and how to tune or disable it.

The built-in rules are intentionally generic — no framework or single-language assumptions — so the same binary works across all your repos. Everything is on by default; you ratchet down with --skip. (The one exception is no-comments, an opt-in mode.) Each rule only looks at the file types where it makes sense (e.g. color ignores .json, inline-svg only scans component sources).

Run straitjacket --list-rules to see them with their one-line descriptions.

Generic rules

ruleflags
emojiemoji glyphs in code, comments, strings, and Markdown (a reliable LLM tell). Color emoji, VS16-presented glyphs, and flag sequences — but not text symbols like © , arrows, dashes, or the geometric star.
colorhardcoded color literals — hex (#1e1e1e) and CSS color functions (rgb(), rgba(), hsl(), hwb(), lab(), lch(), oklab(), oklch(), color()). Use a theme token / CSS variable instead.
inline-svghand-rolled inline <svg> in component code — extract it into a named, reusable icon.
inline-fontinline font-family literal stacks — a quoted font or a multi-family list (Inter, sans-serif). A token reference (fontFamily: MONO), a CSS variable, or a bare generic (monospace) is fine. Define the font once and reference a token.
motionad-hoc transition / animation / @keyframes — centralize motion so it can be tuned or disabled.
file-sizefiles longer than the line budget (default 1500) — sprawling single files are a common LLM tell. Tune with --max-lines, disable with --max-lines 0 or --skip file-size.
deep-nestinglines indented past the nesting budget (default 6) — deeply nested logic is hard to follow. Depth is read off leading indentation (which is canonical when a formatter is enforced), so it's language-agnostic and needs no parser. Runs on programming-language sources only (not markup/data). Tune with --max-nesting, disable with --max-nesting 0 or --skip deep-nesting, or exempt code that legitimately nests deeper with an allow marker.
slop-proseLLM prose tells in .md/.markdown/.mdx/.html: machine artifacts hard-fail, and a high density of style tells warns or fails. Disable with --skip slop-prose. See How slop-prose works.
duplicationcopy/pasted code across the tree — any clone of ≥ 50 tokens fails; a structure may appear only once. Compiled in (no external tool). Tune with --dup-min-tokens, disable with --skip duplication. See Why duplication is compiled in.
no-commentsopt-in — every comment, in every language it knows (//, /* */, #, --, <!-- -->). See no-comments mode below.

deep-nesting and embedded DSLs

deep-nesting reads depth from leading indentation and deliberately does not tokenize the language — that's what keeps it a single, language-agnostic pass. The trade-off: a multi-line string literal that embeds an indented DSL — YAML or JSON in a Python """…""", an HTML/SQL heredoc, a template literal — is counted as if that indentation were code nesting, so a deep enough block can trip the rule even though the surrounding code is flat.

This is by design, not a bug to detect around: reliably knowing "am I inside a string literal?" needs a per-language parser, which the rule exists to avoid. Suppress the false positive with a marker instead.

For a file that carries a lot of embedded DSL — test fixtures, template modules — a file-scoped marker is cleanest, because it's a real comment outside any string and doesn't touch the literal's contents:

# straitjacket-allow-file:deep-nesting — this module is embedded YAML fixtures

A line-scoped marker also works — but it has to sit on the line the finding points at, which for embedded content is inside the string literal, so it becomes part of that text (usually fine as a DSL comment, but check it doesn't change what the fixture means):

CI = """\
jobs:
  build:
    steps:
      - run: deep | embedded | yaml  # straitjacket-allow:deep-nesting
"""

See Suppression markers for the full syntax.

no-comments mode

Where — you guessed it — no comments are allowed. The no-comments rule flags every comment: line, block, doc comments, pragmas, all of them. The position is the maximalist one: a comment is a place where the code stopped speaking for itself, and LLMs narrate relentlessly (// increment the counter). If it matters, say it in the code; if it's history, say it in the commit message.

It's the one rule that is off by default — the rest of the rule set runs at its max and you ratchet down, but comments are ordinary in most codebases, so this one is a mode you opt into:

straitjacket --no-comments        # the mode, alongside every other rule
straitjacket --only no-comments   # just show me the comments (implies the mode)

or check it into .straitjacket.yaml with no-comments: true.

It knows the comment syntax of the common extensions — C-family (//, /* */), hash languages (#: Python, Ruby, shell, YAML, TOML), SQL (--), CSS (/* */), and HTML/Vue/Svelte (<!-- -->) — and tracks string literals, so a // in a URL or a # inside a quoted value isn't a comment. A block comment reports once, at its opening delimiter.

Two things that look like comments are never flagged:

  • Shebang lines (#!/usr/bin/env bash) — interpreter directives, not commentary.
  • Suppression markers — a comment carrying straitjacket-allow[-file] is the escape hatch at work. That's also what keeps the escape hatch usable for every other rule while the mode is on; a marker that suppresses nothing is still caught by unused-marker.

The scanner is deterministic, not a per-language parser, so an exotic literal (a regex literal, a heredoc) can fool it — suppress with a marker as usual, or grandfather a file with straitjacket-allow-file:no-comments.

React rules

The following rules are React-specific and only ever fire on .tsx/.jsx, so they're inert in non-React repos. They're AST-based, using OXC.

ruleflags
one-componentmore than one React component in a .tsx/.jsx file. Disable with --skip one-component.
effect-in-componenta useEffect defined in a component's body — effects belong in a custom use* hook. A hook may live in the same file, so co-location is fine; only the effect-inside-a-component adjacency is flagged. Anonymous components (memo/forwardRef) count. Disable with --skip effect-in-component.
prop-drillinga pure conduit: a component forwards one of its own props unchanged into a local child component and never uses it otherwise — dead-weight drilling. Uses OXC semantic analysis: a prop the component also reads ({value.x}, a computation, a DOM binding) is fine; only forwarded-but-unused is flagged. Also excluded — modified props, library components (a Mantine <Button> must receive props), function-typed slots (callbacks, by type), and .map params. Lift the value into a store or context. Disable with --skip prop-drilling. See Prop-drilling and drill depth.
store-passthrougha value from a use*Store() hook forwarded unchanged into a child component — the child should read the store directly. Same semantic engine; only bare {value} on a component counts. Disable with --skip store-passthrough.

Severity and exit code

Findings are either error or warning (warnings are tagged (warn) in the output). The process exits 1 when there's any error-level finding — so CI fails — and 0 when the scan is clean or found only warnings. Override with --no-fail to always exit 0.

Defaults at a glance

settingdefaultflag
file-size line budget1500--max-lines
deep-nesting depth budget6--max-nesting
slop-prose density window400 chars--prose-window
duplication minimum clone50 tokens--dup-min-tokens
no-comments modeoff--no-comments
scan .jsonoff--include-json
respect .gitignoreon--no-ignore

On this page