Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to papertowel, the specialized toolkit for cleaning up the “slop” of AI-generated code.

The Problem: The AI Fingerprint

In many software communities, the origin of code has become a proxy for its quality. Despite the fact that LLMs are increasingly integrated into the development workflow, there is a lingering stigma—and even a forensic obsession—with identifying AI-generated code.

The “AI fingerprint” isn’t usually found in the logic, but in the style:

  • Slop Vocabulary: A predictable set of adjectives and verbs (“robust,” “comprehensive,” “leverage,” “streamlined”).
  • Over-Documentation: Comments that describe what the code is doing (which is obvious) rather than why it’s doing it.
  • Structural Uniformity: A level of organizational perfection and boilerplate consistency that rarely occurs in human-written code.
  • The “Perfect” History: A git history where every commit is a perfectly scoped unit of work, devoid of the entropy, distractions, and “fix the fix” cycles that characterize real human development.

Our Philosophy

papertowel exists to decouple code quality from code provenance.

We believe that if the code is correct, tested, and solves the problem, its origin should be incidental. However, we recognize that the “purity police” exist. Instead of fighting a philosophical battle, papertowel provides the technical means to sidestep the critique.

Our goal is to make AI-assisted code stylistically indistinguishable from human-written code. We don’t just change words; we humanize the entire lifecycle of the code, from the source files to the commit history.

How it Works

The project is split into two primary subsystems:

1. The Scrubber

The Scrubber is a static analysis engine. It scans your source code for stylistic fingerprints and transforms them. It’s designed to be pluggable, allowing for new detectors to be added as the “slop vocabulary” evolves.

2. The Wringer

The Wringer is a git history humanizer. Rather than rewriting history (which leaves forensic traces in the git object database), it uses git worktrees to “drip-feed” commits from a private development branch to a public branch. This process is driven by Persona Profiles, which simulate human behavior—including working hours, productivity peaks, and the natural chaos of human commit messages.


Built with the assistance of machines. Obviously.

The Scrubber

The Scrubber is the static analysis and transformation engine of papertowel. Its purpose is to identify “AI fingerprints”—stylistic tells that suggest a piece of code was generated by an LLM—and provide mechanisms to remove or modify them.

Overview

The Scrubber operates on a pluggable architecture. Instead of one giant monolithic parser, it uses a series of independent Detectors. Each detector is responsible for a specific category of “slop.”

The Detection Pipeline

  1. Scanning: The Scrubber traverses the target directory, respecting .papertowelignore files and [exclude] patterns in .papertowel.toml.
  2. Directive Check: Files containing a // papertowel:ignore-file comment are skipped entirely.
  3. Analysis: Each enabled detector runs against the source code, producing a set of Finding objects.
  4. Suppression: Findings on lines preceded by // papertowel:ignore-next-line are removed.
  5. Scoring: Remaining findings are assigned a severity (Low, Medium, High) based on how strongly they correlate with AI generation.
  6. Transformation: When running in scrub mode, the Scrubber applies transformations to the code to resolve the findings.

See Configuration and Ignoring for full details on suppression options.

Pluggable Detectors

papertowel ships with several built-in detectors, each targeting a different aspect of AI style.

Recipe-Based Detection

The newest and most flexible detector is the recipe system. Recipes are TOML files that describe pattern groups (words, phrases, regex), cluster-scoring rules, and optional applies_to/excludes glob gating. Built-in recipes cover slop vocabulary, phrase patterns, and comment boilerplate. You can add your own in .papertowel/recipes/. See Recipes for the full format.

Lexical Analysis

The lexical detector searches for “slop vocabulary.” LLMs have a strong preference for certain words that humans rarely use in a technical context unless they are writing marketing copy. Examples include:

  • Adjectives: robust, comprehensive, streamlined, intuitive, performant, granular.
  • Verbs: utilize, facilitate, leverage, delve.
  • Phrases: “it’s worth noting,” “in order to,” “under the hood.”

Comment Thinning

AI-generated comments often suffer from “stating the obvious.” A human might comment why a complex regex is used; an AI will comment // This function uses a regex to validate the email. The comments detector scores comments based on their redundancy and “cookie-cutter” phrasing.

Structural Fingerprints

The structure detector looks for suspiciously uniform code organization—such as perfectly balanced module layouts or a lack of the “scar tissue” (slight inconsistencies) that typically accumulates as a human refactors code over time.

Metadata and Boilerplate

The metadata detector identifies the “instant project” syndrome: when a repository appears with a perfect CONTRIBUTING.md, CODE_OF_CONDUCT.md, and SECURITY.md all in the very first commit.

Security Vulnerabilities

The security detector flags insecure patterns frequently produced by AI code generation. It includes 15 regex-based rules covering OWASP Top 10 categories: SQL/shell injection, weak cryptography, disabled TLS verification, unsafe deserialization, hardcoded secrets, and more. Each rule carries a per-rule confidence score; see the full rule reference for details. See Security Vulnerabilities for the full rule reference.

Additional Detectors

The following detectors run automatically alongside those described above. They can each be toggled individually in .papertowel.toml under [detectors].

