Metadata-Version: 2.4
Name: safelint
Version: 2.1.0rc1
Summary: Engineering safety lint rules and pre-commit integration for modern Python, JavaScript, TypeScript, and Java (with Spring Boot framework preset) codebases
License-Expression: MIT
Project-URL: Homepage, https://github.com/shelkesays/safelint
Project-URL: Repository, https://github.com/shelkesays/safelint
Project-URL: Issues, https://github.com/shelkesays/safelint/issues
Keywords: lint,pre-commit,static-analysis,safety,python,javascript,typescript,java,spring-boot
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: tree-sitter>=0.23.0
Provides-Extra: python
Requires-Dist: tree-sitter-python>=0.23.0; extra == "python"
Provides-Extra: javascript
Requires-Dist: tree-sitter-javascript>=0.23.0; extra == "javascript"
Provides-Extra: typescript
Requires-Dist: tree-sitter-javascript>=0.23.0; extra == "typescript"
Requires-Dist: tree-sitter-typescript>=0.23.0; extra == "typescript"
Provides-Extra: java
Requires-Dist: tree-sitter-java>=0.23.0; extra == "java"
Provides-Extra: all
Requires-Dist: tree-sitter-python>=0.23.0; extra == "all"
Requires-Dist: tree-sitter-javascript>=0.23.0; extra == "all"
Requires-Dist: tree-sitter-typescript>=0.23.0; extra == "all"
Requires-Dist: tree-sitter-java>=0.23.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pre-commit>=4.5.1; extra == "dev"
Requires-Dist: pytest>=9.0.2; extra == "dev"
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
Requires-Dist: ruff>=0.15.8; extra == "dev"
Requires-Dist: ty>=0.0.26; extra == "dev"
Requires-Dist: safelint[all]; extra == "dev"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.6.0; extra == "docs"
Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
Dynamic: license-file

<div align="center">
  <img src="https://raw.githubusercontent.com/shelkesays/safelint/main/logo/safelint-logo.svg" alt="SafeLint logo" width="240" />
</div>


<div align="center">

