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, CONFIGURATION.md, every bundled AI-client doc |
| A new AI-client integration (e.g. JetBrains AI Assistant, a brand-new IDE) | ADDING_AN_AI_CLIENT.md, full walkthrough with a worked example |
src/safelint/_skill_install.py (one ClientSpec append), src/safelint/skill_files/<client>/, tests/test_skill_install.py, AI_CLIENTS.md, skill_files/README.md, CHANGELOG.md |
| A new language safelint can lint (e.g. TypeScript, Go) | ADDING_A_LANGUAGE.md, full walkthrough |
src/safelint/languages/<lang>.py, the Tree-sitter grammar dependency in pyproject.toml, per-rule audit, tests/, CONFIGURATION.md, 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 (seeADDING_A_LANGUAGE.md). 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
CONFIGURATION.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#
Twelve clients ship today (Claude Code, Cursor, GitHub Copilot, Gemini, Windsurf, codex, Continue.dev, Cline, aider, Trae, Antigravity, Zed). 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.md. The short version:
- Decide on the bundled artefact shape (single file under
skill_files/<client>/<filename>is the common case; directory-tree underskill_files/is Claude's pattern). - 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. - Add the new directory name to
_PEER_CLIENT_DIRSso it doesn't leak into Claude's directory-tree install. - Mirror an existing client's test block (10–12 tests covering install / symlink / force / overwrite / auto-detect / CLI routing / path-print / peer-exclusion).
- Update
AI_CLIENTS.md,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#
One language is registered today (Python). Adding a new one (TypeScript, Go, Rust, 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.md.
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.
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