DetectorCategoryWhat it detects
commit_patternHistoryMachine-clean git history: perfectly uniform commit cadence, 100% conventional message format, zero recovery commits (wip, oops, fixup).
testsTestingMissing or shallow test coverage — AI often generates a tests/ directory with one trivial smoke test and nothing else.
workflowWorkflowTemplate-burst GitHub Actions workflows: multiple .github/workflows/*.yml files appearing in the first commit that contain obvious template boilerplate.
promotionPromotionDisproportionate marketing language in README relative to actual codebase size — more sales copy than technical content.
maintenanceMaintenanceHollow repo shape: many code/config files but empty or placeholder docs, indicating a generated scaffold that was never actually used.
name_credibilityCredibilityGeneric or AI-flavoured project names (e.g. ai-tool-app, nextgen-scaffold) combined with repetitive self-promotional usage in the README.
idiom_mismatchStyleLanguage-specific idiom violations — e.g. getter/setter patterns in Rust where idiomatic code would use direct field access or a builder.
promptPrompt LeakageResidual LLM prompt fragments in source files or docs: phrases like “As an AI language model”, “Assistant:”, or “Let’s break this down”.

Using the Scrubber

Scanning

To identify fingerprints without modifying your code, use the scan command:

papertowel scan .

This will output a report of all findings, categorized by severity.

Scrubbing

To automatically fix the identified fingerprints, use the scrub command:

papertowel scrub .

Pro Tip: Always use the --dry-run flag first to see what changes would be made before applying them to your source.

Lexical Analysis

The Lexical detector is the frontline of papertowel. It targets the specific vocabulary that LLMs are statistically predisposed to use.

The Slop Vocabulary

LLMs are trained on vast amounts of documentation and web content, which often contains a specific “corporate-technical” dialect. When this dialect appears in source code or internal comments, it’s a strong signal of AI involvement.

High-Signal Keywords

We maintain a list of keywords that are frequently flagged. When these appear in clusters, the “AI score” for a file increases significantly.

WordWhy it’s flagged
RobustRarely used by humans to describe their own code unless they’re selling it.
ComprehensiveA classic LLM adjective for summaries or utility functions.
LeverageThe quintessential “corporate-speak” replacement for “use.”
UtilizeAlmost always an unnecessary replacement for “use.”
SeamlessMore common in marketing than in actual implementation notes.

Common Phrases

Phrases are even stronger indicators than single words.

  • “It’s worth noting that…”
  • “In order to achieve X, we can…”
  • “This ensures that the system remains…”

How Transformation Works

When the Scrubber is in scrub mode, it doesn’t just delete these words. It attempts to replace them with more “human” alternatives or rephrase the sentence entirely to break the predictable pattern.

For example:

  • AI: // This function provides a robust way to utilize the cache.
  • Humanized: // This handles caching.

By breaking the rhythmic, overly-formal patterns of the LLM, the code becomes indistinguishable from a human’s shorthand.

Comment Thinning

One of the most obvious tells of AI-generated code is the presence of comments that describe the what instead of the why.

The “Obvious” Comment

Humans generally write comments for two reasons:

  1. To explain a non-obvious decision.
  2. To warn future maintainers about a “gotcha.”

AI, however, often generates comments as a way to demonstrate that it understands the task. This results in comments like:

#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
    // This function adds two integers together and returns the result
    a + b
}
}

Scoring Redundancy

The comments detector analyzes the relationship between the comment text and the code it precedes. If the comment is a near-perfect natural language translation of the code’s logic, it is flagged as “High Severity Slop.”

The Thinning Process

When you run papertowel scrub, the comment detector performs “thinning”:

  1. Deletion: Truly redundant comments (like the add example above) are removed entirely.
  2. Simplification: Overly formal descriptions are shortened into human-like shorthand.
  3. Preservation: Comments that contain high-entropy information (like a link to a bug report or a complex mathematical explanation) are preserved.

The result is a codebase that looks like it was written by someone who knows the language well enough that they don’t need to explain every line.

Structural Fingerprints

While lexical and comment analysis look at the text of the code, the structural detector looks at the shape of the project.

LLMs have a very specific way of organizing projects. They tend to produce “perfect” structures:

  • Perfectly balanced folder hierarchies.
  • Every single file having exactly one responsibility.
  • A level of consistency in naming and layout that is almost inhuman.

While this is “good” software engineering in a textbook, in the real world, human code is messier. It has “scar tissue”—remnants of previous iterations, slightly inconsistent naming in old modules, and a layout that evolved organically.

Detecting Uniformity

The structure detector analyzes:

  • File Distribution: Does every module have exactly the same number of files and similar line counts?
  • Boilerplate Consistency: Are the imports and headers identical across 20 different files?
  • The “Day One” Project: Does the project contain a full suite of governance files (CONTRIBUTING.md, SECURITY.md) in the first few commits?

Humanizing Structure

Correcting structural fingerprints is more complex than replacing a word. The Scrubber handles this by:

  1. Introducing Variance: Suggesting small reorganizations to break the “perfect” grid.
  2. Removing Metadata Slop: Identifying and flagging the excessive boilerplate that screams “generated by a project initializer.”

By breaking the mathematical perfection of the LLM’s output, the project feels like it grew organically over time.

Architecture Detection

AI-generated code often lacks coherent architectural patterns. While a human developer would organize code into layers, define abstractions at boundaries, and split responsibilities across modules, AI tends to dump everything into flat files with minimal structure.

The architecture detector analyzes your codebase for these organizational anti-patterns.

What It Detects

ARCH001: Flat Module Structure

Projects with all source files at the same directory level, lacking meaningful subdirectories like domain/, infrastructure/, or services/.

Why it matters: Human developers naturally organize code as it grows. A project with 20+ files all in src/ suggests generation rather than evolution.

ARCH002: Missing Architectural Layers

No recognizable layer directories found in larger projects. The detector looks for patterns from:

  • DDD/Clean Architecture: domain/, application/, infrastructure/, presentation/
  • Hexagonal: ports/, adapters/, core/
  • CQRS: commands/, queries/, handlers/
  • Common conventions: services/, repositories/, models/, cli/, api/

Threshold: Triggered when a project has 16+ source files with no layer directories.

ARCH003: God Files

Files exceeding 800 lines that likely mix multiple responsibilities. AI tends to generate long, monolithic files rather than splitting concerns.

Why 800 lines? Rust files with inline tests commonly reach 400-600 lines legitimately. 800 lines is a reasonable threshold for “this probably does too much.”

ARCH004: Low Trait Ratio

Projects where less than 2% of types are traits. AI-generated code often skips defining abstractions at boundaries, leading to concrete types everywhere with no interfaces.

Why it matters: Traits enable dependency inversion, testability, and clear module boundaries. Their absence suggests “just make it work” generation rather than thoughtful design.

Caveat: CLI tools and scripts may legitimately have few traits. The detector requires at least 5 structs before flagging.

ARCH005: Anemic Domain Models

High ratio of structs with no associated impl blocks. AI tends to generate data-only structs without domain behavior.

Threshold: Flagged when >80% of structs have no methods.

Why it matters: Rich domain models encapsulate both data and behavior. Anemic models push logic into external functions, often a sign of procedural thinking.

Configuration

The architecture detector uses these defaults:

SettingDefaultDescription
min_source_files8Skip analysis for smaller projects
god_file_lines800Lines threshold for god file detection
min_trait_ratio0.02Minimum trait/(trait+struct) ratio
max_anemic_ratio0.80Maximum fraction of structs without methods
min_directory_depth2Minimum nesting depth for non-flat structure

Exclusions

The architecture detector automatically skips:

  • /target/ (build artifacts)
  • /tests/ and *_test.rs (test files)
  • /book/ and /docs/ (documentation)
  • /vendor/ (vendored dependencies)
  • /.git/ and /.coraline/ (tool data)

Use .papertowelignore to exclude additional paths.

Grade Impact

Architecture findings are weighted at 20% of the overall grade, equal to lexical findings. This reflects that code organization is a strong signal of generation vs. authorship.

FindingSeverityConfidence
ARCH001 (flat structure)Medium0.75
ARCH002 (missing layers)Medium0.70
ARCH003 (god file)High0.85
ARCH004 (low traits)Medium0.65
ARCH005 (anemic models)Low0.60

Security Vulnerability Detection

Detects common security vulnerabilities and insecure patterns frequently produced by AI code generation.

The security detector runs regex-based rules covering OWASP Top 10 categories: injection attacks, broken authentication, insecure cryptography, unsafe deserialization, misconfiguration, and more. Each rule targets multiple languages: Rust, Go, Zig, TypeScript/TSX, JavaScript/JSX, Python, and C#.

Architecture

  • 15 rules (SEC001–SEC015) covering high-frequency AI security anti-patterns
  • Regex-based detection with per-rule confidence scores (0.68–0.90)
  • Per-language filtering — most rules target specific extensions; rules with no explicit extensions apply to all supported source languages
  • Compiled once, reused per file — regexes cached in LazyLock for performance (no per-file recompilation)
  • Cross-platform path handling — uses Path::components() instead of string contains for Windows compatibility

What It Detects

SEC001: SQL / Shell Injection

Severity: HIGH | Confidence: 0.80

User input directly interpolated into SQL or shell commands via string concatenation or formatting.

# ❌ AI-generated: uses f-string without parameterized query
cursor.execute(f"SELECT * FROM users WHERE name = {name}")

# ✅ Secure: uses parameterized query
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

Suggestion: Use parameterised queries (prepared statements) for SQL. For shell commands, use an argument list API (std::process::Command, subprocess.run([...])) instead of string interpolation.


SEC002: eval() / exec() with Dynamic Input

Severity: HIGH | Confidence: 0.85

Dynamic code execution via eval() or exec() with a non-literal argument. AI often defaults to eval instead of proper dispatch tables.

# ❌ AI-generated: uses eval with user input
user_code = request.args['code']
result = eval(user_code)

# ✅ Secure: uses a dispatch table or sandboxed interpreter
HANDLERS = {'add': lambda a, b: a + b, 'sub': lambda a, b: a - b}
result = HANDLERS.get(user_code, lambda *_: None)()

Suggestion: Replace with a safe alternative: a dispatch table (dict/match), an AST validator, or a proper plugin API.


SEC003: Hardcoded Secrets

Severity: HIGH | Confidence: 0.75

Hardcoded API key, password, token, or other credential.

# ❌ AI-generated: literal secret in code
api_key = "sk-abc123longkeyvalue"

Suggestion: Load secrets from environment variables or a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault). Rotate any committed credentials immediately.


SEC004: Weak / Broken Cryptography

Severity: HIGH | Confidence: 0.85

Use of deprecated or insecure algorithms: MD5, SHA-1, DES, 3DES, RC4, Blowfish, or ECB mode. AI training data includes many outdated examples.

# ❌ AI-generated: uses MD5 for password hashing
import hashlib
h = hashlib.md5(password).hexdigest()

# ✅ Secure: uses bcrypt / scrypt / Argon2id
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())

Suggestion: Use SHA-256/SHA-3 for hashing, AES-256-GCM or ChaCha20-Poly1305 for encryption, Argon2id/bcrypt/scrypt for password hashing.


SEC005: TLS Verification Disabled

Severity: HIGH | Confidence: 0.90

Certificate verification disabled (InsecureSkipVerify, verify=False, etc.). This is a critical MITM vulnerability; AI disables it to bypass handshake errors during development.

// ❌ AI-generated: disables certificate verification
config := &tls.Config{InsecureSkipVerify: true}

Suggestion: Never disable TLS verification in production. Fix root certificate issues instead (update CA bundle, trust additional CAs, or fix hostname mismatches).


SEC006: JWT Algorithm Confusion / “alg: none”

Severity: HIGH | Confidence: 0.88

JWT with alg: none or algorithm confusion vulnerability. AI copies JWT examples without validating the algorithm.

// ❌ AI-generated: accepts any or no algorithm
const decoded = jwt.decode(token);  // no algorithm check

// ✅ Secure: pins the algorithm and verifies the signature
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });

Suggestion: Always specify and pin the expected algorithm. Reject tokens with alg: none. Use a well-audited library and verify the signature before trusting any claim.


SEC007: dangerouslySetInnerHTML (React XSS)

Severity: HIGH | Confidence: 0.90

React dangerouslySetInnerHTML used with a non-constant value.

// ❌ AI-generated: user input passed directly to dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userContent }} />

// ✅ Secure: sanitise HTML before rendering
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />

Suggestion: Sanitise HTML with DOMPurify before passing to dangerouslySetInnerHTML, or redesign to avoid raw HTML injection entirely.


SEC008: Path Traversal

Severity: HIGH | Confidence: 0.75

User-supplied input used in file path construction without sanitisation or canonicalisation.

#![allow(unused)]
fn main() {
// ❌ AI-generated: uses user input directly in path
let file_path = req.params.filename;
let content = std::fs::read_to_string(&file_path)?;

// ✅ Secure: canonicalise and check bounds
let base = std::path::Path::new("/public");
let resolved = base.join(&filename).canonicalize()?;
if !resolved.starts_with(base.canonicalize()?) {
    return Err("path traversal attempt");
}
}

Suggestion: Canonicalise the resolved path and assert it remains within an allowed base directory. Reject paths containing .. components.


SEC009: Credentials Written to Logs

Severity: MEDIUM | Confidence: 0.70

Possible credential or token written to logs (detected when log, console.log, println, etc. is called alongside keywords like password, token, api_key).

# ❌ AI-generated: logs entire request (may contain tokens)
logger.info(f"Request received: {request.json()}")

# ✅ Secure: logs only safe fields
logger.info(f"Request from user {request.json()['user_id']} to {request.path}")

Suggestion: Redact sensitive fields before logging. Use structured logging with explicit field allow-lists rather than logging entire objects.


SEC010: Debug Mode Enabled in Production

Severity: MEDIUM | Confidence: 0.72

Debug mode or verbose error details enabled (DEBUG=True, app.run(..., debug=True), developer exception pages). AI leaves debug flags on; this leaks stack traces and internal state to clients.

# ❌ AI-generated: debug mode enabled
app.run(host='0.0.0.0', debug=True)

# ✅ Production: debug off, errors suppressed
app.run(host='0.0.0.0', debug=False)

Suggestion: Set debug=False in production. Use generic error messages for HTTP responses; log detailed stack traces server-side only.


SEC011: Non-CSPRNG for Security Values

Severity: HIGH | Confidence: 0.78

Non-cryptographic RNG used for security-sensitive values (tokens, nonces, keys, salts, OTPs). AI defaults to Math.random() or random.random() because they’re simpler.

// ❌ AI-generated: uses weak RNG for token
const token = Math.random().toString(36).slice(2);

// ✅ Secure: uses CSPRNG
const token = crypto.randomBytes(32).toString('hex');

Suggestion: Use a CSPRNG: crypto.randomBytes() in Node.js, secrets module in Python, rand::rngs::OsRng in Rust, crypto/rand in Go, RandomNumberGenerator in C#.


SEC012: Unsafe Deserialization

Severity: HIGH | Confidence: 0.85

Unsafe deserialisation detected (pickle.loads, yaml.load without Loader, BinaryFormatter in C#, Java ObjectInputStream). These are known gadget chains for arbitrary code execution.

# ❌ AI-generated: uses unsafe yaml.load
config = yaml.load(user_provided_yaml)

# ✅ Secure: uses safe_load
config = yaml.safe_load(user_provided_yaml)

Suggestion: For YAML use yaml.safe_load(). Replace Python pickle with JSON or MessagePack for untrusted data. Use allow-lists for C# and Java deserialization.


SEC013: SSRF: Raw User URL Fetched

Severity: HIGH | Confidence: 0.72

A URL sourced from user input is fetched without an allow-list check. AI-generated proxy or webhook handlers frequently forget this step, allowing access to internal services (169.254., 10., 192.168.*, etc.).

// ❌ AI-generated: fetches user-supplied URL without validation
const url = req.query.url;
const resp = await fetch(url);

// ✅ Secure: validates against allow-list
const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];
const parsed = new URL(url);
if (!ALLOWED_HOSTS.includes(parsed.hostname)) throw new Error('forbidden');
const resp = await fetch(url);

Suggestion: Validate the host against a strict allow-list. Block private/link-local ranges (169.254., 10., 172.16-31., 192.168.). Use an HTTP client that does not follow redirects by default.


SEC014: Hardcoded IV / Nonce

Severity: HIGH | Confidence: 0.82

Hardcoded initialisation vector (IV) or nonce. Reusing a static IV with symmetric encryption destroys semantic security; ciphertexts become deterministic.

# ❌ AI-generated: hardcoded IV
cipher = AES.new(key, AES.MODE_CBC, iv=b'0000000000000000')

# ✅ Secure: generates fresh IV for each encryption
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
# prepend iv to ciphertext for decryption

Suggestion: Generate a fresh random IV/nonce for every encryption operation using a CSPRNG, and prepend it to the ciphertext so it can be recovered during decryption.


SEC015: TODO/FIXME in Auth/Authz Code

Severity: MEDIUM | Confidence: 0.68

TODO or FIXME comment inside authentication or authorisation logic. AI frequently stubs out security checks and marks them for later — which in practice means never.

// ❌ AI-generated: security check stubbed with TODO
// TODO: validate the JWT token before proceeding
const user = decodeJWT(token);

Suggestion: Implement the security check now; never ship a TODO inside auth/authz code. If intentional, track it in the issue tracker and add a test that will fail until it is addressed.


Configuration

The security detector runs by default. To disable it, add to .papertowel.toml:

[detectors]
security = false

Language Support

  • Rust (.rs)
  • Go (.go)
  • Zig (.zig)
  • TypeScript/TSX (.ts, .tsx)
  • JavaScript/JSX (.js, .jsx)
  • Python (.py)
  • C# (.cs)

Rules specify target languages; a rule will only match files of the appropriate type.

Performance

  • Regexes are compiled once at startup and cached in LazyLock
  • IO errors are logged at debug level and gracefully handled
  • Non-source files (binaries, lock files, compiled assets) are skipped

Recipes

Recipes are TOML files that define detection patterns and replacement transforms. They are the recommended way to extend papertowel without modifying source code. All built-in detectors ship as recipes; you can add your own to tailor the analysis to a specific codebase or house style.

How Recipes Work

When papertowel scan or papertowel scrub runs, the recipe engine loads every available recipe and runs the enabled patterns against each scanned file. Findings from recipes appear in the same report as findings from structural detectors.

The recipe engine:

  1. Loads built-in recipes (embedded in the binary).
  2. Loads user-global recipes from ~/.config/papertowel/recipes/*.toml.
  3. Loads repo-local recipes from .papertowel/recipes/*.toml.
  4. For each loaded recipe, runs enabled word, phrase, regex, and contextual pattern groups.
  5. Applies cluster-scoring: if cluster_threshold words appear within cluster_range_lines, severity is boosted.
  6. Applies applies_to / excludes glob gating so patterns can target specific file types.

Files larger than 2 MiB are skipped by the recipe scanner to avoid I/O waste on binary files.

Recipe TOML Format

[recipe]
name        = "my-recipe"
version     = "1.0.0"
description = "Detects domain-specific slop vocabulary"
author      = "your-name"
category    = "Lexical"          # Lexical | Comment | Structural | Readme | Metadata | Custom
default_severity = "Low"         # Low | Medium | High

[scoring]
cluster_threshold      = 4       # how many hits trigger a severity boost
cluster_range_lines    = 15      # the rolling window (in lines) for clustering
cluster_severity_boost = "High"  # severity assigned to clustered findings
base_confidence        = 0.6     # confidence score injected into each finding (0.0–1.0)

[patterns.words]
enabled        = true
case_sensitive = false
whole_word     = true            # treats _ as word char; won't match inside snake_case
severity       = "Low"
items = [
    { word = "leverage",    replacement = "use"    },
    { word = "utilize",     replacement = "use"    },
    { word = "seamless",    replacement = "smooth" },
    # omit replacement to flag without auto-fixing:
    { word = "delve" },
]

[patterns.phrases]
enabled  = true
severity = "Medium"
items = [
    { phrase = "it's worth noting",  replacement = "" },
    { phrase = "in order to",        replacement = "to" },
    { phrase = "as mentioned above", replacement = "" },
]

[patterns.regex]
enabled  = true
severity = "Medium"
items = [
    # Flag function-level comment blocks that start with "This function ..."
    { pattern = r"//\s*This function \w+", replacement = "" },
    # Limit to Rust source files only
    { pattern = r"//\s*Helper to \w+",     replacement = "", applies_to = ["*.rs"] },
]

applies_to and excludes

Every item in any pattern group can carry applies_to and excludes fields, each accepting a list of glob patterns matched against the file path:

{ word = "robust", replacement = "sturdy", applies_to = ["*.md", "*.txt"] }
{ phrase = "out of the box", excludes = ["tests/**"] }

Patterns with both fields: applies_to must match and excludes must not match for the pattern to run.

Built-in Recipes

NameCategoryWhat it targets
slop-vocabularyLexicalWord-level overused adjectives, verbs, and transitions
phrase-patternsLexicalMulti-word phrases that read as LLM filler
comment-patternsCommentDoc-comment openers that follow LLM boilerplate templates

List them with:

papertowel recipe list

Inspect a specific recipe:

papertowel recipe show slop-vocabulary

Adding Custom Recipes

Repo-local

Place a TOML file at .papertowel/recipes/my-recipe.toml. It will be loaded automatically whenever papertowel is run from inside that repository. Commit it alongside your code so the team shares the same detection rules.

User-global

Place a TOML file at ~/.config/papertowel/recipes/my-recipe.toml. It applies to every repository on your machine.

Validating a Recipe

Before adding a recipe to your workflow, validate its syntax:

papertowel recipe validate .papertowel/recipes/my-recipe.toml

This checks structure and required fields without scanning any code.

Disabling Built-in Recipes

If a built-in recipe produces too many false positives for your project, exclude the files it trips on using .papertowelignore, or suppress individual lines with // papertowel:ignore-next-line. There is no per-recipe disable flag at this time.

CI Integration

Recipes are validated as part of the built-in CI workflow if you use the GitHub Actions pipeline from papertowel’s own .github/workflows/ci.yml. The recipes job runs papertowel recipe validate against every TOML in src/recipes/ on every push.

The Wringer

The Wringer is papertowel’s system for git history humanization. While the Scrubber cleans the code, the Wringer cleans the provenance.

Overview

A perfectly clean git history—where every commit is a logical unit of work, devoid of mistakes, and applied at a steady cadence—is a strong signal of AI generation (or an obsessively clean rebase, which is similarly suspicious).

The Wringer replaces “perfect” history with “human” history.

The Core Mechanism: Worktree Drip-Feeding

Most history-humanizers attempt to rewrite the existing git log using filter-branch or rebase. This is dangerous and leaves forensic traces in the git object database.

The Wringer takes a different approach. It uses git worktrees to maintain a parallel “public” version of your project.

  1. Private Branch: You continue developing on your private branch, committing at “machine speed” with whatever messages you like.
  2. Public Worktree: The Wringer creates a separate worktree on a public branch.
  3. The Queue: It analyzes the delta between your private and public branches and builds a Replay Plan.
  4. The Drip: Instead of applying all changes at once, the Wringer “drips” commits into the public worktree over time, based on a Persona Profile.

The Replay Plan

When you run papertowel wring queue, the tool doesn’t just copy commits. It analyzes them to create a realistic human flow:

Session Grouping

Humans don’t commit every 5 minutes for 24 hours. They work in bursts. The Wringer groups commits into “sessions” based on temporal proximity.

Intelligent Squashing and Splitting

  • Squashing: Small, related commits (e.g., fixing a typo in the same file) are squashed into a single “human” commit.
  • Splitting: Massive commits that touch unrelated parts of the codebase are flagged as candidates for splitting, simulating the process of a human breaking down a large task.

Target Scheduling

Each commit in the queue is assigned a target_time. This time is calculated based on the Persona’s active hours and productivity peaks, ensuring that the public history looks like it was written by someone with a life, a timezone, and a sleep schedule.

Using the Wringer

Setup

Initialize the public worktree:

papertowel wring init --branch public

Planning

Analyze your dev branch and build the replay queue:

papertowel wring queue --from dev

Execution

Start the drip-feed process. You can run it as a one-off or as a daemon that applies commits as their target_time arrives:

papertowel wring drip --daemon --profile night-owl

Status

Check the current progress of the drip-feed:

papertowel wring status

Lock Recovery

Safely remove a stale drip lock file on demand (it will not remove an active lock):

papertowel wring unlock-stale

The Public Branch Strategy

The fundamental design goal of the Wringer is to avoid the “forensic footprint” of git history rewriting.

Why not just rebase?

When you rewrite history using git rebase or git filter-branch, you are creating new commit objects. While this looks clean on the surface, it creates several problems:

  1. Reflog Traces: Local reflogs still contain the original commits.
  2. Object Database: In some environments, “orphaned” commits can be recovered from the git object database for a period of time.
  3. Push Friction: You have to force-push the entire history, which is a red flag in any shared repository.

The Worktree Solution

The Wringer avoids all of this by treating the public branch as a separate entity.

The Workflow

  1. Development: You work on main or dev. You commit often, you make mistakes, you use wip messages. This is your “True History.”
  2. The Mirror: The Wringer manages a second branch (e.g., public). This branch is purely for consumption by others.
  3. The Transfer: The Wringer cherry-picks logic from the private branch to the public branch.

Because the public branch is built from the ground up, its history is “native.” There are no rewritten commits, only real commits applied at specific times.

Forensic Indistinguishability

By combining this strategy with Persona Profiles and Archaeology, the Wringer ensures that the public branch is not just a cleaned-up version of the private one, but a plausible alternative history.

To an outside observer (or a forensic tool), the public branch looks like the only history that ever existed: a series of organic, human-paced updates.

The Drip-Feed Mechanism

The “Drip” is the execution engine of the Wringer. It transforms a static Replay Plan into a living git history.

How the Drip Works

The wring drip command is essentially a scheduler. It monitors the QueuePlan (stored in .papertowel/queue.json) and compares the target_time of pending entries against the current wall-clock time.

The Tick Cycle

When the Wringer “ticks” (either once or as a daemon):

  1. It loads the current queue.
  2. It identifies all entries where target_time <= now.
  3. For each eligible entry:
    • It optionally injects Archaeological artifacts (see Archaeology).
    • It cherry-picks the source commits from the private branch.
    • It creates a new commit on the public branch with the persona-driven message and the exact target_time as the timestamp.
    • It marks the entry as completed.
  4. It persists the updated queue.

Humanizing the Commits

The Drip doesn’t just move code; it translates the intent of the commits into a human style.

Message Humanization

Using the messages subsystem, the Wringer transforms the original commit messages. Depending on the persona, it might:

  • Convert a “feat: add login” into a “Conventional” commit: feat(auth): implement login logic.
  • Convert it into a “Lazy” commit: fix stuff.
  • Inject “Entropy”: Add typos, mild profanity, or ASCII emojis (:)) based on the persona’s specific rates.

Temporal Jitter

To avoid the “robotic” feel of commits appearing at exactly 10:00, 10:15, and 10:30, the Wringer applies Jitter. It varies the intervals between commits within a session, simulating the natural ebb and flow of human productivity.

Operational Modes

Single-Shot

Running papertowel wring drip without the --daemon flag applies all currently due commits and then exits. This is useful for manual synchronization.

Daemon Mode

Running with --daemon puts the Wringer into the background. It will wake up periodically, check the schedule, and “drip” commits exactly when they are supposed to appear, providing a real-time humanization of your project’s growth.

Configuring Personas

The behavior of the Wringer is governed by Persona Profiles. A profile is a TOML configuration that defines the “human” identity of the project’s author.

The Persona Profile

A profile determines not just when code is committed, but how those commits are presented.

Schedule Configuration

The [persona.schedule] section defines the working rhythm of the author:

  • active_hours: A list of time windows (e.g., ["09:00-17:00", "21:00-01:00"]) when the author is active.
  • peak_productivity: The window where commits are most frequent.
  • avg_commits_per_session: How many commits typically occur in a single work session.
  • session_variance: The amount of randomness applied to session lengths and commit intervals.

Message Style

The [persona.messages] section defines the commit message aesthetic:

  • style: conventional: Uses the Conventional Commits specification (feat(scope): description). lazy: Uses shorthand, lowercase, and vague messages (wip, fix stuff). mixed: A probabilistic blend of both.
  • wip_frequency: How often a commit is replaced by a “Work In Progress” message.
  • typo_rate: The probability that a commit message contains a simulated typo.
  • profanity_frequency: The probability of injecting mild frustration (ugh, damn).
  • emoji_rate: The probability of adding an ASCII emoji (:), :/).

Built-in Profiles

papertowel ships with two standard profiles:

  1. night-owl: High activity in the late evening and early morning, mixed commit styles, and a higher propensity for “lazy” messages and typos.
  2. nine-to-five: Strict daytime activity, highly conventional commit styles, and very low entropy.

Creating Custom Profiles

You can create your own persona using the interactive builder:

papertowel profile create <name>

Custom profiles are stored in ~/.config/papertowel/profiles/*.toml.

Managing Profiles

  • List all profiles: papertowel profile list
  • Inspect a profile: papertowel profile show <name>

Archaeology and Entropy

If the Wringer only cherry-picked clean commits, the history would still look “too perfect.” Real human development is iterative, messy, and often involves a series of failed attempts before the final solution is reached.

Archaeology is the process of injecting synthetic “messy middle” artifacts into the git history to simulate this evolution.

The Concept: Net-Zero Injections

The core challenge of Archaeology is to add “human” noise without actually changing the final state of the code. If you inject a TODO and never remove it, the code is now different.

The Wringer solves this by using Injection Pairs. Every archaeological artifact is added in one commit and removed in another.

The TODO Cycle

The Wringer randomly selects a .rs file and injects a plausible TODO comment:

  1. Commit A: Adds // TODO: review error handling here to src/lib.rs.
  2. Commit B: Removes the comment.

The net effect on the working tree is zero, but the git log now shows a human developer thinking through the problem and then resolving it.

The Dead-Code Cycle

Similarly, the Wringer can simulate “scratch work”:

  1. Commit A: Adds a commented-out eprintln! or a temporary variable used for debugging.
  2. Commit B: Removes the “dead code” before the final “clean” commit is applied.

Configuration

Archaeology is controlled via the [persona.archaeology] section of the persona profile:

  • todo_inject_rate: Probability of a TODO pair being injected before a real commit.
  • dead_code_rate: Probability of a dead-code pair being injected.
  • rename_chains: When enabled, the Wringer can simulate a series of renames for a single function across several commits, simulating the evolution of an API.

Why This Matters

To a forensic analyst, the presence of “resolved” TODOs and “cleaned up” debug statements is a powerful signal of human authorship. It demonstrates a process of iteration and refinement that LLMs (which typically output the final version of a function in one go) cannot naturally replicate.

Style Baselines

One of the most challenging aspects of humanizing code is that “human style” varies wildly between individuals and teams. A “night-owl” profile might be perfect for one developer, but look suspicious for another.

The Learning Mode

To solve this, papertowel includes a Learn Mode. Instead of relying on a pre-defined persona, Learn Mode allows the tool to analyze your actual existing git history to create a custom style baseline.

How it Works

When you run papertowel learn repo <path>, the tool performs a deep analysis of your recent commits:

  1. Temporal Analysis: It maps out your actual active hours, productivity peaks, and session gaps.
  2. Lexical Analysis: It identifies the words and phrases you actually use in your commit messages.
  3. Entropy Analysis: It calculates your natural typo rate and your frequency of “wip” or “fix” commits.

Creating a Baseline

The resulting analysis is stored as a Style Baseline. This baseline acts as a highly accurate Persona Profile that mirrors your own coding habits.

When you then run wring drip, the Wringer uses this baseline instead of a generic profile, ensuring that the “humanized” history is a perfect stylistic match for your actual development pattern.

Using the Baseline

Once a baseline is generated, apply it when dripping commits:

papertowel wring drip --profile <your-baseline-name>

To inspect the stored baseline without re-running the analysis:

papertowel learn show .

By learning from your own history, papertowel moves from “simulating a human” to “simulating you.”

MCP Tooling Setup

papertowel provides a Model Context Protocol (MCP) server that allows AI assistants (like Claude Desktop) to interact directly with your codebase’s AI fingerprints.

The papertowel-mcp Server

The MCP server exposes the core functionality of the Scrubber as a set of tools that the AI can call. This allows the AI to “self-diagnose” its own fingerprints and suggest fixes.

Available Tools

ToolDescription
papertowel_scanScans a directory for AI fingerprints and returns a structured report of findings.
papertowel_scrubApplies fixes to the detected fingerprints in a target directory.
papertowel_gradeGrades a file or directory from A+ to F for overall AI fingerprint presence.

Installation

1. Build the Server

First, build the MCP server binary:

cargo build --release -p papertowel-mcp

2. Configure Claude Desktop

Add the server to your claude_desktop_config.json (usually located at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "papertowel": {
      "type": "stdio",
      "command": "papertowel-mcp",
      "args": [],
      "env": {
        "RUST_LOG": "info"
      }
    }
  }
}

Usage in Chat

Once configured, you can simply ask Claude to clean up your code:

  • “Scan my current directory for AI fingerprints and tell me what you find.”
  • “Run the papertowel scrubber on the src/ directory to remove any obvious slop.”
  • “Grade this repo and explain which categories contributed most to the score.”

The AI will call papertowel_scan, papertowel_scrub, and papertowel_grade as needed, receive the results, and report back to you.

Git Hooks

papertowel can install a pre-commit hook that scans staged files before each commit. If AI fingerprints at medium severity or above are detected, the commit is blocked.

Installing the hook

papertowel hook install

This writes a shell script to .git/hooks/pre-commit. If a hook already exists (e.g. from another tool), papertowel will refuse to overwrite it unless you pass --force.

How it works

When you run git commit, the hook:

  1. Collects the list of staged files (excluding deletions).
  2. Extracts the staged version of each file into a temporary directory — this ensures the scan sees exactly what’s being committed, not your working-tree edits.
  3. Runs papertowel scan <tmpdir> --fail-on medium.
  4. If any findings at medium severity or above are found, the commit is aborted with the scan output.
  5. The temporary directory is cleaned up on exit regardless of outcome.

Checking hook status

papertowel hook status

Reports whether the hook is installed and whether it was created by papertowel.

Removing the hook

papertowel hook uninstall

This only removes hooks that papertowel installed. If the hook was placed by another tool, papertowel will refuse to touch it.

Working with other hooks

If you use a hook manager like lefthook or husky, you can call papertowel directly from your hook config instead of using hook install. For example, in a lefthook config:

pre-commit:
  commands:
    papertowel:
      run: papertowel scan {staged_files} --fail-on medium

Bypassing the hook

If you need to commit despite findings (e.g. you’re committing test fixtures that intentionally contain slop vocabulary), use git’s built-in bypass:

git commit --no-verify

Configuration and Ignoring Files

papertowel provides several layers of control over which files are analysed and which findings are reported.

.papertowel.toml

Drop a .papertowel.toml in your repository root to configure detectors, severity thresholds, and path exclusions. Every section is optional — missing sections use sensible defaults.

[detectors]
lexical = true
comments = true
structure = true
readme = true
metadata = true
commit_pattern = true
tests = true
workflow = true
maintenance = true
promotion = true
name_credibility = true
idiom_mismatch = true
prompt = true
security = true

[severity]
minimum = "medium"   # "low", "medium", or "high"

[scrubber]
aggression = "moderate"  # "gentle", "moderate", or "aggressive"

[exclude]
paths = [
    "vendor/",
    "generated/**/*.rs",
]

