Skip to main content

A cli tool to enforce documentation for code suppressions

Project description

shamefile logo

 

Turn silent tech debt into reviewable and documented decisions.

Tests Lint CodeQL Coverage Quality Gate License Rust shamefile


shamefile won't let anyone silence a linter warning in your code without writing down why.

People are lazy. Both committer and code reviewer.

  • The committer slaps a // NOLINT comment when there's no easy fix. They don't justify it — in most languages there's no good place for that.
  • The code reviewer focuses on more important things: security, bugs, design. There's no dedicated time for checking new suppression arrivals.

Shamefile adds shamefile.yaml for the code reviewer and the shame CLI for the committer to give them tools to react before tech debt gets out of control.

Why it's important

A mysterious # noqa with no explanation, left by a developer who moved on years ago. Nobody remembers why. Nobody wants to touch it. This is how legacy code accumulates — silently, one linter suppression at a time.

shamefile interrupts that pattern. Every suppression is tracked in a single shamefile.yaml — one file, one purpose. When it changes in a pull request, a reviewer sees the full cost of a shortcut in a single diff. And as AI coding agents become routine PR authors, the registry acts as a consistent gate: whether a suppression was introduced by a human or a model, it ships with a written justification or it doesn't ship at all.

How it works

shamefile exposes two stages, one command each.

Scanshame me . walks your project, finds every suppression token, and syncs the central shamefile.yaml. New suppressions are registered with auto-filled metadata (owner from git blame, timestamp, source line). Stale entries are removed. The command fails if any entry lacks a why.

Documentshame next shows the first undocumented suppression, with the exact source line highlighted. Provide the reason inline (shame next "<reason>"), or target a specific entry with shame fix <location> <token> --why "<reason>". To delete a stale entry without editing the YAML by hand, use shame remove <location> <token> (alias shame rm).

The same interface works for a developer opening a PR and for an AI agent iterating through gaps one at a time — without having to read the full registry into context.

Workflow

1. Developer writes code with a suppression:

result = parse_legacy_api(raw)  # type: ignore

2. Pre-commit (or manual) run surfaces the gap:

$ shame me .
Creating new registry at /home/user/myproject/shamefile.yaml
Scanning . for suppressions...
Added 1 new entries to /home/user/myproject/shamefile.yaml
1 suppressions need documentation (why).
Run `shame next` to see the first one, or `shame next "<reason>"` to fill its why.

...

3. Developer documents it:

$ shame next
./src/api.py:42
    |
  42| result = parse_legacy_api(raw)  # type: ignore
    |                                 ^^^^^^^^^^^^^^

Fix with:
  shame next "<reason>"
  shame fix "./src/api.py:42" "# type: ignore" --why "<reason>"

$ shame next "legacy API returns untyped dict; types module in progress"
Documented: # type: ignore at ./src/api.py:42
All entries documented. No shame today!

4. Developer commits both api.py and shamefile.yaml. The shortcut and its justification land in the same PR, reviewable in a single diff.

CI/CD integration

On the CI side, shame me . --dry-run is read-only and deterministic. It validates three contracts:

Check Meaning
Coverage Every suppression in code is registered in shamefile.yaml
Staleness Every registered entry still points at a live suppression in code
Justification Every entry has a non-empty why

A failure on any of the three exits non-zero.

# .github/workflows/ci.yml
- name: Check suppressions
  run: shame me . --dry-run
Flag Description
--dry-run (-n) Read-only validation for CI/CD — never writes to disk
--hidden Also scan hidden files and directories (dotfiles)

Registry format

shamefile.yaml lives at the project root (git root if available, otherwise the working directory). Every entry is human-readable and stable under git diff:

---
config: {}
entries:

- location: ./src/api.py:42
  token: '# type: ignore'
  content: 'result = parse_legacy_api(raw)  # type: ignore'
  created_at: 2026-04-17
  owner: Anna Nowak <anna@example.com>
  why: 'legacy API returns untyped dict; types module in progress'
  • location and token form the entry's identity.
  • content is the verbatim source line — used for reconciliation when code moves.
  • owner and created_at are populated automatically on first run via git blame.
  • why is the only field that requires a written justification — from a developer or an AI agent. The PR reviewer decides whether the reason is good enough.

Cascade matching

A registry that breaks every time you refactor is worse than no registry. shamefile reconciles entries against source code in two passes:

  1. Location match — exact file:line + token.
  2. Content match — same source line + token (handles line shifts, with rename detection limited to the most recent commit via git diff HEAD~1..HEAD -M).

Reformatting a function or inserting imports above a suppression preserves the entry — owner, created_at, and why stay intact. Entries are only removed when the token itself is gone from the code.

Supported tokens

