A fast, deterministic quality gate for AI-assisted code.
slopfence finds the junk that AI coding assistants leave behind (hallucinated packages, tests that test nothing, placeholder stubs, duplicate helpers, swallowed errors and leftover chat text) before it gets merged.
🚧 Status: alpha (v0.2, Python only). Nine detectors work, each tuned against 20,000+ files of real-world code for false positives. Expect rough edges. Feedback is very welcome. See Contributing.
Quick start
pip install slopfence
Or run it without installing, using uv or pipx:
uvx slopfence .
pipx run slopfence .
Check a project:
slopfence .
Check only what your branch changed (ideal for pull requests):
slopfence --diff main
Options
| Option | What it does |
|---|---|
--diff REF |
Only report issues on lines changed since REF (plus new untracked files) |
--format text|json|sarif |
Output format. SARIF shows findings inline on GitHub pull requests |
-o, --output FILE |
Write the report to a file |
--offline |
Make no network requests (disables SLOP001) |
--check-imports / --no-check-imports |
Also look up unknown imports in your source code on PyPI. Off by default, because it sends those names to pypi.org (see What slopfence sends over the network) |
--select / --ignore |
Comma-separated rule IDs to run or skip, e.g. --ignore SLOP051 |
--exclude |
Comma-separated paths or globs to skip, e.g. --exclude migrations,*_pb2.py |
--known-packages |
Comma-separated private package names or globs that SLOP001 must accept, e.g. --known-packages corp-* |
--detect-private-index / --no-detect-private-index |
Skip PyPI lookups when pip or uv is set up with a private index (on by default). Turn it off if that index only mirrors PyPI. See Private package indexes |
--fail-on high|medium|low |
Lowest severity that fails the run (default low: any issue). All issues are still reported |
--strict / --no-strict |
Exit 2 if any Python file can't be parsed (by default it's skipped with a warning) |
--exit-zero |
Always exit 0 (report only) |
--list-rules |
Show all rules |
Exit codes: 0 no issues (or none at or above --fail-on), 1 issues found, 2 error (bad options or config, or unparseable files with --strict).
Run slopfence with a Python at least as new as the syntax your code uses; otherwise newer syntax can't be parsed.
Configuration
Add a [tool.slopfence] table to the pyproject.toml at your project root:
[tool.slopfence]
select = ["SLOP001", "SLOP010", "SLOP020"] # rules to run (default: all)
ignore = ["SLOP051"] # rules to skip
exclude = ["migrations", "tests/fixtures/", "*_pb2.py"]
known-packages = ["corp-auth", "corp-*"] # private packages that aren't on PyPI
check-imports = false # true also looks up imports on PyPI (default: false)
detect-private-index = true # false if your private index only mirrors PyPI
fail-on = "high" # only high-severity issues fail the run
strict = true # unparseable files are an error
exclude works like .gitignore: a pattern without a / in the middle matches any file or folder name anywhere (migrations, *_pb2.py), and a pattern with one is relative to the project root (tests/fixtures/). Excluded files are never checked, but imports of excluded modules still count as your own code.
Command-line options (--select, --ignore, --exclude, --known-packages, --check-imports, --detect-private-index, --fail-on, --strict) replace the matching config values. Unknown keys and rule IDs are reported as errors (exit code 2).
Ignoring a finding
# In a real implementation, use the cache. # slopfence: ignore[SLOP010]
x = legacy() # slopfence: ignore
Put # slopfence: ignore-file anywhere in a file to skip it. In requirements.txt, add # slopfence: ignore to a line.
Private packages: if your project uses internal packages that aren't on PyPI, list them once in known-packages instead of adding ignore comments everywhere. Names are matched case-insensitively with -, _ and . treated alike (so corp_auth matches corp-auth), and * globs work. Known packages are never looked up on PyPI, and imports of them are never flagged.
SLOP010 (placeholders) also skips test code and demo or example code: files under demo/, examples/ or samples/ folders, or named like demo.py or auth_example.py.
What slopfence sends over the network
slopfence only talks to one server, https://pypi.org, and only for SLOP001. It sends a request per package name (HEAD https://pypi.org/pypi/<name>/json) and nothing else: no code, no file names, no telemetry.
| Names sent to PyPI | When |
|---|---|
Dependencies declared in requirements*.txt and pyproject.toml |
By default. These names are already meant for a package index. Entries installed from URLs, paths or git are never sent. Neither is any dependency in a file that uses a private index: --index-url / --extra-index-url in a requirements file, a [[tool.poetry.source]] or [[tool.uv.index]] that isn't PyPI (unless it's explicit, which only affects the dependencies that name it), or a per-dependency Poetry source or [tool.uv.sources] entry |
| Imports in your source code that aren't stdlib, installed, part of your project, or provided by a declared or locked package | Only with --check-imports (or check-imports = true). This can catch an invented package that was imported but never declared, but for private code it may reveal internal package names |
Names in known-packages are never sent. Answers are cached for a day (missing) or a week (found) in ~/.cache/slopfence, so repeated runs send fewer requests. --offline sends nothing at all.
Private package indexes
If your packages may come from an index other than PyPI, a name that PyPI doesn't know could be a private package. slopfence then doesn't report it and doesn't send it to PyPI. It looks for private indexes in:
| Where | What counts as private |
|---|---|
requirements*.txt |
-i / --index-url / --extra-index-url with a non-PyPI URL: that file's packages |
pyproject.toml |
[[tool.poetry.source]] (except explicit ones), [[tool.uv.index]] (except explicit ones), [tool.uv] index-url / extra-index-url (also under [tool.uv.pip]), [[tool.pdm.source]]: that file's packages. A per-dependency Poetry source or [tool.uv.sources] entry: that package |
Pipfile, Pipfile.lock |
A [[source]] that isn't PyPI |
| Environment variables | PIP_INDEX_URL, PIP_EXTRA_INDEX_URL, UV_INDEX, UV_DEFAULT_INDEX, UV_INDEX_URL, UV_EXTRA_INDEX_URL |
| pip configuration | pip.conf / pip.ini in pip's global, user and virtualenv locations, and PIP_CONFIG_FILE ([global] and [install] sections) |
| uv configuration | uv.toml in the project, user and system config folders, or UV_CONFIG_FILE (UV_NO_CONFIG turns this off), including its [pip] table used by uv pip install |
A private index in a project file stops lookups for that file's dependencies, and for imports with --check-imports. A private index in your environment (variables, pip or uv configuration) could serve any package, so SLOP001 makes no lookups at all and the report says so in a note. An index is public only if its host is pypi.org or pythonhosted.org. slopfence never prints index URLs, because they can contain credentials.
If your company index only mirrors PyPI, set detect-private-index = false (or pass --no-detect-private-index) to check packages against PyPI again, and list your own private packages in known-packages.
Imports with a different package name
With --check-imports, an import isn't looked up when a declared dependency or a locked package provides it, even if the names differ and the package isn't installed where slopfence runs. For example, import bs4 is covered by beautifulsoup4, import dateutil by python-dateutil, and from google.cloud import storage by google-cloud-storage. slopfence reads uv.lock, poetry.lock, pdm.lock, Pipfile.lock and Pipfile, so indirect dependencies count too (import idna when only requests is declared).
What the code-quality rules skip
To keep false positives low, the newer rules are deliberately conservative:
SLOP011stub functions needs a docstring that promises work (it starts with a verb like validate, fetch, send, calculate) or admits it isn't done (placeholder, TODO, for now). The body must then return hardcoded data while ignoring every input, or return nothing although the return type promises a value. A placeholder docstring over a body that does nothing (pass,...,return None) is reported even without a return type..pyistub files are never checked. Abstract methods,@overload, properties,Protocol/ABC classes,if TYPE_CHECKING:blocks, documented hooks ("override this", "does nothing by default") and tests are skipped.SLOP030duplicate functions ignores functions with fewer than 3 statements and test code. Parameter and variable names and docstrings don't matter; called functions, attributes and constants do. A full scan reports identical copies (every copy except the first). With--diff, functions you changed are also compared with the rest of the project and reported when they're ≥90% similar to an existing function, because that's when an assistant re-writes a helper that already exists. Never reported: methods with the same name in different classes (plugins, drivers), functions defined twice in one file (underif/try), near-duplicate methods, and parallel families that differ only by swapped names, operators or constants (md5_utf8/sha_utf8,polyadd/polysub).SLOP040swallowed exceptions only flagsexcept Exception,except BaseExceptionand bareexcept:whose body is justpassor.... A comment in the handler (# best effort), cleanup code (__del__,__exit__,close(),atexithandlers,try: os.unlink(...), also in a loop), optional imports (try: import ujson),contextlib.suppress, tests and examples are skipped.
GitHub Action
# .github/workflows/slopfence.yml
name: slopfence
on: pull_request
permissions:
contents: read
jobs:
slopfence:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: syedmuhdahmad/slopfence@v0.2.0
On pull requests it automatically checks only the changed lines (set diff: none to check everything, or diff: <ref> for a specific ref). To show findings inline on the pull request, set sarif-file: slopfence.sarif and upload it with github/codeql-action/upload-sarif (needs security-events: write).
pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/syedmuhdahmad/slopfence
rev: v0.2.0
hooks:
- id: slopfence
The problem
AI coding tools (Claude, Copilot, Cursor, Gemini, Codex and others) are now part of everyday development. They write code fast, and that code usually looks correct. It compiles, it's well formatted, and it has comments and tests.
But it often comes with recognizable junk:
- Packages that don't exist. The AI imports
flask-jwt-simple-authbecause the name sounds plausible. Attackers now register these invented names to spread malware, a technique known as slopsquatting. - Tests that test nothing. A test creates a mock, sets a value on it, then asserts that value. It passes forever and protects nothing.
- Placeholder code shipped as real code. Comments like
# In a real implementation, validate the tokenabove a function that returnsTrue. - Duplicate helpers.
format_date(),formatDate()anddate_formatter()in three different files, because the AI wrote a new helper instead of finding the existing one. - Over-defensive code.
try/except Exception: passaround code that can't fail, which hides real errors. - Comment noise.
# Increment counter by 1abovecounter += 1, or leftover chat text like "Here's the updated function:".
Reviewers miss these because the code looks fine. Each one is small. Together they add up to security holes, false confidence from green test suites, and codebases that get harder to maintain every sprint.
Why existing tools aren't enough
| Tool | What it's good at | What it misses |
|---|---|---|
| Ruff, ESLint, Pylint | Style and common bugs | AI-specific patterns, invented packages |
| Vulture, Knip | Dead code | Fake tests, placeholders, near-duplicates |
| jscpd | Copy-pasted code | Code that's similar but not identical |
| Socket, Snyk | Supply-chain security | Code-quality patterns; mostly paid |
| slopcheck (unrelated project) | Checking for hallucinated packages before you install them | Code-quality patterns such as fake tests, placeholders and chat leftovers |
AI code reviewers (e.g. Claude /code-review) |
Deep, context-aware review | Not deterministic, costs tokens, doesn't automatically check package registries, runs only when someone asks |
No single tool focuses on the failure modes of AI-written code, built for reviewing pull requests. That's the gap slopfence fills.
slopfence vs. AI reviewers: complementary, not competing
Developer + AI writes code
│
▼
slopfence ← fast, free, same result every time; runs on every commit
│
▼
AI / LLM review ← deep reasoning about logic and design
│
▼
Human review
AI reviewers are smart but nondeterministic and cost money on every run. slopfence catches the objective, repeatable problems in seconds with no API keys, so reviewers (human or AI) can spend their attention on what matters.
Who needs it
- Teams adopting AI coding tools that want a safety net without banning the tools.
- Open-source maintainers reviewing a growing number of AI-generated pull requests.
- Security teams worried about hallucinated or slopsquatted dependencies.
- Tech leads who want an objective quality gate instead of arguing over style in review.
- Solo developers who want to catch their AI assistant's mistakes before they commit.
Design principles
- Detect patterns, not authorship. slopfence never tries to guess whether AI wrote the code. That's unreliable and leads to arguments. It flags low-quality patterns that AI commonly produces, no matter who wrote them.
- Deterministic by default. Rule-based checks with no LLM required. The same input always gives the same output.
- Diff-first. Check only what changed in a pull request. Nobody wants 4,000 warnings on an old codebase.
- Low false positives over high recall. A noisy linter gets uninstalled. Start strict, flag only obvious cases, and make everything configurable.
- Fits existing workflows. CLI, pre-commit hook, GitHub Action, and SARIF output for GitHub code scanning.
Detectors
| ID | Detector | Severity | Status |
|---|---|---|---|
SLOP001 |
Dependency (or, with --check-imports, import) of a package that doesn't exist on PyPI |
🔴 High | ✅ v0.1 (Python) |
SLOP002 |
Dependency that is very new or has suspiciously few downloads (slopsquatting risk) | 🔴 High | Planned |
SLOP010 |
Placeholder / stub comment (In a real implementation…, Simplified for demo) |
🟠 Medium | ✅ v0.1 |
SLOP011 |
Function whose docstring promises work ("Validate the token") but which only returns hardcoded data or nothing | 🟠 Medium | ✅ v0.2 |
SLOP020 |
Test that only asserts on its own mocks | 🔴 High | ✅ v0.1 |
SLOP021 |
Test with no meaningful assertion (e.g. only assert True) |
🟠 Medium | ✅ v0.1 |
SLOP022 |
Test wrapped in try/except so it can never fail |
🔴 High | ✅ v0.1 |
SLOP030 |
Function identical to another one in the project (names and docstrings ignored); with --diff, also new functions ≥90% similar to existing ones |
🟠 Medium | ✅ v0.2 |
SLOP040 |
except Exception (or bare except) that silently swallows every error |
🟡 Low | ✅ v0.2 |
SLOP050 |
Comment that only restates the code | 🟡 Low | Planned (#23) |
SLOP051 |
Leftover chat text in code (Certainly! Here's…) |
🟠 Medium | ✅ v0.1 |
Example output
$ slopfence --diff main
requirements.txt
2:1 SLOP001 Dependency 'flask-jwt-simple-auth' does not exist on PyPI (possible hallucination) [high]
src/auth.py
5:5 SLOP010 Placeholder left in code: "In a real implementation, validate the token signature." [medium]
tests/test_billing.py
7:5 SLOP022 Test 'test_refund' catches assertion failures without re-raising, so it can never fail [high]
tests/test_user.py
4:1 SLOP020 Test 'test_user_name' only asserts on its own mocks and never calls real code [high]
4 issues (3 high, 1 medium)
Architecture
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐
│ Files / │──▶│ Parser │──▶│ Rule engine │──▶│ Reporter │
│ git diff │ │ (ast / t-s) │ │ (detectors) │ │ CLI/JSON/SARIF│
└─────────────┘ └──────────────┘ └──────┬───────┘ └───────────────┘
│
┌─────────▼─────────┐
│ Optional: │
│ - registry lookup │
│ (cached) │
│ - LLM second │
│ opinion (--llm) │
└───────────────────┘
- Parser: slopfence uses Python's built-in
astandtokenize(no dependencies). tree-sitter is planned so new languages can be added without rewriting the detectors. - Registry lookups are cached locally (
~/.cache/slopfence) to stay fast and avoid rate limits. If PyPI can't be reached, nothing is flagged: an unknown answer is never treated as "missing". Import names are only sent with--check-imports(see What slopfence sends over the network). - Optional LLM mode (planned, not built yet) would send only the unclear cases for a second opinion. Never required.
Challenges (and how we plan to handle them)
| Challenge | Why it's hard | Plan |
|---|---|---|
| False positives | Every noisy warning erodes trust; one bad week and teams disable the tool. | Conservative rules, severity levels, # slopfence: ignore[SLOP0xx] comments, and a test corpus of real-world false positives. |
| Private / internal packages | Internal packages look identical to hallucinated ones. | Allowlists, private registry support, and reading local lockfiles and workspace packages. |
| Registry rate limits | Checking every import against PyPI/npm on every run is slow and gets throttled. | Local cache with TTL, batch lookups, offline mode. |
| "Is this really junk?" | Some patterns (defensive code, stubs) are legitimate in context. | Flag only high-confidence cases by default; stricter checks are opt-in. |
| Near-duplicate detection at scale | Comparing every function with every other is expensive. | AST normalization + hashing, and only comparing changed functions against the index. |
| Multi-language support | Each language has different idioms and test frameworks. | Start with Python only, do it well, then JS/TS via tree-sitter. |
| Tone and adoption | A tool that "shames AI users" will be resisted. | Position it as a quality gate for AI-assisted development, not against it. |
| Moving target | AI tools improve and their failure modes change. | Rules are data-driven and easy to add; community-contributed detectors. |
Roadmap
v0.1 (MVP): Python
- CLI:
slopfence .andslopfence --diff <branch> -
SLOP001packages that don't exist on PyPI (dependency files and imports) -
SLOP010placeholder comments -
SLOP020/SLOP021/SLOP022fake tests -
SLOP051leftover chat text - Ignore comments
- JSON and SARIF output
- GitHub Action and pre-commit hook
- Publish to PyPI
v0.2: private code, new rules (released)
Tracked in the v0.2.0 milestone. Upcoming work is on the roadmap board.
Planned features
Reliability
- Test the GitHub Action in a real workflow, including SARIF upload (#13)
- Test the pre-commit hook with
pre-commit(#14) - Allowlist for private packages (#15)
- Better import-name to package-name mapping (#16)
- Detect private indexes from
pip.conf, uv and Poetry (#17) -
--fail-onseverity threshold (#18) - Option to fail on unparseable files (#19)
- Fewer
SLOP010false positives from "for demo purposes" (#20) - Import lookups are opt-in, so private package names aren't sent to PyPI (#34)
- Smaller source distribution (#35)
New rules
-
SLOP040except Exceptionthat silently swallows errors (#21) -
SLOP011stub functions that onlypassor return fake data (#22)
Project
Later
-
SLOP050comments that only restate the code (#23) - Parallel processing for large repos (#12)
- JavaScript / TypeScript (npm registry)
- Slopsquatting risk scoring (
SLOP002) - VS Code extension
- Optional
--llmsecond opinion
Contributing
The project is at the very start, which is the best time to shape it.
- 💡 Have an example of AI-generated junk? Open an issue with a code snippet. Real examples are the most valuable input for designing detectors.
- 🐛 Know a pattern we're missing? Propose a detector in an issue.
- 🗣️ Disagree with the approach? Open a discussion. Better now than after v1.
Please read CONTRIBUTING.md before opening a pull request. Everyone taking part is expected to follow our Code of Conduct. Found a security issue? Report it privately as described in SECURITY.md. Need help? See SUPPORT.md.
License
Metadata
Release files for slopfence 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| slopfence-0.2.0.tar.gz | 71.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| slopfence-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 126.2 kB
Release files / slopfence-0.2.0.tar.gz
| Download URL | slopfence-0.2.0.tar.gz |
|---|---|
| Size | 71.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a89ad1d75201debd5ffa2402cbe75d200dc25fb404209176387aff8c406d0fe9
|
|
BLAKE2b-256 checksum How to use checksums |
732fd175d0fa4942ea5ab6f5312fad5fca0387e322e6107eb7590650dfeb45bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / slopfence-0.2.0-py3-none-any.whl
| Download URL | slopfence-0.2.0-py3-none-any.whl |
|---|---|
| Size | 54.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8cd31cf89ba8f45d4a7485ecabef4d9543cf91356ceceb3140ad91870a184a53
|
|
BLAKE2b-256 checksum How to use checksums |
340653b0beab369b99ed5b1132c12cdba2856de4fb1c7f6c49a21fad112f2423
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log