Patterns in [exclude].paths use gitignore syntax and are merged with the .papertowelignore file described below.

.papertowelignore

Create a .papertowelignore file in your repo root to list paths that should be skipped entirely. The syntax is identical to .gitignore.

# Build output
target/

# Vendored code we don't control
third_party/

# Files that legitimately contain slop vocabulary
src/scrubber/lexical.rs

Both scan and scrub honour this file. Ignored files are never read by any detector or transform.

Inline Directives

For finer-grained control, papertowel recognises two comment directives that can be placed directly in source files.

papertowel:ignore-file

Place this directive in any comment near the top of a file to tell papertowel to skip the entire file. This is useful when a file must contain slop vocabulary (for example, a test fixture or a reference corpus).

#![allow(unused)]
fn main() {
// papertowel:ignore-file
pub const SLOP_WORDS: &[&str] = &["robust", "seamless", "delve"];
}

The directive works with any single-line comment style:

# papertowel:ignore-file
SLOP = ["robust", "seamless", "delve"]
-- papertowel:ignore-file
SELECT * FROM slop_words;

papertowel:ignore-next-line

Suppresses findings that start on the immediately following line. Use this when a single line legitimately triggers a detector but the rest of the file should still be analysed.

#![allow(unused)]
fn main() {
// papertowel:ignore-next-line
let description = "A robust and comprehensive guide";
}

