Contributing to SafeLint#
Contributions are welcome - bug fixes, new rules, new AI clients, new languages, documentation improvements, or ideas.
By participating in this project you agree to abide by our Code of Conduct. If you use safelint in academic or scientific work, see CITATION.cff for canonical citation metadata. If you're stuck and not sure whether to file a bug, request a feature, or just ask a question, see SUPPORT.md for a guide to the right channel.
Getting started#
- Fork the repository and clone your fork.
- Install dev dependencies. The project uses
uvfor dependency management, most contributors invoke tools through it: - Create a branch:
git checkout -b your-feature-name. - Make your changes, then run the full check suite, every command must pass before you open a PR:
- Open a pull request against the
mainbranch.
Three contribution paths#
Most contributions to safelint fall into one of three categories. Each has its own checklist below; pick the one that matches what you're adding.
| You want to add… | Read this | What you'll touch |
|---|---|---|
| A new safety rule (e.g. another Power-of-Ten check) | The "Adding a new rule" checklist below | src/safelint/rules/, core/config.py, docs/configuration/rules.md, every bundled AI-client doc |
| A new AI-client integration (e.g. JetBrains AI Assistant, a brand-new IDE) | Adding an AI client, full walkthrough with a worked example | src/safelint/_skill_install.py (one ClientSpec append), src/safelint/skill_files/<client>/, tests/test_skill_install.py, docs/ai-clients/, src/safelint/skill_files/README.md, CHANGELOG.md |
| A new language safelint can lint (e.g. Kotlin, Ruby, Swift) | Adding a language, full walkthrough | src/safelint/languages/<lang>.py, the Tree-sitter grammar dependency in pyproject.toml, per-rule audit, tests/, docs/configuration/, every bundled AI-client doc |
The architecture for each path is open-ended: rules go into one tuple (ALL_RULES), AI clients go into one tuple (_CLIENT_SPECS), languages go into one registry (safelint.languages._REGISTRY). Drift-detection tests parametrise over those registries automatically, when you add a new rule, the bundled-doc-coverage tests fail until every registered AI client mentions the new rule. When you add a new AI client, the parametrised tests fail until its bundled doc mentions every existing rule + extension. The architecture pulls you toward consistency rather than relying on memory.
Adding a new rule#
Each rule lives in its own class inside src/safelint/rules/. Follow this checklist:
- Subclass
BaseRuleand implementcheck_file(filepath, tree) -> list[Violation]. Thetreeargument is a Tree-sitter parse tree, not a Pythonasttree; see existing rules for traversal patterns (walk,lineno,node_textinsafelint.languages._node_utils). - Set a unique
name(the human-friendly key users put in their config, e.g.function_length) andcode(the short identifier, e.g.SAFE105, pick the next free number in the appropriateSAFE9xxband). - (Default suffices for now) Each rule inherits
BaseRule.language = ("python",). Leave it alone unless your rule applies to a non-Python language too; that's only relevant once a second language is registered (see Adding a language). The engine consults this tuple in_run_rulesand skips rules whoselanguagedoesn't include the active file'sLanguageDefinition.name. - Add the rule's class to
ALL_RULESinsrc/safelint/rules/__init__.py. The position in this tuple is the execution order, keep cheap structural rules first, expensive dataflow rules last. - Add default config to
DEFAULTS["rules"]insrc/safelint/core/config.py. Setenabled: falseif your rule is expensive or false-positive-prone (the dataflow rules do this). - Write tests covering both the violation case and the clean case. Coverage must stay at ≥97% (the project's enforced threshold).
- Document the rule in
docs/configuration/rules.mdunder the matching category, following the format used by existing rules. - Update every bundled AI-client artefact under
src/safelint/skill_files/to mention the new rule code + name. The drift-detection tests (test_skill_documents_every_active_rule[<client>]) parametrise over the registry and will fail CI for every client whose docs are missing the new rule.
Self-imposed constraints: safelint runs itself in CI, so your new rule's source code must obey the same rules it enforces: function_length ≤ 60, nesting_depth ≤ 2, complexity ≤ 10, etc. If safelint check src/ fails on the new rule's own implementation, that's a meta-bug; refactor the rule's code until it passes.
Adding a new AI client#
Fourteen clients ship today (Claude Code, Cursor, GitHub Copilot, Gemini, Windsurf, codex, Continue.dev, Cline, aider, Trae, Antigravity, Zed, Warp, Kiro). Adding the next is one ClientSpec append plus a bundled artefact and ~10 regression tests, no control-flow changes elsewhere. The full walkthrough with a worked example lives in Adding an AI client. The short version:
- Decide on the bundled artefact shape. Every client installs a single file under
skill_files/<client>/<filename>(Claude Code isclaude/SKILL.md, Cursor iscursor/safelint.mdc, and so on); the sharedlanguages/addendums at the bundle root are looked up on demand viasafelint skill path. - Write the bundled file by adapting an existing one (Cursor's
cursor/safelint.mdcis a clean single-file template). Strip MDC frontmatter if your client doesn't use it. - Append one
ClientSpecentry to_CLIENT_SPECSinsrc/safelint/_skill_install.py, fields:name,display_name,artefact_label,cwd_markers,home_markers,install_relpath,bundled_relpath,restart_hint,usage_hint,documentation_relpaths. If your client also writes to a cross-agent shared file (like codex'sAGENTS.md), setsecondary_install_relpathandsecondary_install_section_markers, the install primitives handle the rest. - Mirror an existing client's test block (10–12 tests covering install / symlink / force / overwrite / auto-detect / CLI routing / path-print / peer-exclusion).
- Update the docs-site AI-client pages under
docs/ai-clients/(index table, new per-client page, manual-install, mkdocs nav),src/safelint/skill_files/README.md, andCHANGELOG.md.
The security guards (symlink refusal at the secondary destination, skill remove --path PATH install-shape validation, etc.) live in _skill_install.py and apply to your client automatically, no per-client implementation needed.
Adding a new language#
Nine languages are registered today (Python, JavaScript, TypeScript, Java, Rust, Go, PHP, C, C++). Adding a new one (Kotlin, etc.) needs three things: a Tree-sitter grammar package for the language, a per-language module exposing node-type constants, and a per-rule audit to identify which rules port cleanly. The full walkthrough with a worked TypeScript example lives in Adding a language - note its "scattered enumerations" step (the docs that list every language's extension / grammar / --language value outside the headline tables), which is the easiest part to miss.
Ground rules#
- SafeLint must pass itself. Zero blocking violations on
src/at all times. Runsafelint check src/before opening a PR. - Tests are not optional. Every rule needs at least one test for the violation case and one for the clean case.
- No breaking changes to rule names or codes. Downstream users pin to these in config files and CI scripts. If a rule needs to change, add a new one and deprecate the old.
- Keep rules focused. One rule, one concern. If you find yourself adding multiple
ifbranches for different failure modes, it is probably two rules. - Defaults must be safe. New rules should default to
enabled: falseif they have a high false-positive rate or are expensive to run. Let users opt in. - Bump the version with your work. The version bump in
pyproject.tomlis what triggers the automated release; add your CHANGELOG entries under## [Unreleased]and leave the heading undated. See Releasing for the full flow.
Reporting issues#
Open an issue at github.com/shelkesays/safelint/issues with:
- The SafeLint version (
pip show safelint) - The rule code that fired (e.g.
SAFE101) - A minimal code example that reproduces the problem
- Whether it is a false positive or a missed violation