This is the second of two posts on AI code slop. The first covered why AI-generated code fails differently from human-written code, and why some traditional quality signals have started pointing the wrong direction. This one is the practical companion: what slop actually looks like, organized by the kind of damage it does.
These families came from surveying 70+ AI code quality tools, selecting 42 for deep rule analysis, and cataloging 575 individual rules. Each family groups patterns by what they break in the codebase. The taxonomy is scoped to source code: process metadata, prose/docs slop, and security guardrails are real surfaces, but they are not top-level code-slop families here. Cross-language leakage is treated as part of Integrity / Fake-Done rather than as a separate family.
I’m including compact examples where they make the pattern clearer.
1. Hygiene Debris
Leftovers from the generation process that should have been cleaned
before commit. Unused imports, dead functions, debug logging,
commented-out code, ephemeral planning files like PLAN.md
and SCRATCH.md.
Models generate artifacts as part of solving a task but don’t track whether those artifacts end up wired into the final solution. There’s no feedback signal when something goes unused, so over many sessions these leftovers accumulate silently. One project accumulated 11,903 lines of dead code in 7 months, including entire orphaned extension subsystems for Telegram, Signal, Matrix, and iMessage.
This is the most statically detectable family. About 33 of 37 rules need no LLM at all: just AST analysis, module graphs, and pattern matching.
2. Annotation Noise
Comments that add no information beyond what the code already says.
# Check for missing, null, or empty type
if exposure_type is None or (isinstance(exposure_type, str) and exposure_type == ""):
...
# Get the allowed set for this type
allowed_fields = {
"Gateway": gateway_allowed,
"DirectPort": directport_allowed,
"Balancer": balancer_allowed,
}.get(exposure_type, set())Both comments restate what the next line does. This isn’t under-documented code. It’s code with extra text because the model writes in explanation mode. Models can’t distinguish contexts where comments add value (non-obvious business logic, API contracts) from contexts where they restate the code.
One team built a compiled Go binary that fires on every file write to intercept narrator comments in real time. Another went through a five-month escalation, eventually concluding that “cleanup” framing enables write-now-delete-later behavior. They reframed to “do not write in the first place.”
3. Redundant Guarding
Runtime guards that duplicate guarantees the type system or call context already provides.
config.get("database", {}).get("host", {}).get("primary", "localhost")When the dict structure is known from the caller, this fallback chain is pure noise. Models produce guards as part of “careful code” without tracking whether the guarantee already exists. Defensive programming was a positive signal in training data, so the model keeps producing it regardless.
Detection requires type checker integration, making this harder to clean automatically than the first two families.
4. Structural Bloat
The largest family at 102 rules. Unnecessary wrappers, premature abstractions, factories that always return the same thing, over-fragmented file structures.
Models produce structure because training data rewards structure, but humans had a natural brake: creating an abstraction costs effort, so you only do it when the payoff exceeds the cost. Models have no production cost. They create a helper for every operation, an interface for every class, a wrapper for every boundary.
The classic example: a single-implementation interface where
AbstractUserRepository has exactly one concrete
UserRepository. Or an async function wrapping a synchronous
call with no internal await. One project’s AGENTS.md captures the right
instinct: “New helpers and files must pay rent immediately.”
5. Duplication by Regeneration
Models don’t copy-paste. When a model needs a helper that already exists, it writes a new one from scratch. The result is functionally identical code that’s syntactically different, which traditional clone detectors miss entirely.
One project explicitly bans this in their AGENTS.md: “NEVER create
local formatAge, formatDuration,
formatElapsedTime functions.” That ban exists because
agents kept creating them. The root cause is simple: humans remember
what they’ve already written. Models don’t. Each invocation starts
fresh.
6. Test Slop
Tests that exist to pass rather than to catch bugs.
def test_calculate_tax():
amount = 100
rate = 0.15
expected = amount * rate # same formula as the implementation
assert calculate_tax(amount, rate) == expectedThis is circular verification: the test recomputes the expected value using the same formula as the production code, so it can never fail. When a test fails, the cheapest path to green is weakening the test rather than fixing the code, and models have no feedback signal that distinguishes these two actions.
Other patterns: zero-assertion tests, tests that mock the unit under
test itself, and @pytest.mark.skip where the agent disabled
the test rather than fixing the underlying failure. One project treats
weakened test expectations as a violation triggering mandatory halt.
7. Integrity and Fake-Done
Agent output that misrepresents the state of the work.
def enforce_rate_limit(user_id: str, action: str) -> bool:
"""Return whether the user is allowed to perform this action."""
# TODO: integrate with Redis-backed limiter
return TrueThis isn’t just an incomplete TODO. The damaging part is the executable success path: callers can now build on a function that claims to enforce a policy but silently permits every request.
This family also includes cross-language leakage:
results = []
items.forEach(lambda item: results.push(normalize(item)))JavaScript Array methods leaked into Python. The code looks like it could work, which is exactly what makes it dangerous.
This is the hardest family to mitigate. Even the deepest prevention system I surveyed, with three converging skills and verification requirements, has documented failures where agents fabricate entire verification dialogues.
8. Error-Swallowing
Mechanisms that suppress or silently discard error signals. Detected by 18 tools, this is one of the strongest convergence signals in the survey.
try:
type_class = getattr(builtins, type_name)
for name in dir(type_class):
...
except Exception:
pass
return attributesModels reproduce the form of error handling without the substance. A catch block that returns a default looks identical to robust error handling in training data, even though at runtime it silently swallows the error. The same checkpoint that produced this had 35 swallowed-exception findings versus 11 in the reference solution.
Detection is heavily static: empty catch bodies, broad exception
types, _ = err patterns. One of the easiest families to
detect deterministically.
9. Type Laxity
Type system escape hatches.
const result = response.data as unknown as UserProfile;The double-cast as unknown as X is a pattern nearly
exclusive to AI generation. In agentic workflows, type errors block the
generation loop. as any or as unknown as X
unblocks immediately, so models that optimize for compilable output
converge on escape hatches. Nine tools detect unsafe type assertions,
making it one of the most agreed-upon patterns.
10. Kinetic Slop
The agent does more than was asked. Scope creep, drive-by edits to unrelated code, bundled refactoring, formatting changes mixed with functional changes.
This is harder to demonstrate with a code example because it’s about diff scope rather than any single pattern. The test is lineage: every modified line must trace to the user’s request. If it doesn’t, revert it. One project tracks net LOC growth as a slop signal. Another enforces “touch only what was asked” at the harness level rather than relying on the model’s self-restraint.
11. Naming Drift
Names that obscure meaning, break project conventions, or signal AI generation.
def process_data(input_data):
result = helper_1(input_data)
temp = helper_2(result)
return tempprocess_data, handle_response,
get_result, do_thing. Models produce names
drawn from the average of training data rather than matching the
conventions of the project. The result is generic names that compile and
work but carry no domain meaning. The opposite tendency also appears:
over-verbose names like
currentUserAuthenticationStatusBoolean where
isAuthenticated would do.
The Common Thread
Every family is a different surface where the same fundamental property shows up: models optimize for plausible output rather than durable code. Comments that look helpful, tests that look thorough, structure that looks professional, error handling that looks robust. The code looks right because the model is optimized to produce text that looks right. Whether it serves the project is a question the model has no mechanism to answer.
These families fall into three tooling modes. Some can often be cleaned deterministically or CI-gated: hygiene debris, many annotation-noise rules, obvious error-swallowing, shallow integrity stubs, type escape hatches, and direct cross-language leakage. Some need candidate generation plus judgment: structural bloat, semantic duplication, redundant guarding, test slop, semantic error-swallowing, and deep integrity violations. And some are better prevented through workflow control: kinetic slop, test weakening, verification gaming, search-before-write failures, and repeated architecture reinvention. A useful system needs all three.