Multiple directives can appear in the same file:

#![allow(unused)]
fn main() {
fn build_corpus() -> Vec<&'static str> {
    vec![
        // papertowel:ignore-next-line
        "delve",
        // papertowel:ignore-next-line
        "facilitate",
        "normal_word",
    ]
}
}

Precedence

Suppression layers are evaluated in order:

  1. .papertowelignore / [exclude].paths — file is never opened.
  2. papertowel:ignore-file — file is read but all detectors are skipped.
  3. papertowel:ignore-next-line — detectors run, but findings on the suppressed line are removed from the report.

CLI Command Reference

This section provides a detailed reference for the papertowel command-line interface.

Scrubber Commands

papertowel scan <path>

Reports AI fingerprints in the specified path. This is a read-only operation.

Options:

  • --format <json|text|sarif|github-actions>: The output format. sarif emits SARIF 2.1.0 for integration with VS Code SARIF Viewer and GitHub Code Scanning. (Default: text)
  • --severity <low|medium|high>: Filter findings by minimum severity.
  • --fail-on <low|medium|high>: Exit with code 1 if any finding at or above the threshold is found.
  • --ci: Auto-detected from the CI env var. Implies --fail-on medium and --format github-actions unless overridden.

Files listed in .papertowelignore (or [exclude].paths in .papertowel.toml) are skipped. Files containing a // papertowel:ignore-file directive are also skipped. Individual lines can be suppressed with // papertowel:ignore-next-line.

