Skip to content

SafeLint#

Holzmann "Power of Ten" safety lint rules for modern Python, JavaScript, TypeScript, Java (with Spring Boot framework preset), Rust, Go, PHP, C, and C++, adapted from C/C++ aerospace conventions to bound function length, nesting depth, cyclomatic complexity, error-handling discipline, hidden side effects, dataflow taint, and other classes of bugs that a typical linter (ruff, pylint, mypy, ESLint, SpotBugs, Checkstyle, clippy, go vet) doesn't reach.

SafeLint complements your existing linters. Where ruff handles style and pylint catches general defects, SafeLint enforces a focused set of safety rules: the kind you'd want in code that has to be reviewable, testable, and predictably-terminating. See The Power of Ten, adapted for how each of Holzmann's ten rules maps onto SafeLint's checks across the supported languages. It's a CLI, a pre-commit hook, a JSON / SARIF emitter for editor and CI consumers, and an AI-client skill that fourteen agents (Claude Code, Cursor, GitHub Copilot, Gemini, Windsurf, codex, Continue.dev, Cline, aider, Trae, Antigravity, Zed, Warp, Kiro) speak.

Currently supported languages#

Language Extensions Notes
Python .py, .pyw Default language; the rule set was originally designed for Python and ports unchanged. Web-framework defaults are switchable via the [tool.safelint.python] framework = "..." preset (django / flask / fastapi) plus an orthogonal pydantic = true, which selectively enable the shared SAFE905-907 rules (Django / FastAPI enable all three; Flask enables debug_mode_enabled + unvalidated_request_input; pydantic = true enables mass_assignment). See Python.
JavaScript .js, .mjs, .cjs Runtime-agnostic source analysis covering Node.js, browser, Deno, Cloudflare Workers, Bun, and any WASM-hosted JS engine. Per-runtime defaults are switchable via [tool.safelint.javascript] runtime = "...".
TypeScript (including AssemblyScript) .ts, .tsx, .as Reuses the JS rule implementations end-to-end with TS-specific handling for type-only constructs (generics, as casts, non-null assertions, declare global blocks, etc.). Shares JS runtime presets since TS compiles to JS.
Java (with Spring Boot framework preset) .java 20 rules apply (the 15 cross-language core plus 5 shared with Python / JS / TS); 4 Spring-specific structural rules (SAFE901-904) target Spring annotation patterns. Per-framework defaults are switchable via [tool.safelint.java] framework = "...".
Rust .rs 15 cross-language rules port cleanly; 11 Rust-only rules cover panic placement, lock poisoning, unsafe block documentation, truncating as casts, silent Err arms, dangerous mem::* ops, needless mut, unchecked arithmetic on integer params, broad .unwrap() outside tests, interior-mutable statics, plus the empty-Err / unlogged-Err Rust analogues of empty_except / logging_on_error. Recognises both inline #[cfg(test)] mod tests and Cargo tests/<stem>.rs integration-test conventions. See Rust. New in v2.2.0.
Go .go 16 cross-language rules apply (the 13 all-language core plus global_mutation / dynamic_code_execution / resource_lifecycle); 2 Go-only rules cover Go-idiom patterns: empty_error_check (the empty if err != nil {} swallow) and panic_calls_outside_tests (18 rules total for Go). Headline adaptations: the bare for {} infinite loop, the sibling foo_test.go convention, the _ = f() explicit-discard exemption, and the defer x.Close() resource form. See Go. New in v2.5.0.
PHP .php 21 rules apply, the widest coverage of any non-Python language (only bare_except and wide_scope_declaration are skipped). First non-Python home for global_state (PHP has a literal global keyword). Headline highlights: the @-operator error-suppression idiom, superglobal taint sources ($_GET / $_POST / $_REQUEST) feeding tainted_sink, and the break N; / continue N; multi-level loop forms. Web-framework defaults are switchable via the [tool.safelint.php] framework = "laravel" preset, which enables the shared SAFE905-907 rules. See PHP. New in v2.6.0.
C .c, .h 21 rules apply: the 16 cross-language ports plus 5 new C-family rules (shared with C++) - Holzmann's original language gets clauses every other language adapts away, expressed literally: nonlocal_jumps (goto / setjmp, rule 1), dynamic_allocation (malloc family, rule 3), complex_macro + conditional_compilation (the preprocessor, rule 8), and restricted_pointers (rule 9). nonlocal_jumps is enabled (warning); the other four are opt-in. .h headers lint as C. See C. New in v2.7.0.
C++ .cpp, .cxx, .cc, .hpp, .hxx, .hh 26 rules apply: the cross-language ports, the five C-family rules widened to C and C++, the three try / catch / throw rules (bare_except via catch (...), empty_except, logging_on_error), plus 2 new C++-only rules: raw_new_delete and dangerous_casts. Plain .h lints as C; use .hpp / .hxx / .hh for C++ headers. See C++. New in v2.8.0.

