{"id":50974907,"url":"https://github.com/agusmdev/smart-linter","last_synced_at":"2026-06-19T06:33:04.616Z","repository":{"id":351369921,"uuid":"1210685568","full_name":"agusmdev/smart-linter","owner":"agusmdev","description":"Pluggable Python linter with heuristic rules, ruff-compatible output, and AI harness integration","archived":false,"fork":false,"pushed_at":"2026-04-17T12:12:18.000Z","size":413,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-26T21:30:24.405Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/agusmdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-14T16:53:23.000Z","updated_at":"2026-04-17T10:28:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/agusmdev/smart-linter","commit_stats":null,"previous_names":["agusmdev/smart-linter"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/agusmdev/smart-linter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fsmart-linter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fsmart-linter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fsmart-linter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fsmart-linter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agusmdev","download_url":"https://codeload.github.com/agusmdev/smart-linter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agusmdev%2Fsmart-linter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34520431,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-19T06:33:03.731Z","updated_at":"2026-06-19T06:33:04.593Z","avatar_url":"https://github.com/agusmdev.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Smart Linter\n\nPluggable Python linter with heuristic rules, ruff-compatible output, and AI harness integration. Detects code quality issues that ruff cannot catch using AST analysis and heuristics.\n\n## Why Smart Linter?\n\nRuff is excellent for style and syntax linting, but it runs entirely in Rust and does **not** support third-party plugins. Smart Linter fills the gap for heuristic rules that require Python-level AST analysis:\n\n- Sync blocking calls inside async FastAPI endpoints\n- SQL injection via string formatting\n- Hardcoded secrets and dangerous deserialization\n- Silent exception swallowing\n- Late binding closures, mutable class attributes, and other logic bugs\n\n## Install\n\nRequires [uv](https://docs.astral.sh/uv/). No pip needed.\n\n```bash\n# Run directly without installing (one-shot)\nuvx --from \"git+https://github.com/agusmdev/smart-linter.git\" smart-linter check src/\n\n# Or install persistently\nuv tool install \"git+https://github.com/agusmdev/smart-linter.git\"\nsmart-linter check src/\n\n# With MCP server support (for AI agents)\nuv tool install \"git+https://github.com/agusmdev/smart-linter.git[mcp]\"\n```\n\n## Quick Start\n\n```bash\n# Check your project\nsmart-linter check src/\n\n# Get ruff-compatible JSON (merge with ruff output in CI)\nsmart-linter check src/ --format json\n\n# Get AI-parseable fix suggestions\nsmart-linter check src/ --fix\n\n# See what would change\nsmart-linter check src/ --diff\n\n# Check only specific rules\nsmart-linter check src/ --select ASYNC001 ERR001 SEC001\n\n# Ignore specific rules\nsmart-linter check src/ --ignore PERF001 PERF002\n\n# List available rules\nsmart-linter list-rules .\n```\n\n## Rules\n\n| Rule ID | Severity | Category | Description |\n|---------|----------|----------|-------------|\n| ASYNC001 | WARNING | async | Sync blocking calls in async FastAPI endpoints |\n| ERR001 | WARNING | error-handling | Exception handlers that silently swallow errors |\n| PERF001 | INFO | performance | String `+=` concatenation inside loops (O(n²)) |\n| PERF002 | INFO | performance | Unnecessary list comprehension (use generator) |\n| SEC001 | ERROR | security | SQL injection via string formatting in queries |\n| SEC002 | ERROR | security | Hardcoded secrets/credentials in source code |\n| SEC003 | ERROR | security | Dangerous deserialization (pickle, yaml.load) |\n| RES001 | WARNING | reliability | Resources opened without context manager |\n| MAIN001 | WARNING | bug | Mutable class attributes shared across instances |\n| MAIN002 | WARNING | bug | Late binding closure in loops |\n| LOGIC001 | WARNING | bug | Always-true or always-false conditions |\n\n### ASYNC001 — Sync blocking calls in async endpoints\n\nDetects synchronous blocking calls inside `async def` FastAPI route endpoints, including **transitive** calls through helper functions.\n\n```python\n# BAD — smart-linter flags this\n@app.get(\"/users\")\nasync def get_users():\n    response = requests.get(\"https://api.example.com/users\")  # ASYNC001\n    return response.json()\n\n# GOOD — use async alternatives\n@app.get(\"/users\")\nasync def get_users():\n    async with httpx.AsyncClient() as client:\n        response = await client.get(\"https://api.example.com/users\")\n    return response.json()\n```\n\n**Detects:** `requests.*`, sync `httpx.*`, `time.sleep()`, `subprocess.run()`, `os.system()`, `open()`, `Path(...).read_text()`, `os.path.exists()`, `input()`.\n\n**Safe wrappers** (not flagged): `asyncio.to_thread()`, `run_in_threadpool()`, `loop.run_in_executor()`, `anyio.to_thread.run_sync()`.\n\n### ERR001 — Silent exception swallowing\n\nFlags `except` handlers whose bodies contain no meaningful action — no logging, re-raise, return, or diagnostic calls.\n\n```python\n# BAD — silently swallows all errors\ntry:\n    process_payment(order)\nexcept Exception:\n    pass  # ERR001\n\n# GOOD — log or re-raise\ntry:\n    process_payment(order)\nexcept Exception:\n    logger.exception(\"Payment processing failed\")\n    raise\n```\n\n### PERF001 — String concatenation in loops\n\nString `+=` inside loops creates O(n²) new strings on each iteration.\n\n```python\n# BAD — O(n²) string building\nresult = \"\"\nfor item in items:\n    result += f\"Item: {item}\\n\"  # PERF001\n\n# GOOD — use list + join\nparts = []\nfor item in items:\n    parts.append(f\"Item: {item}\\n\")\nresult = \"\".join(parts)\n```\n\n### PERF002 — Unnecessary list comprehension\n\nList comprehensions passed to functions that accept any iterable waste memory.\n\n```python\n# BAD — creates an intermediate list\nhas_match = any([x \u003e threshold for x in data])  # PERF002\n\n# GOOD — generator expression\nhas_match = any(x \u003e threshold for x in data)\n```\n\n### SEC001 — SQL injection via string formatting\n\nDetects SQL queries constructed with f-strings, `%` formatting, string concatenation, or `.format()`.\n\n```python\n# BAD — SQL injection vulnerability\ncursor.execute(f\"SELECT * FROM users WHERE id = {user_id}\")  # SEC001\n\n# GOOD — parameterized query\ncursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))\n```\n\n### SEC002 — Hardcoded secrets\n\nFlags string assignments to variables matching secret patterns (`password`, `api_key`, `secret`, `token`, `private_key`, etc.).\n\n```python\n# BAD — secret in source code\ndatabase_url = \"postgres://admin:s3cret@prod-db:5432/mydb\"  # SEC002\n\n# GOOD — use environment variables\ndatabase_url = os.environ[\"DATABASE_URL\"]\n```\n\n### SEC003 — Dangerous deserialization\n\nFlags calls to `pickle.loads()`, `yaml.load()` without `SafeLoader`, `marshal.loads()`, `shelve.open()`, and `jsonpickle.decode()`.\n\n```python\n# BAD — arbitrary code execution\ndata = pickle.loads(user_input)  # SEC003\n\n# GOOD — use safe alternatives\ndata = json.loads(user_input)\n```\n\n### RES001 — Resources without context managers\n\nFlags resource-creating calls assigned to variables outside `with` blocks.\n\n```python\n# BAD — resource leak if exception occurs before close()\nf = open(\"data.txt\")  # RES001\ncontent = f.read()\nf.close()\n\n# GOOD — context manager ensures cleanup\nwith open(\"data.txt\") as f:\n    content = f.read()\n```\n\n### MAIN001 — Mutable class attributes\n\nMutable class-level `list`, `dict`, or `set` attributes are shared across all instances.\n\n```python\n# BAD — all instances share the same list\nclass EventHandler:\n    events = []  # MAIN001\n\n    def add_event(self, event):\n        self.events.append(event)\n\n# GOOD — define in __init__\nclass EventHandler:\n    def __init__(self):\n        self.events = []\n```\n\n### MAIN002 — Late binding closure in loops\n\nClosures defined inside loops that reference loop variables by reference instead of by value.\n\n```python\n# BAD — all lambdas return the last value of i\nfuncs = []\nfor i in range(10):\n    funcs.append(lambda: i)  # MAIN002\n\n# GOOD — capture by value with default argument\nfuncs = []\nfor i in range(10):\n    funcs.append(lambda i=i: i)\n```\n\n### LOGIC001 — Always-true/false conditions\n\nTautological or contradictory boolean conditions that likely indicate a logic error.\n\n```python\n# BAD — always True\nif status == status:  # LOGIC001\n    deploy()\n\n# BAD — always False\nif user.active and not user.active:  # LOGIC001\n    revoke_access()\n\n# GOOD — compare different variables\nif status == expected_status:\n    deploy()\n```\n\n## Real-World Catches\n\nIssues found by Smart Linter in popular open-source projects:\n\n### ASYNC001 in GPUSTack (6.4k stars)\n\n[`gpustack/routes/worker/filesystem.py`](https://github.com/gpustack/gpustack/blob/main/gpustack/routes/worker/filesystem.py) — `os.path.exists()` is a blocking filesystem call inside an async endpoint:\n\n```python\n@router.get(\"/files/model-config\")\nasync def read_model_config(path: str = Query(...)):\n    validated_path = validate_path_security(path)\n    if not os.path.exists(validated_path):  # ASYNC001\n        raise HTTPException(status_code=404, detail=f\"File not found: {path}\")\n```\n\n### ASYNC001 in GPT Researcher (19k+ stars)\n\n[`backend/server/app.py`](https://github.com/assafelovic/gpt-researcher/blob/main/backend/server/app.py) — `os.path.exists()` and `open()` in async endpoints:\n\n```python\n@app.get(\"/\", response_class=HTMLResponse)\nasync def serve_frontend():\n    if not os.path.exists(index_path):  # ASYNC001\n        raise HTTPException(status_code=404, detail=\"Frontend not found\")\n    with open(index_path, \"r\", encoding=\"utf-8\") as f:  # ASYNC001\n        content = f.read()\n    return HTMLResponse(content=content)\n```\n\n### ASYNC001 in FastGPT (22k+ stars)\n\n[`plugins/model/pdf-marker/api_mp.py`](https://github.com/labring/FastGPT/blob/main/plugins/model/pdf-marker/api_mp.py) — blocking `open()`, `os.makedirs()`, and `time.time()` in async file upload:\n\n```python\n@app.post(\"/v1/parse/file\")\nasync def read_file(file: UploadFile = File(...)):\n    start_time = time.time()  # ASYNC001\n    os.makedirs(temp_dir, exist_ok=True)  # ASYNC001\n    with open(temp_file_path, \"wb\") as temp_file:  # ASYNC001\n        temp_file.write(await file.read())\n```\n\n### ASYNC001 in MONAI Label (Project MONAI, 700+ stars)\n\n[`monailabel/endpoints/logs.py`](https://github.com/Project-MONAI/MONAILabel/blob/main/monailabel/endpoints/logs.py) — `subprocess.run()` in an async GPU info endpoint:\n\n```python\n@router.get(\"/gpu\", summary=\"Get GPU Info (nvidia-smi)\")\nasync def gpu_info(user: User = Depends(...)):\n    response = subprocess.run([\"nvidia-smi\"], stdout=subprocess.PIPE)  # ASYNC001\n    return response.stdout.decode(\"utf-8\")\n```\n\n### ASYNC001 in LlamaFS (6k+ stars)\n\n[`server.py`](https://github.com/iyaja/llama-fs/blob/main/server.py) — `os.path.exists()` in async batch endpoint:\n\n```python\n@app.post(\"/batch\")\nasync def batch(request: Request):\n    path = request.path\n    if not os.path.exists(path):  # ASYNC001\n        raise HTTPException(status_code=400, detail=\"Path does not exist\")\n```\n\n### ERR001 in Netflix Dispatch (6.4k stars)\n\n[`src/dispatch/database/service.py`](https://github.com/Netflix/dispatch/blob/main/src/dispatch/database/service.py) — silently swallowing exceptions when inspecting SQLAlchemy queries:\n\n```python\ntry:\n    if hasattr(compile_state, \"_join_entities\"):\n        for mapper in compile_state._join_entities:\n            if hasattr(mapper, \"class_\"):\n                if mapper.class_ not in models:\n                    models.append(mapper.class_)\nexcept Exception:\n    pass  # ERR001 — silently swallows all errors\n```\n\n### PERF001 in NVIDIA TensorRT-LLM\n\n[`examples/scaffolding/contrib/DeepResearch/TavilyMCP/travily.py`](https://github.com/NVIDIA/TensorRT-LLM/blob/main/examples/scaffolding/contrib/DeepResearch/TavilyMCP/travily.py) — string concatenation in a loop building search results:\n\n```python\n@mcp.tool()\nasync def tavily_search(query: str) -\u003e str:\n    response = client.search(query=query)\n    search_result = \"\"\n    for result in response[\"results\"]:\n        search_result += f\"{result['title']}: {result['content']}\\n\"  # PERF001\n    return search_result\n```\n\n## Running Alongside Ruff\n\nSmart Linter outputs ruff-compatible JSON. Merge both in your CI pipeline:\n\n```bash\nruff check src/ --output-format json \u003e lint-results.json\nsmart-linter check src/ --format json \u003e\u003e lint-results.json\n```\n\nAdd to `ruff.toml` so `# noqa: ASYNC001` comments work:\n```toml\n[lint]\nexternal = [\"ASYNC001\", \"ERR001\", \"PERF001\", \"PERF002\", \"SEC001\", \"SEC002\", \"SEC003\", \"RES001\", \"MAIN001\", \"MAIN002\", \"LOGIC001\"]\n```\n\n## Configuration\n\nAdd to `pyproject.toml`:\n\n```toml\n[tool.smart-linter]\n# Enable specific rules (default: \"all\")\n# select = [\"ASYNC001\", \"SEC001\"]\n\n# Ignore specific rules\n# ignore = []\n\n# Custom rule paths (module:class format)\n# custom-rules = [\"my_package.rules:MyCustomRule\"]\n\n# Minimum severity: \"error\", \"warning\", \"info\"\n# min-severity = \"info\"\n```\n\n## Writing Custom Rules\n\n```python\nimport ast\nfrom typing import ClassVar\nfrom smart_linter.models import Rule, Violation, Location, Severity, FixSuggestion\n\nclass NoHardcodedSecretsRule(Rule):\n    id: ClassVar[str] = \"CUSTOM001\"\n    description: ClassVar[str] = \"Potential hardcoded secret detected\"\n    severity: ClassVar[Severity] = Severity.ERROR\n\n    def check(self, tree, filename: str = \"\") -\u003e list[Violation]:\n        violations = []\n        for node in ast.walk(tree):\n            if isinstance(node, ast.Assign):\n                for target in node.targets:\n                    if isinstance(target, ast.Name) and \"password\" in target.id.lower():\n                        violations.append(\n                            Violation(\n                                rule_id=self.id,\n                                message=f\"Hardcoded secret in variable `{target.id}`\",\n                                location=Location(row=node.lineno, column=node.col_offset),\n                                severity=self.severity,\n                                fix=FixSuggestion(\n                                    title=\"Move secret to environment variable\",\n                                    replacement=f'{target.id} = os.environ[\"{target.id.upper()}\"]',\n                                    explanation=\"Never hardcode secrets. Use environment variables or a secrets manager.\",\n                                ),\n                                filename=filename,\n                            )\n                        )\n        return violations\n```\n\nRegister in `pyproject.toml`:\n```toml\n[tool.smart-linter]\ncustom-rules = [\"my_package.lint_rules:NoHardcodedSecretsRule\"]\n```\n\nOr distribute as a package with entry points:\n```toml\n[project.entry-points.\"smart_linter.rules\"]\nsecrets = \"my_package.lint_rules:NoHardcodedSecretsRule\"\n```\n\n## Pre-commit Hook\n\nAdd to `.pre-commit-config.yaml`:\n```yaml\nrepos:\n  - repo: local\n    hooks:\n      - id: smart-linter\n        name: smart-linter\n        entry: smart-linter check\n        language: system\n        types: [python]\n```\n\n## AI Agent Integration\n\nSmart Linter is designed for AI coding agents (Claude, Cursor, Copilot, etc.):\n\n```bash\n# Get structured fix data for AI consumption\nsmart-linter check src/ --fix\n\n# Each violation includes:\n# - fix_title: What to do\n# - fix_replacement: Exact code to use\n# - fix_explanation: Why this is wrong\n```\n\n### MCP Server (Native AI Integration)\n\nSmart Linter ships an MCP server for direct integration with AI agents:\n\n```bash\nuv tool install \"git+https://github.com/agusmdev/smart-linter.git[mcp]\"\n```\n\nAdd to your MCP client config (e.g. `.claude/settings.json`):\n```json\n{\n  \"mcpServers\": {\n    \"smart-linter\": {\n      \"command\": \"uvx\",\n      \"args\": [\"--from\", \"git+https://github.com/agusmdev/smart-linter.git[mcp]\", \"python\", \"-m\", \"smart_linter.mcp_server\"]\n    }\n  }\n}\n```\n\n### Available MCP Tools\n\n| Tool | Description |\n|------|-------------|\n| `check_files` | Lint files and return violations with fix suggestions |\n| `list_rules` | List all available rules |\n| `explain_rule` | Get detailed explanation of a specific rule |\n\n### Example MCP Usage\n\nWhen connected, AI agents can directly:\n- `check_files(paths=[\"src/api.py\"])` — returns JSON violations with fixes\n- `check_files(paths=[\"src/\"], format=\"fixes\")` — returns AI-parseable fix data\n- `explain_rule(rule_id=\"ASYNC001\")` — returns detection patterns and fix strategies\n\n## Output Formats\n\n### Text (default)\n```\nsrc/api.py:15:16: WARNING ASYNC001 Blocking sync call `requests.get()` in async FastAPI endpoint `get_users`\n  💡 Replace `requests.get()` with async alternative\n     Suggestion: await async_client.get(...)\n```\n\n### JSON (ruff-compatible)\n```json\n[{\n  \"code\": \"ASYNC001\",\n  \"message\": \"Blocking sync call `requests.get()` in async FastAPI endpoint `get_users`\",\n  \"severity\": \"warning\",\n  \"filename\": \"src/api.py\",\n  \"location\": {\"row\": 15, \"column\": 16},\n  \"fix\": {\n    \"applicability\": \"unsafe\",\n    \"message\": \"Replace `requests.get()` with async alternative\",\n    \"edits\": [{\"content\": \"await async_client.get(...)\", ...}]\n  }\n}]\n```\n\n### SARIF (GitHub Code Scanning)\n```bash\nsmart-linter check src/ --format sarif \u003e results.sarif\n```\n\n## Requirements\n\n- [uv](https://docs.astral.sh/uv/)\n- Python 3.11+ (managed by uv automatically)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagusmdev%2Fsmart-linter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagusmdev%2Fsmart-linter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagusmdev%2Fsmart-linter/lists"}