Example:

papertowel scan . --severity medium

papertowel scrub <path>

Detects and automatically fixes AI fingerprints in the specified path.

Options:

  • --dry-run: Preview changes without applying them to the filesystem.
  • --detectors <list>: A comma-separated list of detectors to run (e.g., lexical,comments).

Example:

papertowel scrub . --dry-run

papertowel clean <path>

A convenience command that runs the full pipeline: scan followed by scrub.

Options:

  • --dry-run: Preview changes.

papertowel grade <path>

Assigns a letter grade (A+ to F) based on the project’s overall AI fingerprint level. Lower slop scores yield better grades.

Options:

  • --format <json|text>: Output format. JSON includes detailed category breakdowns. (Default: text)
  • --min-grade <grade>: Exit with code 1 if the grade is below this threshold. Useful for CI gating. Valid values: A+, A, A-, B+, B, B-, C+, C, C-, D+, D, D-, F.
  • --ci: Shorthand for --min-grade C. Fails the build if the project scores C- or below.

Grade calculation:

Grades are based on a weighted “slop score” across categories:

CategoryWeight
Lexical (slop vocabulary)20%
Architecture20%
Comments15%
Structure15%
Metadata10%
Testing10%
History10%
Workflow5%

Example:

papertowel grade .
papertowel grade . --min-grade B --ci
papertowel grade . --format json