Rule coverage - 50 rules total, scoped per language:

Applies to # Rules
All nine (Python, JavaScript, TypeScript, Java, Rust, Go, PHP, C, C++) 13 the cross-language core: function_length, nesting_depth, max_arguments, complexity, no_recursion, side_effects_hidden, side_effects, unbounded_loops, blanket_suppression, test_existence, test_coupling, tainted_sink, return_value_ignored
Python / JS / TS / Java / Rust / PHP / C / C++ (not Go) 1 missing_assertions
Python / JS / TS / Java / Rust / PHP (not Go, not C, not C++) 1 null_dereference
Python / JS / TS / Java / Go / PHP / C / C++ (not Rust) 2 global_mutation, dynamic_code_execution
Python / JS / TS / Java / Go / PHP (not Rust, not C, not C++) 1 resource_lifecycle
Python / JS / TS / Java / PHP / C++ 2 empty_except, logging_on_error
Python + PHP 1 global_state
Python + C++ 1 bare_except (Python except: / C++ catch (...))
JavaScript family (JS / TS) 1 wide_scope_declaration (varlet / const)
Java + Spring Boot only 4 spring_* (SAFE901-904)
Python / PHP framework presets only 3 debug_mode_enabled (SAFE905), mass_assignment (SAFE906), unvalidated_request_input (SAFE907); enabled by the Python / PHP framework presets (Django / FastAPI / Laravel: all three; Flask: SAFE905 + SAFE907; pydantic = true: SAFE906)
Rust only 11 needless_mut, unchecked_arithmetic_on_input, panic_macros_outside_tests, lock_poisoning_ignored, silent_result_discard, unlogged_error_branch, result_unwrap_outside_tests, dangerous_mem_ops, interior_mutable_static, truncating_as_cast, undocumented_unsafe
Go only 2 empty_error_check, panic_calls_outside_tests
C family (C and C++) 5 nonlocal_jumps, dynamic_allocation, complex_macro, conditional_compilation, restricted_pointers
C++ only 2 raw_new_delete, dangerous_casts

Rules are skipped per language where the semantics don't translate - e.g. Go has no try/catch, global keyword, var hoisting, production assertion idiom, or chained-nullable idiom; Rust's Result / Option / Drop model covers its skips with Rust-specific replacements; C skips try/catch rules, global_state, wide_scope_declaration, and (documented gaps) resource_lifecycle / null_dereference.

See the language-coverage roadmap for future languages.

Quick start#

pip install 'safelint[python]'         # adds .py, .pyw
pip install 'safelint[javascript]'     # adds .js, .mjs, .cjs
pip install 'safelint[typescript]'     # adds .ts, .tsx, .as (also bundles JS)
pip install 'safelint[java]'           # adds .java (Spring Boot framework preset available)
pip install 'safelint[rust]'           # adds .rs
pip install 'safelint[go]'             # adds .go
pip install 'safelint[php]'            # adds .php
pip install 'safelint[c]'              # adds .c, .h
pip install 'safelint[cpp]'            # adds .cpp, .cxx, .cc, .hpp, .hxx, .hh
pip install 'safelint[all]'            # every supported language
pip install 'safelint[python,rust]'    # multiple extras compose (e.g. PyO3 / maturin)
# uv add 'safelint[typescript]' works the same way.

safelint check src/                    # lint a directory
safelint check --all-files .           # lint everything (default is git-modified only)
safelint check --format json src/      # machine-readable output for editors / CI

Pick the extras that match the languages you actually lint. Every grammar ships as an opt-in extra so a Python-only project never pays for JavaScript / TypeScript grammars, a Go/JS-only project never pays for the Python grammar, and so on. pip install safelint alone installs only the engine; safelint emits a one-line install hint on first run telling you which extra to add for the files it found.

Where to go from here#

  • Configuration: every CLI flag, every rule, every TOML option. Start here once you've installed.
  • Pre-commit: drop a 10-line block into .pre-commit-config.yaml and SafeLint runs on every git commit.
  • AI client integrations: install the skill into Claude Code / Cursor / 10 other agents with one command, then ask "run safelint" in the chat.
  • JSON output schema: for editor and plugin authors building on top of safelint --format json.
  • Contributing: three contribution paths (rule, AI client, language), each with its own walkthrough.
  • Changelog: what shipped when.

What SafeLint won't do#

SafeLint is a review tool, not a refactor tool. It surfaces violations and may emit advisory Suggestions in JSON output for editor integrations, but it never auto-fixes. There is no --fix flag and there never will be: every change to your code goes through your eyes.