Skip to main content
Glama

๐Ÿงบ Code Laundry

Clean AI-generated code before you commit to it.

Code Laundry is an MCP server and CLI that AI coding agents call to check, fix, and sanitize the code they generate before a full build. You submit code as a string, a file, or a diff. You get back cleaned code, a structured report of what was wrong, and one of three verdicts: pass, fixed, or reject.

It runs deterministic checks first, using real tools (TypeScript, ESLint + typescript-eslint, Prettier, Semgrep, gitleaks, osv-scanner, the npm registry). An LLM is used only for repairs those tools can't make, and every LLM edit is checked again before it's accepted.

Status: v0.1. TypeScript and JavaScript. See Limitations.


Why

AI-generated code fails in predictable ways:

  • Hallucinated packages: imports that don't exist on npm, typosquats, packages npm removed as malware

  • Security holes: SQL and command injection, eval, path traversal, SSRF, XSS, weak crypto

  • Leaked secrets: API keys and tokens pasted inline

  • Silent bugs: missing await, swallowed errors, overly broad try/catch

  • Slop: truncated output (// ... rest of the code), placeholder TODOs, mock data left in, dead code, console.log

  • Invented APIs: calls to functions that don't exist (fs.readFileAsync) or not in the installed version of a package

Related MCP server: MCP Code Reviewer

How it works

 code โ”€โ–บ detect โ”€โ–บ parse โ”€โ–บ format โ”€โ–บ lint+fix โ”€โ”ฌโ–บ typecheck โ”€โ”
                     โ”‚                           โ”œโ–บ semgrep    โ”œโ”€โ–บ slop โ”€โ–บ repair loop โ”€โ–บ report
                     โ””โ”€ syntax error โ‡’ reject    โ”œโ–บ secrets    โ”‚   (fix)   (fix/strict:
                                                 โ””โ–บ deps โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            LLM, re-verified)

Stage

Tool

Fixes

Parse

TypeScript parser

Format

Prettier (project config if present)

โœ“

Lint

ESLint + typescript-eslint, including type-aware rules such as no-floating-promises (project flat config if present)

โœ“

Type check

TypeScript compiler (project tsconfig.json if present)

Security

Semgrep with bundled rules (rules/semgrep/)

Secrets

gitleaks + built-in patterns; values are always redacted

Dependencies

npm registry, OSV (osv-scanner, falling back to the OSV API), typosquat list

AI slop

AST heuristics; type errors that point at a package's own declarations become slop/invented-api

console.log

Repair

Claude, structured line-range edits, only inside flagged ranges

โœ“

  • Speed. ESLint, Prettier and TypeScript run in-process and stay warm in the MCP server. Analysis stages run in parallel. A typical check takes 2โ€“3 s, most of it Semgrep.

  • No silent skips. If a tool isn't installed or a service can't be reached, stagesRun says so, with the reason. In strict mode, a security or dependency check that didn't run rejects the code.

Verdicts

Verdict

Meaning

โœ… pass

Nothing blocking was found. Unfixed low/medium findings are returned as warnings.

๐Ÿ”ง fixed

Issues were found and nothing blocking remains. Use cleanedCode.

โ›” reject

Any of: an unfixed critical/high finding, a syntax error, a hardcoded secret, or a hallucinated/malicious dependency

All of these are configurable (see Configuration). In strict mode, medium findings also reject.

Installation

Requirements:

  • Node 20+ and pnpm

  • Strongly recommended, otherwise the related stages report themselves as skipped:

git clone https://github.com/stevenleon30/code-laundry.git
cd code-laundry
pnpm install        # also builds dist/

Optional LLM repair (fix/strict modes):

export ANTHROPIC_API_KEY=sk-ant-...
export CODELAUNDRY_MODEL=claude-opus-5-5     # optional; default claude-opus-5-5
export CODELAUNDRY_EFFORT=high               # optional; low | medium | high | xhigh | max

Without a key, every deterministic check still runs, and the repair stage reports skipped.

Use with Claude Code

claude mcp add code-laundry -- node /absolute/path/to/code-laundry/dist/mcp/server.js

Or commit a .mcp.json in your project:

{
  "mcpServers": {
    "code-laundry": {
      "command": "node",
      "args": ["/absolute/path/to/code-laundry/dist/mcp/server.js"],
      "env": { "ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}" }
    }
  }
}

To make the agent use it, add a line like this to your CLAUDE.md:

Before writing generated code to disk, call launder_code with mode: "fix" and context.projectRoot set to this repo. Use cleanedCode; if the verdict is reject, fix the findings and call it again. Call check_dependencies before adding any package.

Use with Claude Desktop

Add this to claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/

  • Windows: %APPDATA%\Claude\

{
  "mcpServers": {
    "code-laundry": {
      "command": "node",
      "args": ["/absolute/path/to/code-laundry/dist/mcp/server.js"],
      "env": { "ANTHROPIC_API_KEY": "sk-ant-..." }
    }
  }
}

Claude Desktop starts servers with a minimal PATH. If Semgrep or gitleaks show as skipped, point to them explicitly with "CODELAUNDRY_SEMGREP": "/path/to/semgrep" and "CODELAUNDRY_GITLEAKS": "/path/to/gitleaks" in env.

MCP tools

launder_code

Input

code or diff

Full source, or a unified diff for one file. A diff is applied to projectRoot/filename, or to an empty file when creating one.

language?

typescript | javascript. Detected from the filename or content if omitted.

filename?

Path relative to projectRoot. Selects the parser and the project config that applies.

context?

{ projectRoot?, configPath? }. projectRoot enables your tsconfig, ESLint and Prettier configs, .codelaundry.json, installed package types, and the undeclared-import check.

mode

check (default; report only) | fix | strict

Returns { verdict, verdictReasons, language, cleanedCode, findings[], diff, stagesRun[], durationMs }. Each finding has:

{
  "ruleId": "semgrep/sql-injection",
  "source": "semgrep",
  "category": "security",
  "severity": "critical",
  "message": "User-controlled data flows into a SQL query string. ...",
  "location": { "line": 8, "column": 35, "endLine": 8, "endColumn": 90 },
  "fixable": false,
  "fixed": false,
  "snippet": "..."
}

check_dependencies

Input is { packageJson } (object or string) or { imports: ["lodash", "lodash@4.17.15", "@scope/pkg/sub"] }. It flags:

  • packages that don't exist, or that npm replaced with a security placeholder

  • typosquats of popular packages

  • known vulnerabilities, with the version to upgrade to

  • deprecated, very new, or rarely downloaded packages

  • imports missing from package.json

explain_findings

Input is { findings }. Returns plain-language explanations and concrete fix guidance, blocking findings first. It is deterministic; no LLM is called.

CLI

codelaundry check src/handler.ts          # report only
codelaundry fix src/handler.ts            # apply fixes (and LLM repair if configured), write the file
codelaundry fix src/handler.ts --stdout   # print cleaned code instead
cat gen.ts | codelaundry check - --filename gen.ts
codelaundry deps                          # check ./package.json
codelaundry deps axois lodash@4.17.15     # check specific packages

Options: --strict, --project <dir> (default: nearest package.json), --no-project, --config <file>, --diff, --json.

Exit codes: 0 for pass or fixed, 1 for reject, 2 for a usage or internal error. fix never writes a file that contained a secret: the cleaned code has the secret redacted, and writing it would break the file.

During development, use pnpm cli check <file> and pnpm mcp.

Configuration

Put a .codelaundry.json file at your project root. It is validated with Zod, and unknown keys are errors.

{
  "stages": { "ts.semgrep": true, "ts.deps": true, "repair": true },
  "thresholds": {
    "rejectOn": ["critical", "high"],
    "strictRejectOn": ["medium"],
    "rejectOnSyntaxError": true,
    "rejectOnSecret": true,
    "rejectOnHallucinatedDependency": true
  },
  "ignoreRules": ["slop/console-log", "eslint/@typescript-eslint/no-explicit-any"],
  "severityOverrides": { "slop/todo": "medium" },
  "allowedPackages": ["@my-org/*"],
  "repair": { "maxIterations": 3, "contextLines": 5, "model": "claude-opus-5-5" },
  "deps": { "minWeeklyDownloads": 500, "minAgeDays": 30 },
  "useProjectConfigs": true
}

Stage ids: ts.parse, ts.format, ts.lint, ts.typecheck, ts.semgrep, secrets, ts.deps, ts.slop, repair.

Environment variable

Purpose

ANTHROPIC_API_KEY

Enables LLM repair

CODELAUNDRY_MODEL, CODELAUNDRY_EFFORT

Repair model and effort

CODELAUNDRY_SEMGREP, CODELAUNDRY_GITLEAKS, CODELAUNDRY_OSV_SCANNER

Explicit tool paths

CODELAUNDRY_OSV_OFFLINE=1

Use osv-scanner's offline database (when api.osv.dev is blocked; ~200 MB download once)

CODELAUNDRY_NPM_REGISTRY

Registry URL (default https://registry.npmjs.org)

CODELAUNDRY_ALLOWED_ROOTS

Restrict which projectRoots callers may use (path-list)

CODELAUNDRY_MAX_BYTES

Input size limit (default 1 MB)

CODELAUNDRY_DEPS_FIXTURE

Serve dependency data from a JSON fixture (tests and evals)

Safety

  • Submitted code is never executed.

    • It is parsed and analysed in memory.

    • Semgrep and gitleaks read a copy in a private temporary directory, which is deleted afterwards.

    • The test-run stage is not implemented in v1. If added, it will be opt-in and run only in a network-less, resource-limited container.

  • Secrets are never returned or sent anywhere.

    • Detected secret values are redacted from findings, messages, diffs and cleanedCode.

    • Secrets are never included in what is sent to the LLM.

    • External tools run without your API keys in their environment.

  • Submitted code is treated as untrusted data.

    • Instructions in comments or strings have no effect on the deterministic stages.

    • The LLM gets the code inside <untrusted_code> delimiters, is told to ignore embedded instructions, and can only answer with schema-validated line edits.

  • LLM edits are restricted.

    • Edits outside the flagged lines (plus repair.contextLines of context) are rejected.

    • Each candidate is re-verified by all deterministic stages and kept only if findings decrease.

    • The loop runs at most 3 rounds.

  • Project configs are executed. With projectRoot, ESLint and Prettier configs from that project are loaded, and they are JavaScript. Only point Code Laundry at projects you trust, and use CODELAUNDRY_ALLOWED_ROOTS to restrict callers.

Evals

evals/ holds 89 labeled cases: 74 bad AI-style snippets and 15 clean controls. Each case is a snippet plus a JSON file listing the findings expected for it.

Category

Cases

Security (command injection, path traversal, SSRF, XSS, weak crypto, TLS)

11

Wrong/invented API usage

9

Hallucinated, typosquatted or removed packages

8

SQL injection

7

Leaked secrets

7

Missing await

7

Bad types

7

AI slop

7

Swallowed errors

6

Dead code

5

pnpm evals               # detection + repair
pnpm evals --no-repair   # detection only
pnpm evals --filter sqli

The runner reports recall, precision (medium+ findings), the false-positive rate on clean controls, and the repair success rate. It fails below the thresholds in evals/thresholds.json. Dependency lookups use a recorded fixture, so results are deterministic.

The GitHub Action runs type-checking, unit tests and detection evals on every PR. LLM repair evals call the API for every repairable case, so they run only on manual dispatch with an ANTHROPIC_API_KEY secret.

The current detection results are 100% recall, 100% precision and a 0% false-positive rate, including the 16-case held-out subset. Read that with care: the same author wrote the rules and the cases, so it shows coverage of the anticipated failure modes, not real-world accuracy. A corpus of real agent output is the next step.

Limitations

  • TypeScript/JavaScript only in v1. See Adding a language.

  • Invented APIs in third-party packages are detected only with projectRoot, because the installed type declarations are needed. Node built-ins are always checked.

  • Semgrep's open-source taint analysis is per-function. Flows across functions or files are missed.

  • Typosquat detection compares against a hand-curated list of about 400 popular packages.

  • Private packages 404 on the public registry. They are accepted when installed in projectRoot/node_modules; otherwise add them to allowedPackages.

  • Download counts come from api.npmjs.org. If it's unreachable, the check is noted and never blocks, because it is a weak signal.

  • Diff input produces findings for the whole resulting file, not only the changed lines.

  • ESLint 10 dropped legacy .eslintrc. Such projects get the bundled config.

  • Fixed-finding attribution after ESLint autofix or LLM repair is counted per rule, not per occurrence.

Adding a language

The core is language-agnostic. A language is a LanguageModule (detection plus an ordered list of Stages) registered in src/core/registry.ts. For Python:

  1. Add src/stages/python/ with ruff format, ruff check --fix, mypy, bandit, a Semgrep Python ruleset, and a PyPI dependency provider.

  2. Register the module.

The pipeline, verdict logic, repair loop, MCP server, CLI and eval runner don't change. See docs/ARCHITECTURE.md.

Development

pnpm test        # unit and integration tests (vitest)
pnpm typecheck
pnpm evals
pnpm build

Contributing

Issues and PRs are welcome. Every new detection should come with eval cases in evals/cases/, so that recall and false-positive rate stay measurable. Fake credentials go in evals/synthetic-secrets.ts and are referenced as __SECRET_NAME__, so no secret-shaped literal is committed.

License

MIT

Related MCP Connectors

Related MCP Servers