Recipe Commands

See Recipes for the full recipe TOML format and how to write custom recipes.

papertowel recipe list

Lists all available recipes from all sources (built-in, user-global, and repo-local).

Options:

  • --source <builtin|user|repo>: Filter results to a specific source.

Example:

papertowel recipe list
papertowel recipe list --source builtin

papertowel recipe show <name>

Displays details of a specific recipe including its patterns and scoring config.

Options:

  • --raw: For file-backed recipes, output the raw TOML instead of the parsed summary.

Example:

papertowel recipe show slop-vocabulary

papertowel recipe validate <path>

Validates the syntax and structure of a recipe TOML file without scanning any code.

Example:

papertowel recipe validate .papertowel/recipes/my-recipe.toml

Wringer Commands

papertowel wring init

Sets up the git worktree for the public branch.

Options:

  • --branch <name>: The name of the public branch to create/use. (Default: public)

Example:

papertowel wring init --branch public

papertowel wring queue

Analyzes the difference between your development branch and the public branch to build a replay plan.

Options:

  • --from <branch>: The source branch containing your development work.
  • --profile <name>: The persona profile to use when scheduling the replay plan.

Example:

papertowel wring queue --from dev

papertowel wring drip

Replays commits from the queue into the public worktree on a human schedule.

Options:

  • --daemon: Run in the background and apply commits as their target time arrives.
  • --profile <name>: The persona profile to use for scheduling and message humanization.