Token Tool Language
# noqa Flake8 / Ruff Python
# pylint: disable Pylint Python
# type: ignore Mypy Python
# pyright: ignore Pyright Python
# pytype: disable Pytype Python
# pyre-ignore / # pyre-fixme Pyre Python
nosec Bandit Python
# pragma: no cover Coverage.py Python
# fmt: off / # fmt: skip Black / Ruff Python
# isort: skip isort Python
# lint-fixme / # lint-ignore Fixit Python
# autopep8: off autopep8 Python
// eslint-disable, /* eslint-disable ESLint JS / TS / TSX
// tslint:disable, /* tslint:disable TSLint TS / TSX
// @ts-ignore, /* @ts-ignore TypeScript JS / TS / TSX
// @ts-expect-error, /* @ts-expect-error TypeScript JS / TS / TSX

Experimental tokens

New languages added via the Add a language template land here first. They have passed the existing test suite and a sanity run on a real codebase, but no real-world showcase has been contributed yet. Promotion to Supported tokens happens through the Verify and release a language flow.

Language

Supported file extensions: .py, .js, .jsx, .mjs, .cjs, .ts, .tsx.

Installation

Source Command
npm npm install -g shamefile
PyPI pip install shamefile
crates.io cargo install shamefile
From source cargo install --git https://github.com/BKDDFS/shamefile
Homebrew coming soon

All channels install the shame CLI. Run shame --help to verify.

Or as a pre-commit hook:

# .pre-commit-config.yaml
- repo: https://github.com/BKDDFS/shamefile
  rev: main
  hooks:
    - id: shamefile

Roadmap

  • MCP server — native integration for LLM-based PR authors (avoids loading the full registry into agent context)
  • Custom git merge driver — auto-resolve shamefile.yaml conflicts on parallel PRs
  • Additional language grammars — Rust, Go, Java, Kotlin, C# via tree-sitter
  • Custom entry fields — attach ticket, reviewer, or deadline metadata to suppressions
  • Native exclusion config — first-class exclude: patterns in shamefile.yaml for checked-in vendored or generated code that bypasses the default .gitignore discovery

FAQ

Why not just write the reason inline, like # noqa: F401 # legacy import?

  • Reviewers don't see it. A # noqa buried in one of seven changed files rarely gets pushback. shamefile.yaml puts every suppression in the PR into one diff — the reviewer sees the full cost as a single list, with author and why per entry.
  • Nothing forces a reason. Linters accept any string after the token, or none. shame me . --dry-run fails the build until every entry has a non-empty why. This matters most for AI coding agents, which lose the suppression's context the moment the session ends — the registry forces them to write the reason to disk while it still exists.
  • Inline is a bad trade-off. A short reason carries no information; a useful one drowns the line of code it is attached to. The registry keeps source readable and justifications detailed.

What stops developers from writing why: 'TODO' and moving on?

The tool guarantees a string is written; the reviewer judges whether it is a real reason. If why: 'TODO' passes review, that is an organisational gap, not a tool gap — but the registry makes the gap visible: every lazy entry is one grep away, by author and date. Before shamefile, the same shortcut was hidden inside whichever file it lived in.

Won't shamefile.yaml become a merge conflict magnet on parallel PRs?

The registry is sorted by (location, token), so suppressions added in unrelated parts of the codebase land in different regions of the file — most parallel PRs do not collide. When they do, shame me is idempotent: after a merge, running it on the resolved tree deterministically reconciles entries from source, so git checkout --theirs shamefile.yaml && shame me . is the escape hatch. A custom git merge driver that resolves automatically is on the Roadmap. This is the same trade-off every shared-file tool (lockfiles, changelogs, schema migrations) has accepted in exchange for single-source-of-truth visibility.

What about generated, vendored, or third-party code?

A repo's typical generated/vendored content is excluded for free:

  • .gitignore and .ignore files are respected (handled by the same engine ripgrep uses), so node_modules/, target/, dist/, __pycache__/ etc. are skipped without configuration.
  • Only .py / .js / .jsx / .mjs / .cjs / .ts / .tsx are scanned, so vendored content in any other language is silently ignored.

A first-class exclude: config in shamefile.yaml is on the Roadmap.

Contributing

Contributions are welcome. Where you start depends on what you have:

  • Found a bug? Open an issue with a minimal repro.
  • Idea or design question? Open a Discussion under Ideas so direction can be agreed before any code is written.
  • Usage question or trouble setting things up? Ask in Q&A.
  • Want to send a PR? Read CONTRIBUTING.md first — dev setup, build/test/lint commands, commit format.
  • Security vulnerability? Use the private advisory form — see SECURITY.md. Do not open a public issue.

