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. 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.

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.

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
scan .jsonoff--include-json
respect .gitignoreon--no-ignore

On this page