[![CI](https://github.com/shelkesays/safelint/actions/workflows/ci.yml/badge.svg)](https://github.com/shelkesays/safelint/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/safelint)](https://pypi.org/project/safelint/)
[![Python](https://img.shields.io/pypi/pyversions/safelint)](https://pypi.org/project/safelint/)
[![Docs](https://img.shields.io/badge/docs-shelkesays.github.io%2Fsafelint-blue)](https://shelkesays.github.io/safelint/)

</div>

> **📖 Full documentation:** <https://shelkesays.github.io/safelint/>, searchable, navigable, with a per-client install guide for each of the twelve supported AI agents. The README below is the repo home; the docs site is the user reference.

SafeLint is a configurable static analysis tool that enforces safety-critical coding practices inspired by Gerard J. Holzmann's "Power of Ten" rules at NASA/JPL.

Originally designed for mission-critical systems, these principles apply to any modern codebase - and are especially valuable when code is written fast, reviewed quickly, or generated by AI.

**Languages supported:**

| Language | Extensions | Notes |
|---|---|---|
| **Python** | `.py`, `.pyw` |  |
| **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 selectable via [`[tool.safelint.javascript] runtime = "..."`](https://shelkesays.github.io/safelint/configuration/toml/#javascript-runtime-presets). |
| **TypeScript** (incl. **TSX** + **AssemblyScript**) | `.ts`, `.tsx`, `.as` | Reuses the JS rule implementations end-to-end with TS-specific handling for 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` | 16 cross-language rules port cleanly; 4 Spring-specific structural rules (`SAFE901-904`) target Spring annotation patterns. Per-framework *defaults* are selectable via [`[tool.safelint.java] framework = "..."`](https://shelkesays.github.io/safelint/languages/java/#framework-presets). New in v2.1.0rc1 (release candidate; install with `pip install --pre 'safelint[java]==2.1.0rc1'`). |

**Rule coverage:** 16 cross-language rules apply across all four languages; 2 are Python-only (`bare_except`, `global_state`); 1 applies to Python / JS / TS but not Java (`global_mutation`: Java has no clean analogue, deferred); 1 is JavaScript-family-only (`wide_scope_declaration` for `var`); and 4 are Java + Spring Boot only (`spring_field_injection`, `spring_missing_transactional`, `spring_unvalidated_input`, `spring_async_checked_exception`).

**Planned future languages** (working-priority order, no timelines committed): Go, Rust, C, C++, PHP. SafeLint's registry-driven design makes each addition incremental; see the [language-coverage roadmap](https://shelkesays.github.io/safelint/configuration/rules/#planned), and [Adding a language](https://shelkesays.github.io/safelint/contributing/adding-a-language/) if you'd like to help.

SafeLint integrates with pre-commit and CI pipelines to prevent unsafe code from entering your codebase.

## Why SafeLint?

Fast-moving codebases - whether written by humans under pressure or generated by AI tools - tend to drift toward the same failure patterns:

- Unbounded loops
- Silent error handling
- Hidden side effects
- Poor resource management

SafeLint catches these early, automatically, regardless of who wrote the code.

## Philosophy

> "When it really counts, it may be worth going the extra mile and living within stricter limits than may be desirable."
> - Gerard J. Holzmann, NASA/JPL

---

## Power of Ten - adapted for modern languages

In 1987, Holzmann wrote ten rules for spacecraft software at NASA/JPL. Nearly four decades later, the same failure patterns appear in every fast-moving codebase. SafeLint is those ten rules, adapted for modern languages (Python, JavaScript, TypeScript, and Java with the Spring Boot framework preset today; further languages in future releases) and automated.

| # | Holzmann's Rule | SafeLint Rule | Code |
|---|---|---|---|
| 1 | No complex control flow - no `goto`, no deep recursion | `nesting_depth`, `complexity` | [SAFE102](https://shelkesays.github.io/safelint/configuration/rules/#safe102-nesting_depth), [SAFE104](https://shelkesays.github.io/safelint/configuration/rules/#safe104-complexity) |
| 2 | All loops must have a fixed upper bound | `unbounded_loops` | [SAFE501](https://shelkesays.github.io/safelint/configuration/rules/#safe501-unbounded_loops) |
| 3 | No dynamic memory allocation after startup | `resource_lifecycle` | [SAFE401](https://shelkesays.github.io/safelint/configuration/rules/#safe401-resource_lifecycle), *adapted: managed runtimes allocate dynamically by default; the rule becomes "acquired resources must have guaranteed cleanup"* |
| 4 | Functions must fit on one printed page | `function_length` | [SAFE101](https://shelkesays.github.io/safelint/configuration/rules/#safe101-function_length) |
| 5 | Use at least two assertions per function | `missing_assertions` | [SAFE601](https://shelkesays.github.io/safelint/configuration/rules/#safe601-missing_assertions) |
| 6 | Declare variables at the smallest scope | `wide_scope_declaration` | [SAFE305](https://shelkesays.github.io/safelint/configuration/rules/#safe305-wide_scope_declaration) *(JavaScript, `var` → `let` / `const`; Python's lexical scoping handles this natively)* |
| 7 | Check the return value of every non-void function | `return_value_ignored`, `bare_except`, `empty_except`, `logging_on_error` | [SAFE802](https://shelkesays.github.io/safelint/configuration/rules/#safe802-return_value_ignored), [SAFE201](https://shelkesays.github.io/safelint/configuration/rules/#safe201-bare_except), [SAFE202](https://shelkesays.github.io/safelint/configuration/rules/#safe202-empty_except), [SAFE203](https://shelkesays.github.io/safelint/configuration/rules/#safe203-logging_on_error) |
| 8 | Limit preprocessor use | - | *(not applicable to Python or JavaScript)* |
| 9 | Restrict pointer use - no chained indirection | `null_dereference` | [SAFE803](https://shelkesays.github.io/safelint/configuration/rules/#safe803-null_dereference) |
| 10 | Compile with all warnings; use static analysis | SafeLint itself | - |

Original paper: [spinroot.com/gerard/pdf/P10.pdf](https://spinroot.com/gerard/pdf/P10.pdf)

SafeLint also ships several rules that go beyond Holzmann's original ten: modern additions for state purity (`global_state` / `global_mutation` for shared-state writes), hidden side effects (`side_effects_hidden` / `side_effects` for I/O lurking behind pure-sounding names), dataflow taint (`tainted_sink` for unsanitised input reaching `eval` / `exec` / equivalents), and test discipline (`test_existence` / `test_coupling` for paired-test enforcement). See the [full rules reference](https://shelkesays.github.io/safelint/configuration/rules/) for every rule with examples and configuration.

---

## Installation

SafeLint ships **every** per-language grammar as an opt-in extra. The base install includes only the engine, no grammars, so a Python-only project doesn't pay for JS/TS grammars, a Go/JS project doesn't pay for the Python grammar, and so on. Pick the extras that match the languages you actually lint:

```bash
pip install 'safelint[python]'         # adds .py, .pyw
pip install 'safelint[javascript]'     # adds .js, .mjs, .cjs
pip install 'safelint[typescript]'     # adds .ts, .tsx, .as (and bundles JS too)
pip install --pre 'safelint[all]==2.1.0rc1'         # every supported language (RC pin needed until v2.1.0 GA so [all] actually includes Java)

# Multiple extras compose, for monorepos:
pip install 'safelint[python,javascript]'
pip install 'safelint[python,typescript]'
```

`pip install safelint` (no extras) installs only the engine. If you run it that way, safelint will emit a one-line hint on first run telling you which extra to add for the files it found. Same story if you mix-and-match, e.g. running safelint with `[python]` installed on a directory containing `.ts` files will skip the `.ts` files with a clear `pip install` hint while continuing to lint the supported (`.py`) files. **If every candidate file ends up skipped** (the typical TS-only / JS-only run against a bare base install), safelint exits with code 2 as a setup error instead so CI / pre-commit can't silently report clean on an un-linted run.

---

## Usage

**Discover the CLI surface** (ruff-style help and version):

```bash
safelint --help              # or: safelint help, safelint -h
safelint help check          # subcommand-specific help
safelint --version           # or: safelint version, safelint -V
```

**Check modified files** (default, only files changed since last commit):

```bash
safelint check src/
```

**Check all files** (full scan, e.g. in CI):

```bash
safelint check src/ --all-files
```

**Check specific files** (pre-commit style):

```bash
safelint src/mymodule.py src/utils.py
```

**Fail on warnings too** (useful in CI):

```bash
safelint check src/ --all-files --fail-on=warning
```

**Run in CI mode** (warnings become blocking):

```bash
safelint check src/ --all-files --mode=ci
```

**Ignore specific rules for one run:**

```bash
safelint check src/ --ignore SAFE203 --ignore side_effects
```

**Machine-readable output for tooling consumers (editors, CI, the Claude Code skill):**

```bash
safelint check src/ --format=json     # stable JSON schema
safelint check src/ --format=sarif    # SARIF 2.1.0 (GitHub code scanning, etc.)
```

**Lint un-saved buffer contents from stdin (editor mode):**

```bash
cat my_module.py | safelint --stdin --stdin-filename my_module.py --format=json
```

`--stdin-filename` drives language detection by extension and is shown as the violation file path. Combine with `--format=json` so the editor can parse the result.

**Disable the lint-result cache:**

```bash
safelint check src/ --no-cache       # otherwise: ~instant re-runs on unchanged files
```

By default safelint memoises rule output keyed on `sha256(source + engine config + filepath)` in a `.safelint_cache/` directory next to your config file (mirroring `.pytest_cache`). The filepath is folded in so two files with identical contents under different paths get separate entries, and `Violation.filepath` always reflects the current call. Add `.safelint_cache/` to `.gitignore`.

---

## Pre-commit integration

Add this to your `.pre-commit-config.yaml`, pick the `additional_dependencies` line that matches the languages your repo contains:

```yaml
repos:
  - repo: https://github.com/shelkesays/safelint
    rev: v2.1.0rc1  # replace with the latest release tag (v2.1.0rc1 is required for Java support)
    hooks:
      - id: safelint
        # Required in v2.0.0+, pick the line for the language(s) your repo lints:
        additional_dependencies: ['safelint[python]']               # Python-only repo
        # additional_dependencies: ['safelint[javascript]']         # JS-only repo (Node / browser / Deno / Cloudflare Workers / Bun)
        # additional_dependencies: ['safelint[typescript]']         # TypeScript repo (bundles tree-sitter-javascript too)
        # additional_dependencies: ['safelint[java]==2.1.0rc1']     # Java repo, RC pin until v2.1.0 GA (Spring Boot via [tool.safelint.java] framework = "spring-boot")
        # additional_dependencies: ['safelint[python,javascript]']  # mixed monorepo
        # additional_dependencies: ['safelint[all]==2.1.0rc1']      # kitchen-sink, RC pin until v2.1.0 GA so [all] actually includes Java

        args: [--fail-on=error]   # or --fail-on=warning for stricter CI
        files: ^src/              # optional, scope to a directory
```

### One hook, every language

The same `id: safelint` handles Python, JavaScript, TypeScript, and Java; there's no `safelint-python` / `safelint-js` / `safelint-ts` / `safelint-java` split. The published hook spec sets `types_or: [python, javascript, ts, tsx, java]` so pre-commit automatically routes the right files to safelint; the engine then dispatches each file to its language-specific rule implementations based on extension. **AssemblyScript `.as` files are not matched by that default `types_or` list**, pre-commit's `identify` library has no `as` filetype tag, so AS users must override `types_or` with a permissive tag `.as` files *actually* carry (e.g. `types_or: [text]`) and use `files: \.(ts|tsx|as)$` to scope the match. (`types_or: []` doesn't work, pre-commit treats an empty list as "no tag matches", not "filter disabled".) **Every flag (`--fail-on`, `--mode`, `--ignore`, `--format`, `--statistics`) and every config option behaves identically across languages.** The only per-project lever is the `additional_dependencies` extra, that's what tells pre-commit which tree-sitter grammar(s) to install into the hook's isolated environment.

### What happens if you forget the extra

The `additional_dependencies` line is genuinely required in v2.0.0+, including for Python projects, which used to work without it. Forgetting it doesn't silently pass: safelint exits with code **2**, which pre-commit reports as **Failed** (red). When *every* passed file is skipped for a missing grammar (the usual "forgot the extra" case) it prints a single error line with the exact fix:

```text
safelint: error: no files linted, every file pre-commit passed had a grammar that isn't installed, add 'safelint[python]' to additional_dependencies in your .pre-commit-config.yaml
```

In a *mixed* run, where some files lint successfully and others are skipped for a missing grammar, safelint additionally prints one `safelint: warning: skipping .X files …` line per missing grammar as actionable context for the skipped files. The all-skipped case omits that warning because the error above already carries the same install hint.

See [Exit codes](https://shelkesays.github.io/safelint/configuration/cli/#exit-codes) for the full table.

Then install the hooks:

```bash
pre-commit install
```

SafeLint will now run on every `git commit` and block the commit if it finds errors.

---

## What it checks

SafeLint ships **24 rules** across the Holzmann safety categories. **14 are on by default**; **10 are opt-in** (the dataflow trio is opt-in for performance reasons; the test-discipline and assertion rules are opt-in because they only make sense in projects that follow paired-test conventions; the four Java + Spring Boot rules are opt-in under vanilla and flipped on automatically by the `spring-boot` framework preset).

### Default-on rules (14)

| Code | Rule | Severity | What it flags |
|---|---|---|---|
| [SAFE101](https://shelkesays.github.io/safelint/configuration/rules/#safe101-function_length) | `function_length` | error | Functions longer than 60 lines |
| [SAFE102](https://shelkesays.github.io/safelint/configuration/rules/#safe102-nesting_depth) | `nesting_depth` | error | Control flow nested more than 2 levels deep |
| [SAFE103](https://shelkesays.github.io/safelint/configuration/rules/#safe103-max_arguments) | `max_arguments` | error | Functions with more than 7 parameters |
| [SAFE104](https://shelkesays.github.io/safelint/configuration/rules/#safe104-complexity) | `complexity` | error | Functions with high cyclomatic complexity |
| [SAFE201](https://shelkesays.github.io/safelint/configuration/rules/#safe201-bare_except) | `bare_except` | error | `except:` with no exception type *(Python-only)* |
| [SAFE202](https://shelkesays.github.io/safelint/configuration/rules/#safe202-empty_except) | `empty_except` | error | `except` / `catch` blocks that do nothing |
| [SAFE203](https://shelkesays.github.io/safelint/configuration/rules/#safe203-logging_on_error) | `logging_on_error` | warning | Except / catch blocks that swallow errors silently |
| [SAFE301](https://shelkesays.github.io/safelint/configuration/rules/#safe301-global_state) | `global_state` | warning | Use of the `global` keyword inside functions *(Python-only)* |
| [SAFE302](https://shelkesays.github.io/safelint/configuration/rules/#safe302-global_mutation) | `global_mutation` | error | Writing to global variables inside functions |
| [SAFE303](https://shelkesays.github.io/safelint/configuration/rules/#safe303-side_effects_hidden) | `side_effects_hidden` | error | Pure-looking functions that secretly do I/O |
| [SAFE304](https://shelkesays.github.io/safelint/configuration/rules/#safe304-side_effects) | `side_effects` | warning | Functions that call `print`, `open`, etc. without signalling intent |
| [SAFE305](https://shelkesays.github.io/safelint/configuration/rules/#safe305-wide_scope_declaration) | `wide_scope_declaration` | warning | `var` declarations, prefer `let` / `const` *(JavaScript / TypeScript only)* |
| [SAFE401](https://shelkesays.github.io/safelint/configuration/rules/#safe401-resource_lifecycle) | `resource_lifecycle` | error | Files or connections opened outside a `with` block (Python) or without paired cleanup (JS / TS) |
| [SAFE501](https://shelkesays.github.io/safelint/configuration/rules/#safe501-unbounded_loops) | `unbounded_loops` | warning | `while True` loops with no `break` |

### Opt-in rules (10): enable via `[tool.safelint.rules.<name>] enabled = true`

| Code | Rule | Severity | What it flags |
|---|---|---|---|
| [SAFE601](https://shelkesays.github.io/safelint/configuration/rules/#safe601-missing_assertions) | `missing_assertions` | warning | Functions with fewer than 2 assertions (Holzmann rule #5) |
| [SAFE701](https://shelkesays.github.io/safelint/configuration/rules/#safe701-test_existence) | `test_existence` | warning | Source files without a paired test file |
| [SAFE702](https://shelkesays.github.io/safelint/configuration/rules/#safe702-test_coupling) | `test_coupling` | warning | Source changes in a commit without a matching test change |
| [SAFE801](https://shelkesays.github.io/safelint/configuration/rules/#safe801-tainted_sink) | `tainted_sink` | error | User input flowing into `eval`, `exec`, `subprocess`, etc. without sanitization *(dataflow)* |
| [SAFE802](https://shelkesays.github.io/safelint/configuration/rules/#safe802-return_value_ignored) | `return_value_ignored` | warning | Discarding the return value of calls like `subprocess.run` or `file.write` *(dataflow)* |
| [SAFE803](https://shelkesays.github.io/safelint/configuration/rules/#safe803-null_dereference) | `null_dereference` | error | Chaining methods directly on calls that can return `None`, e.g. `d.get("key").strip()` *(dataflow)* |
| [SAFE901](https://shelkesays.github.io/safelint/configuration/rules/#safe901-spring_field_injection) | `spring_field_injection` | warning | `@Autowired` on a field; prefer constructor injection *(Java + Spring Boot only; auto-enabled by the `spring-boot` framework preset)* |
| [SAFE902](https://shelkesays.github.io/safelint/configuration/rules/#safe902-spring_missing_transactional) | `spring_missing_transactional` | error | `@Service` / `@Component` method doing 2+ repository writes without `@Transactional` *(Java + Spring Boot only; auto-enabled by the `spring-boot` framework preset)* |
| [SAFE903](https://shelkesays.github.io/safelint/configuration/rules/#safe903-spring_unvalidated_input) | `spring_unvalidated_input` | error | `@RequestBody` / `@ModelAttribute` parameter without `@Valid` / `@Validated` *(Java + Spring Boot only; auto-enabled by the `spring-boot` framework preset)* |
| [SAFE904](https://shelkesays.github.io/safelint/configuration/rules/#safe904-spring_async_checked_exception) | `spring_async_checked_exception` | warning | `@Async` method with a `throws` clause (Spring's executor swallows it) *(Java + Spring Boot only; auto-enabled by the `spring-boot` framework preset)* |

Plus **`SAFE004 unused_suppression`** (engine meta-check, on by default), flags stale `# nosafe` directives that no longer suppress anything. Disable globally via `ignore = ["SAFE004"]` if undesired.

For per-rule defaults, configuration knobs, and full examples, see the [Rules reference](https://shelkesays.github.io/safelint/configuration/rules/).

---

## Suppressing violations inline

Add a `# nosafe` comment to suppress a violation on a specific line without changing global config.

**Suppress all violations on a line:**
```python
result = eval(user_input)  # nosafe
```

**Suppress a specific rule by code:**
```python
while True:  # nosafe: SAFE501
    ...
```

**Suppress by rule name:**
```python
while True:  # nosafe: unbounded_loops
    ...
```

**Suppress multiple rules at once:**
```python
def get_data(conn, q, p1, p2, p3, p4, p5, p6):  # nosafe: SAFE101, SAFE103
    ...
```

When at least one violation is suppressed, the CLI summary reports a per-code breakdown (e.g. `(2 SAFE501, 1 SAFE304 suppressed)`) so suppressions remain visible and auditable. Use `# nosafe` sparingly, it's for line-level exceptions only. For broader suppression use the config-level options:

```toml
# pyproject.toml
[tool.safelint]
ignore = ["SAFE203", "side_effects"]          # suppress project-wide

[tool.safelint.per_file_ignores]
"tests/**" = ["SAFE101", "SAFE103"]           # suppress only for matching files
```

See [Inline suppression](https://shelkesays.github.io/safelint/configuration/suppression/#inline-nosafe), [Global ignore list](https://shelkesays.github.io/safelint/configuration/toml/#global-ignore-list), and [Per-file ignore list](https://shelkesays.github.io/safelint/configuration/toml/#per-file-ignore-list) for full reference.

---

## Configuration

SafeLint is configured via `[tool.safelint]` in your `pyproject.toml`, or a standalone `safelint.toml` file at your project root. When both exist in the same directory, **`safelint.toml` wins**, its values override anything in `[tool.safelint]`, matching ruff's `ruff.toml` / `pyproject.toml` precedence. See the [Configuration reference](https://shelkesays.github.io/safelint/configuration/) for all options, defaults, and examples.

Highlights:

- **Incremental ignore lists**, use `extend_ignore` / `extend_per_file_ignores` to grow the defaults instead of replacing them (mirrors ruff's `extend-select` ergonomics).
- **`--statistics` flag**, print a per-rule violation-count table at the end of a run (`safelint check src/ --statistics`).
- **`SAFE004 unused_suppression`**, automatically warns about stale `# nosafe` directives that no longer suppress anything. Disable globally via `ignore = ["SAFE004"]` if undesired.
- **No `--fix` flag**, SafeLint is review-only by design. Editor integrations may surface suggestions as code actions, but every edit is user-confirmed.

Ready-to-copy samples:

- [examples/sample.pyproject.toml](examples/sample.pyproject.toml), `[tool.safelint]` block for an existing pyproject.toml
- [examples/sample.safelint.toml](examples/sample.safelint.toml), standalone `safelint.toml` (no `[tool.safelint]` wrapper)

---

## Editor / agent integrations

### AI-client skills (12 clients supported)

```bash
pip install safelint
safelint skill install          # auto-detects which AI client(s) you use
```

**Twelve AI clients are supported today**, every one of them runs through the same `safelint skill` command surface, so you only need to learn one workflow:

| Client | Marker safelint looks for | Where the skill lands |
|---|---|---|
| Claude Code | `CLAUDE.md`, `.claude/`, `.claude.json` | `~/.claude/skills/safelint/SKILL.md` (or `<cwd>/...`) |
| Cursor | `.cursor/`, `.cursorrules` | `~/.cursor/rules/safelint.mdc` (or `<cwd>/...`) |
| GitHub Copilot | `.github/copilot-instructions.md`, `.github/copilot/`, `.github/instructions/` | `.github/copilot-instructions.md` |
| Gemini | `GEMINI.md`, `.gemini/` | `GEMINI.md` at repo root |
| Windsurf | `.windsurfrules`, `.codeium/` | `.windsurfrules` at repo root |
| codex | `.codex/`, `AGENTS.md` | `.codex/instructions.md` (and a section in `AGENTS.md` if present) |
| Continue.dev | `.continue/`, `.continuerc`, `.continuerc.json` | `.continue/rules/safelint.md` |
| Cline | `.clinerules/` | `.clinerules/safelint.md` |
| aider | `.aider.conf.yml`, `CONVENTIONS.md` | `CONVENTIONS.md` (you wire it in via `read:` in `.aider.conf.yml`) |
| Trae | `.trae/` | `.trae/rules/safelint.md` |
| Antigravity | `.antigravity/` | `.antigravity/rules/safelint.md` |
| Zed | `.rules`, `.zed/` | `.rules` at repo root |

`safelint skill install` (with no flags) is `--client auto` under the hood: it looks for any of the markers above in your current directory, falls back to your home directory if cwd has none, and installs whatever it finds. If your project uses two clients (e.g. Claude *and* Cursor), it installs both, no flag needed.

**After install,** restart the AI client (or reload its window) and ask things like *"run safelint"*, *"lint my changes with safelint"*, or *"do a Power-of-Ten review on src/api/auth.py"*. The skill takes care of the rest: it invokes safelint with structured JSON output, groups violations by file, and offers to walk through fixes.

**After `pip install --upgrade safelint`**, your installed skill files are still the *old* version, the wheel's bundled files moved on without them. To catch up:

```bash
safelint skill status        # shows fresh / differs per detected install (exit 1 if anything differs)
safelint skill update        # re-installs only the ones that drifted (no-op if everything is fresh)
safelint skill remove        # auto-detects and removes every install
```

`safelint skill remove` accepts a few filter flags: `--symlink` keeps copy installs and only removes the ones created with `--symlink` (i.e., the skill file is a symlink pointing back at the bundled wheel, handy for skill developers); `--path PATH` removes one specific install location safelint's auto-detect didn't find; `--dry-run` previews everything without touching disk.

For explicit control (`--client <name>` for any of the twelve), per-client setup, project-vs-user-scope semantics, symlink mode for skill development, post-upgrade workflow, and troubleshooting, see the [AI client integrations guide](https://shelkesays.github.io/safelint/ai-clients/). To add support for a new AI client (the registry is open-ended), follow the contributor walkthrough in [Adding a new AI client](https://shelkesays.github.io/safelint/contributing/adding-an-ai-client/).

### Other integration points

- **Stdin mode**, `safelint --stdin --stdin-filename PATH --format json` lints unsaved buffer contents fed via stdin. Designed for editor extensions (VSCode plugin, custom LSP wrappers).
- **JSON / SARIF output**, `--format json` and `--format sarif` emit stable, machine-readable documents. The JSON schema is documented in the [JSON output schema](https://shelkesays.github.io/safelint/json-schema/). SARIF output is GitHub code-scanning compatible.
- **Column-precise positions**, every violation carries `lineno`, `end_lineno`, `column_start`, `column_end` (1-based, half-open). Maps directly to LSP / VSCode `Range` and SARIF `region.endColumn`. Synthetic violations (e.g. `test_existence`) leave column fields `null`; editors should treat that as "underline the whole line".
- **Advisory suggestions**, every violation may carry a `suggestions` array with one-line descriptions and `TextEdit` ranges. **Editor integrations must never apply these automatically**, every edit goes through user confirmation. SARIF output uses the spec's native `fixes[]` block for the same data. SafeLint will never ship a `--fix` flag.

---

## Development

```bash
# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run the linter on itself
safelint check src/
```

## Releasing to PyPI (Trusted Publishing)

This project publishes to PyPI via GitHub Actions using PyPI Trusted Publishing (OIDC). Do not use local `uv publish` username/password auth.

One-time setup:

1. In PyPI, open your project → **Manage** → **Publishing** → **Add a trusted publisher**.
2. Use:
   - Owner: `shelkesays`
   - Repository: `safelint`
   - Workflow: `publish.yml`
   - Environment: `pypi`
3. In GitHub, create an environment named `pypi` in **Settings → Environments**.

Release flow:

```bash
# 1) bump version in pyproject.toml
# 2) commit and push
git tag vX.Y.Z
git push origin vX.Y.Z
```

Pushing the version tag triggers `.github/workflows/publish.yml`, which builds and publishes to PyPI.

---

## Getting help

If you hit a bug, want a feature, or just don't know which flag to reach for, see [SUPPORT.md](SUPPORT.md), it lists where to ask each kind of question and what to include in a bug report so we can help quickly.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on bug reports, adding new rules, AI clients, or languages, and opening pull requests. By participating in this project you agree to abide by the [Code of Conduct](CODE_OF_CONDUCT.md).

## Citing

If you use safelint in academic work, see [CITATION.cff](CITATION.cff) for the canonical citation metadata. GitHub renders this file as a "Cite this repository" button on the repo home page.