By participating you agree to the Code of Conduct.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shamefile-0.1.5.tar.gz (142.2 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

shamefile-0.1.5-py3-none-win_arm64.whl (1.7 MB view details)

Uploaded Python 3Windows ARM64

shamefile-0.1.5-py3-none-win_amd64.whl (1.8 MB view details)

Uploaded Python 3Windows x86-64

shamefile-0.1.5-py3-none-musllinux_1_2_x86_64.whl (2.0 MB view details)

Uploaded Python 3musllinux: musl 1.2+ x86-64

shamefile-0.1.5-py3-none-musllinux_1_2_aarch64.whl (1.8 MB view details)

Uploaded Python 3musllinux: musl 1.2+ ARM64

shamefile-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.9 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

shamefile-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.8 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

shamefile-0.1.5-py3-none-macosx_11_0_arm64.whl (1.8 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

shamefile-0.1.5-py3-none-macosx_10_12_x86_64.whl (1.8 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file shamefile-0.1.5.tar.gz.

File metadata

  • Download URL: shamefile-0.1.5.tar.gz
  • Upload date:
  • Size: 142.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for shamefile-0.1.5.tar.gz
Algorithm Hash digest
SHA256 162904c5dc6f4a98587cabbdee7ff366d17e3b53cddede36dc3e60e83b81bbd6
MD5 07f7df5f1ca086555e018480cdb6b159
BLAKE2b-256 5f80f42169c5d327404bf4409e4526a1271004bdfc936b65e4f5f7e7696802dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5.tar.gz:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-win_arm64.whl.

File metadata

  • Download URL: shamefile-0.1.5-py3-none-win_arm64.whl
  • Upload date:
  • Size: 1.7 MB
  • Tags: Python 3, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for shamefile-0.1.5-py3-none-win_arm64.whl
Algorithm Hash digest
SHA256 2a50bdfae61a069e4934b3d343fe8119e442739a0d006c09f571769d7f588244
MD5 962274a2d346a828839e26c844721de1
BLAKE2b-256 6b4dc45c017efc84f6ff885c9f31c573ac22fb5ac681eb54121bdda6ea2d1b26

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-win_arm64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-win_amd64.whl.

File metadata

  • Download URL: shamefile-0.1.5-py3-none-win_amd64.whl
  • Upload date:
  • Size: 1.8 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for shamefile-0.1.5-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 d7f52f2bf42656d8b0eccc677298df7720f45710a8ba11874c7a4ced29ce9941
MD5 f73522b24ecf96de3b434716dca98314
BLAKE2b-256 63dc0786a461c111bacb879c349a12753dc22dd4e1dea64fd7b3e236805dbbeb

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-win_amd64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for shamefile-0.1.5-py3-none-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 02917f1dd08182ce4665c8bf77b3b857a7f0aaa25525666425d1a56ffb192983
MD5 fed03b76c5332bff4d950e5b8fae7409
BLAKE2b-256 5c06f0f441e65925b8d2f83d8f175437ab8013386d82bd313bd53f2e639aec35

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-musllinux_1_2_x86_64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for shamefile-0.1.5-py3-none-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 d7a88de30f3e47c3b8cf3274a124beffab32b8f8036e66f5889cc4a1adb6318b
MD5 1ba7e8b82ada518a97d23d3e51629e7b
BLAKE2b-256 ec3435a3da8d99cd958111416946beb4d0b3dde26b77483728e994f3f501a4d0

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-musllinux_1_2_aarch64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for shamefile-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4470d79fa648d73a282acea4e4f681037be752ca1ff84ef6282a00cba68d1ae4
MD5 fd3eb7a194007446f9f951dfa3a0b8e8
BLAKE2b-256 96d83f8c4017afdc133755853a8eb05cfc5da907d9f78ac07c7ccfde5a955ca4

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for shamefile-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 65a1c20644e3ef8a7dde968a54ac82e5ef7860cc6fee960a178375216ac6ab50
MD5 95148dc9b56ca2754ccb30bb7ee32c6a
BLAKE2b-256 1ac0908ed37e2bbdeca328b73858ecc20814108ef4b773a979bfea66e45cb96d

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for shamefile-0.1.5-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f63a73c27a055bff626f60f4f7bd62214a3ff159a1b996511feb75100c867429
MD5 1958b8fd05d5aee426bcdb63d4ee256e
BLAKE2b-256 c075848761b328bb9b4e35fcb0a6663d26df88bb0619698530972ae16e40de0c

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-macosx_11_0_arm64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shamefile-0.1.5-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for shamefile-0.1.5-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 0f2ba2bb996eaf8b1a2a0124a513d1b4fc04abd06f39d80a76884d0428d2d4a1
MD5 20182500f862f3166d6475de1763011c
BLAKE2b-256 7693c0d65470d5ca4d433e6fbe80b37fc90c55f9eb11b54b3b7770917bfb9421

See more details on using hashes here.

Provenance

The following attestation bundles were made for shamefile-0.1.5-py3-none-macosx_10_12_x86_64.whl:

Publisher: publish-pypi.yml on BKDDFS/shamefile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page