Example:

papertowel wring drip --daemon --profile night-owl

papertowel wring status

Shows the current state of the queue and the synchronization position.

papertowel wring unlock-stale

Removes a stale drip lock file left behind by a previously crashed drip session. Safe to run while another drip is active — it only removes locks whose owning process is no longer running.


Profile Commands

papertowel profile create <name>

Launches an interactive builder to create a new persona profile.

papertowel profile list

Lists all available persona profiles (built-in and custom).

papertowel profile show <name>

Dumps the TOML configuration of a specific persona profile.


Hook Commands

papertowel hook install

Installs a pre-commit hook that scans staged files for AI fingerprints. The hook copies staged file contents to a temp directory (so it scans what’s being committed, not working-tree state) and runs papertowel scan --fail-on medium. If findings at medium severity or above are found, the commit is blocked.

Options:

  • --force: Overwrite an existing pre-commit hook, even if it wasn’t installed by papertowel.

The hook is idempotent — running install when the hook is already present is a no-op.

Example:

papertowel hook install
papertowel hook install --force  # overwrite a foreign hook

papertowel hook uninstall

Removes the papertowel pre-commit hook. Refuses to remove hooks that weren’t installed by papertowel.

Example:

papertowel hook uninstall

papertowel hook status

Shows whether a papertowel pre-commit hook is installed, and whether the installed hook was created by papertowel or is a foreign hook.


Learn Commands

papertowel learn repo <path>

Analyses the git history and source files in <path> to produce a Style Baseline — a statistical model of your coding habits. The baseline is saved to .papertowel/baseline.json in the repo root and can be passed to wring drip to ensure humanized history mirrors your real style.

Arguments:

  • <path>: Path to the repository root to analyse.

Example:

papertowel learn repo .

papertowel learn show <path>

Displays the Style Baseline previously generated for the repository at <path>.

Example:

papertowel learn show .