agent-rules-lint 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,27 @@
1
+ name: Rule request
2
+ description: Suggest a new lint rule
3
+ title: "[Rule] "
4
+ labels:
5
+ - rule-request
6
+ body:
7
+ - type: textarea
8
+ id: problem
9
+ attributes:
10
+ label: Problem
11
+ description: What agent instruction problem should this catch?
12
+ validations:
13
+ required: true
14
+ - type: textarea
15
+ id: example
16
+ attributes:
17
+ label: Example
18
+ description: Provide a short instruction snippet that should trigger the rule.
19
+ validations:
20
+ required: true
21
+ - type: textarea
22
+ id: expected
23
+ attributes:
24
+ label: Expected finding
25
+ description: What should the tool report?
26
+ validations:
27
+ required: true
@@ -0,0 +1,11 @@
1
+ ## Summary
2
+
3
+ -
4
+
5
+ ## Checks
6
+
7
+ - [ ] `python -m unittest discover -s tests`
8
+ - [ ] `python -m agent_rules_lint .`
9
+
10
+ ## Notes
11
+
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ os: [ubuntu-latest, windows-latest, macos-latest]
18
+ python-version: ["3.10", "3.14"]
19
+ runs-on: ${{ matrix.os }}
20
+ steps:
21
+ - uses: actions/checkout@v7.0.0
22
+ - uses: actions/setup-python@v6.3.0
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ - name: Install package
26
+ run: python -m pip install -e .
27
+ - name: Run tests
28
+ run: python -m unittest discover -s tests -v
29
+ - name: Compile source
30
+ run: python -m compileall -q agent_rules_lint tests
31
+ - name: Run linter on itself
32
+ run: python -m agent_rules_lint .
33
+
34
+ package:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v7.0.0
38
+ - uses: actions/setup-python@v6.3.0
39
+ with:
40
+ python-version: "3.12"
41
+ - run: python -m pip install build twine
42
+ - run: python -m build
43
+ - run: python -m twine check dist/*
44
+ - run: python -m pip install dist/*.whl
45
+ - run: agent-rules-lint --version
@@ -0,0 +1,40 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v7.0.0
15
+ - uses: actions/setup-python@v6.3.0
16
+ with:
17
+ python-version: "3.12"
18
+ - run: python -m pip install build
19
+ - run: python -m build
20
+ - uses: actions/upload-artifact@v7.0.1
21
+ with:
22
+ name: python-package
23
+ path: dist/
24
+ if-no-files-found: error
25
+
26
+ publish:
27
+ needs: build
28
+ runs-on: ubuntu-latest
29
+ environment:
30
+ name: pypi
31
+ url: https://pypi.org/p/agent-rules-lint
32
+ permissions:
33
+ id-token: write
34
+ steps:
35
+ - uses: actions/download-artifact@v8.0.0
36
+ with:
37
+ name: python-package
38
+ path: dist/
39
+ - name: Publish distributions to PyPI
40
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ .mypy_cache/
5
+ .venv/
6
+ venv/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.pyc
11
+ .DS_Store
12
+ Thumbs.db
@@ -0,0 +1,31 @@
1
+ # Agent Instructions
2
+
3
+ ## Purpose
4
+
5
+ Maintain `agent-rules-lint`, a dependency-free Python CLI for linting AI agent instruction files.
6
+
7
+ ## Scope
8
+
9
+ These instructions apply to code, tests, documentation, and GitHub workflow files in this repository.
10
+
11
+ ## Commands
12
+
13
+ Run the standard checks before publishing:
14
+
15
+ ```bash
16
+ python -m unittest discover -s tests
17
+ python -m agent_rules_lint .
18
+ ```
19
+
20
+ ## Safety
21
+
22
+ - Do not add network calls to the default lint path.
23
+ - Do not add model calls to the default lint path.
24
+ - Do not commit API keys, access tokens, cookies, private credentials, or generated build output.
25
+ - Keep checks deterministic so they are safe for CI.
26
+
27
+ ## Style
28
+
29
+ - Prefer standard-library Python.
30
+ - Keep rule messages short and actionable.
31
+ - Add tests when changing lint behavior.
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to `agent-rules-lint` are documented here.
4
+
5
+ ## [0.1.1] - 2026-07-14
6
+
7
+ ### Added
8
+
9
+ - PyPI installation instructions for `uvx`, `pipx`, and `pip`.
10
+ - Credential-free PyPI publishing with GitHub Trusted Publishing.
11
+ - Cross-platform CI for Python 3.10 and 3.14.
12
+ - Package build, metadata, wheel installation, and CLI smoke checks.
13
+
14
+ ### Fixed
15
+
16
+ - Project metadata links now point to the current `Uky0Yang` repository.
17
+
18
+ ## [0.1.0] - 2026-07-01
19
+
20
+ - Initial public release.
21
+
22
+ [0.1.1]: https://github.com/Uky0Yang/agent-rules-lint/compare/v0.1.0...v0.1.1
23
+ [0.1.0]: https://github.com/Uky0Yang/agent-rules-lint/releases/tag/v0.1.0
@@ -0,0 +1,34 @@
1
+ # Contributing
2
+
3
+ Thanks for helping improve `agent-rules-lint`.
4
+
5
+ ## Good Contributions
6
+
7
+ - New checks for common agent instruction mistakes
8
+ - Better false-positive handling
9
+ - Tests for real-world `AGENTS.md`, `CLAUDE.md`, Cursor, or Copilot instruction patterns
10
+ - Documentation examples
11
+ - CI integration examples
12
+
13
+ ## Before Opening a PR
14
+
15
+ Run:
16
+
17
+ ```bash
18
+ python -m unittest discover -s tests -v
19
+ python -m compileall -q agent_rules_lint tests
20
+ python -m agent_rules_lint .
21
+ ```
22
+
23
+ If your change adds or changes a lint rule, include tests.
24
+
25
+ ## Rule Design
26
+
27
+ Rules should be:
28
+
29
+ - Easy to explain
30
+ - Low noise
31
+ - Useful without sending data to a model
32
+ - Safe for public and private repositories
33
+
34
+ Avoid checks that require personal taste or project-specific assumptions.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ukyo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-rules-lint
3
+ Version: 0.1.1
4
+ Summary: Lint AI agent instruction files such as AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions.
5
+ Project-URL: Homepage, https://github.com/Uky0Yang/agent-rules-lint
6
+ Project-URL: Repository, https://github.com/Uky0Yang/agent-rules-lint
7
+ Project-URL: Issues, https://github.com/Uky0Yang/agent-rules-lint/issues
8
+ Project-URL: Changelog, https://github.com/Uky0Yang/agent-rules-lint/blob/main/CHANGELOG.md
9
+ Author: Ukyo
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,claude-code,copilot,cursor,developer-tools,lint,llm
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Quality Assurance
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+
28
+ # agent-rules-lint
29
+
30
+ [![PyPI](https://img.shields.io/pypi/v/agent-rules-lint)](https://pypi.org/project/agent-rules-lint/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/agent-rules-lint)](https://pypi.org/project/agent-rules-lint/)
32
+ [![CI](https://github.com/Uky0Yang/agent-rules-lint/actions/workflows/ci.yml/badge.svg)](https://github.com/Uky0Yang/agent-rules-lint/actions/workflows/ci.yml)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+
35
+ Lint AI agent instruction files before they confuse your coding agent.
36
+
37
+ `agent-rules-lint` is a small, dependency-free Python CLI that scans files such as `AGENTS.md`, `CLAUDE.md`, Cursor rules, and GitHub Copilot instructions for common quality and safety problems.
38
+
39
+ ## Why
40
+
41
+ Recent high-growth developer repos show a clear pattern: teams are adding more agent skills, rule files, memory files, and coding-agent playbooks. The missing piece is a quick quality gate for those instructions.
42
+
43
+ Bad agent instructions are expensive. They can be too long, vague, contradictory, unsafe, or accidentally include secrets. This tool gives you a fast local and CI check before those rules affect real work.
44
+
45
+ ## What It Checks
46
+
47
+ - Common instruction files:
48
+ - `AGENTS.md`
49
+ - `CLAUDE.md`
50
+ - `GEMINI.md`
51
+ - `.cursorrules`
52
+ - `.cursor/rules/*.md`
53
+ - `.github/copilot-instructions.md`
54
+ - `.github/instructions/*.md`
55
+ - Secret-like values
56
+ - Risky shell and Git commands
57
+ - Prompt-injection-like language
58
+ - Missing title
59
+ - Missing purpose, scope, command, or safety guidance
60
+ - Vague language
61
+ - Files that are too long for practical agent use
62
+ - A small set of obvious contradictory rules
63
+
64
+ ## Install
65
+
66
+ Run it without installing:
67
+
68
+ ```bash
69
+ uvx agent-rules-lint .
70
+ ```
71
+
72
+ Or install it as an isolated CLI:
73
+
74
+ ```bash
75
+ pipx install agent-rules-lint
76
+ agent-rules-lint .
77
+ ```
78
+
79
+ Standard pip installation also works:
80
+
81
+ ```bash
82
+ python -m pip install agent-rules-lint
83
+ ```
84
+
85
+ For local development:
86
+
87
+ ```bash
88
+ python -m pip install -e .
89
+ ```
90
+
91
+ ## Usage
92
+
93
+ Scan the current repository:
94
+
95
+ ```bash
96
+ agent-rules-lint .
97
+ ```
98
+
99
+ Return JSON for CI or custom reports:
100
+
101
+ ```bash
102
+ agent-rules-lint . --format json
103
+ ```
104
+
105
+ Fail CI on warnings:
106
+
107
+ ```bash
108
+ agent-rules-lint . --warnings-as-errors
109
+ ```
110
+
111
+ Change the long-file threshold:
112
+
113
+ ```bash
114
+ agent-rules-lint . --max-chars 8000
115
+ ```
116
+
117
+ ## Example Output
118
+
119
+ ```text
120
+ agent-rules-lint checked 1 file(s)
121
+ errors=0 warnings=1 info=2
122
+
123
+ Files:
124
+ - AGENTS.md
125
+
126
+ Findings:
127
+ [warning] AGENTS.md:42 risky-command: Risky command should include explicit safeguards or approval rules.
128
+ [info] AGENTS.md missing-safety: Consider adding a clear safety section.
129
+ ```
130
+
131
+ ## CI
132
+
133
+ Add this to GitHub Actions:
134
+
135
+ ```yaml
136
+ name: Lint agent rules
137
+
138
+ on:
139
+ pull_request:
140
+ push:
141
+ branches:
142
+ - main
143
+
144
+ jobs:
145
+ lint-agent-rules:
146
+ runs-on: ubuntu-latest
147
+ steps:
148
+ - uses: actions/checkout@v7.0.0
149
+ - uses: actions/setup-python@v6.3.0
150
+ with:
151
+ python-version: "3.12"
152
+ - run: pipx run agent-rules-lint . --warnings-as-errors
153
+ ```
154
+
155
+ ## Design Principles
156
+
157
+ - No model calls
158
+ - No network calls
159
+ - No dependencies
160
+ - Safe to run in CI
161
+ - Findings should be specific enough to act on
162
+ - Warnings should improve instruction quality, not enforce one writing style
163
+
164
+ ## Current Limitations
165
+
166
+ - Conflict detection is intentionally conservative and only catches obvious pairs.
167
+ - It does not estimate real tokenizer counts; it uses character length as a practical proxy.
168
+ - It does not validate whether an instruction is correct for a specific tool.
169
+
170
+ ## Development
171
+
172
+ ```bash
173
+ python -m pip install -e .
174
+ python -m unittest discover -s tests -v
175
+ python -m agent_rules_lint .
176
+ ```
177
+
178
+ ## Agent OSS Toolkit
179
+
180
+ This project is part of a small toolkit for building and launching agent-ready open-source repositories:
181
+
182
+ - [agent-repo-kit](https://github.com/Uky0Yang/agent-repo-kit): scaffold launch-ready, AI-agent-friendly repositories
183
+ - [oss-launch-check](https://github.com/Uky0Yang/oss-launch-check): audit whether a repository is ready to launch as open source
184
+ - [repo-context-card](https://github.com/Uky0Yang/repo-context-card): generate compact repository context cards for coding agents
185
+ - [agent-rules-lint](https://github.com/Uky0Yang/agent-rules-lint): lint AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions
186
+ - [awesome-ai-agents-zh](https://github.com/Uky0Yang/awesome-ai-agents-zh): Chinese AI Agents / MCP / AI DevTools directory
187
+
188
+ ## Contributing
189
+
190
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,167 @@
1
+ # agent-rules-lint
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/agent-rules-lint)](https://pypi.org/project/agent-rules-lint/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/agent-rules-lint)](https://pypi.org/project/agent-rules-lint/)
5
+ [![CI](https://github.com/Uky0Yang/agent-rules-lint/actions/workflows/ci.yml/badge.svg)](https://github.com/Uky0Yang/agent-rules-lint/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ Lint AI agent instruction files before they confuse your coding agent.
9
+
10
+ `agent-rules-lint` is a small, dependency-free Python CLI that scans files such as `AGENTS.md`, `CLAUDE.md`, Cursor rules, and GitHub Copilot instructions for common quality and safety problems.
11
+
12
+ ## Why
13
+
14
+ Recent high-growth developer repos show a clear pattern: teams are adding more agent skills, rule files, memory files, and coding-agent playbooks. The missing piece is a quick quality gate for those instructions.
15
+
16
+ Bad agent instructions are expensive. They can be too long, vague, contradictory, unsafe, or accidentally include secrets. This tool gives you a fast local and CI check before those rules affect real work.
17
+
18
+ ## What It Checks
19
+
20
+ - Common instruction files:
21
+ - `AGENTS.md`
22
+ - `CLAUDE.md`
23
+ - `GEMINI.md`
24
+ - `.cursorrules`
25
+ - `.cursor/rules/*.md`
26
+ - `.github/copilot-instructions.md`
27
+ - `.github/instructions/*.md`
28
+ - Secret-like values
29
+ - Risky shell and Git commands
30
+ - Prompt-injection-like language
31
+ - Missing title
32
+ - Missing purpose, scope, command, or safety guidance
33
+ - Vague language
34
+ - Files that are too long for practical agent use
35
+ - A small set of obvious contradictory rules
36
+
37
+ ## Install
38
+
39
+ Run it without installing:
40
+
41
+ ```bash
42
+ uvx agent-rules-lint .
43
+ ```
44
+
45
+ Or install it as an isolated CLI:
46
+
47
+ ```bash
48
+ pipx install agent-rules-lint
49
+ agent-rules-lint .
50
+ ```
51
+
52
+ Standard pip installation also works:
53
+
54
+ ```bash
55
+ python -m pip install agent-rules-lint
56
+ ```
57
+
58
+ For local development:
59
+
60
+ ```bash
61
+ python -m pip install -e .
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ Scan the current repository:
67
+
68
+ ```bash
69
+ agent-rules-lint .
70
+ ```
71
+
72
+ Return JSON for CI or custom reports:
73
+
74
+ ```bash
75
+ agent-rules-lint . --format json
76
+ ```
77
+
78
+ Fail CI on warnings:
79
+
80
+ ```bash
81
+ agent-rules-lint . --warnings-as-errors
82
+ ```
83
+
84
+ Change the long-file threshold:
85
+
86
+ ```bash
87
+ agent-rules-lint . --max-chars 8000
88
+ ```
89
+
90
+ ## Example Output
91
+
92
+ ```text
93
+ agent-rules-lint checked 1 file(s)
94
+ errors=0 warnings=1 info=2
95
+
96
+ Files:
97
+ - AGENTS.md
98
+
99
+ Findings:
100
+ [warning] AGENTS.md:42 risky-command: Risky command should include explicit safeguards or approval rules.
101
+ [info] AGENTS.md missing-safety: Consider adding a clear safety section.
102
+ ```
103
+
104
+ ## CI
105
+
106
+ Add this to GitHub Actions:
107
+
108
+ ```yaml
109
+ name: Lint agent rules
110
+
111
+ on:
112
+ pull_request:
113
+ push:
114
+ branches:
115
+ - main
116
+
117
+ jobs:
118
+ lint-agent-rules:
119
+ runs-on: ubuntu-latest
120
+ steps:
121
+ - uses: actions/checkout@v7.0.0
122
+ - uses: actions/setup-python@v6.3.0
123
+ with:
124
+ python-version: "3.12"
125
+ - run: pipx run agent-rules-lint . --warnings-as-errors
126
+ ```
127
+
128
+ ## Design Principles
129
+
130
+ - No model calls
131
+ - No network calls
132
+ - No dependencies
133
+ - Safe to run in CI
134
+ - Findings should be specific enough to act on
135
+ - Warnings should improve instruction quality, not enforce one writing style
136
+
137
+ ## Current Limitations
138
+
139
+ - Conflict detection is intentionally conservative and only catches obvious pairs.
140
+ - It does not estimate real tokenizer counts; it uses character length as a practical proxy.
141
+ - It does not validate whether an instruction is correct for a specific tool.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ python -m pip install -e .
147
+ python -m unittest discover -s tests -v
148
+ python -m agent_rules_lint .
149
+ ```
150
+
151
+ ## Agent OSS Toolkit
152
+
153
+ This project is part of a small toolkit for building and launching agent-ready open-source repositories:
154
+
155
+ - [agent-repo-kit](https://github.com/Uky0Yang/agent-repo-kit): scaffold launch-ready, AI-agent-friendly repositories
156
+ - [oss-launch-check](https://github.com/Uky0Yang/oss-launch-check): audit whether a repository is ready to launch as open source
157
+ - [repo-context-card](https://github.com/Uky0Yang/repo-context-card): generate compact repository context cards for coding agents
158
+ - [agent-rules-lint](https://github.com/Uky0Yang/agent-rules-lint): lint AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions
159
+ - [awesome-ai-agents-zh](https://github.com/Uky0Yang/awesome-ai-agents-zh): Chinese AI Agents / MCP / AI DevTools directory
160
+
161
+ ## Contributing
162
+
163
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
164
+
165
+ ## License
166
+
167
+ MIT
@@ -0,0 +1,29 @@
1
+ # Roadmap
2
+
3
+ ## Near Term
4
+
5
+ - Add more real-world examples
6
+ - Add markdown output
7
+ - Add line excerpts in JSON output
8
+ - Support config in `pyproject.toml`
9
+ - Keep PyPI releases automated through Trusted Publishing
10
+
11
+ ## Mid Term
12
+
13
+ - Add rule IDs with documentation pages
14
+ - Add SARIF output for GitHub code scanning
15
+ - Add pre-commit hook documentation
16
+ - Add optional token estimation adapters
17
+ - Add fixture corpus from public agent instruction files
18
+
19
+ ## Long Term
20
+
21
+ - Build a benchmark of high-quality agent instruction files
22
+ - Add project-specific profiles for Codex, Claude Code, Cursor, Copilot, and Gemini CLI
23
+ - Provide a GitHub Action wrapper
24
+
25
+ ## Non-Goals
26
+
27
+ - No model-based scoring by default
28
+ - No network calls by default
29
+ - No automatic rewriting of user instructions without explicit user action
@@ -0,0 +1,3 @@
1
+ """Lint AI agent instruction files."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .linter import LintResult, lint_path
9
+
10
+
11
+ def build_parser() -> argparse.ArgumentParser:
12
+ parser = argparse.ArgumentParser(
13
+ prog="agent-rules-lint",
14
+ description="Lint AI agent instruction files such as AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions.",
15
+ )
16
+ parser.add_argument("path", nargs="?", default=".", help="Repository or directory to scan.")
17
+ parser.add_argument("--format", choices=("text", "json"), default="text", help="Output format.")
18
+ parser.add_argument("--max-chars", type=int, default=12000, help="Warn when a file exceeds this many characters.")
19
+ parser.add_argument("--warnings-as-errors", action="store_true", help="Exit non-zero when warnings are found.")
20
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
21
+ return parser
22
+
23
+
24
+ def render_text(result: LintResult) -> str:
25
+ lines = [
26
+ f"agent-rules-lint checked {len(result.files_checked)} file(s)",
27
+ f"errors={result.error_count} warnings={result.warning_count} info={result.info_count}",
28
+ ]
29
+ if result.files_checked:
30
+ lines.append("")
31
+ lines.append("Files:")
32
+ for file in result.files_checked:
33
+ lines.append(f" - {file}")
34
+
35
+ if result.findings:
36
+ lines.append("")
37
+ lines.append("Findings:")
38
+ for finding in result.findings:
39
+ location = finding.file
40
+ if finding.line is not None:
41
+ location = f"{location}:{finding.line}"
42
+ lines.append(f" [{finding.severity}] {location} {finding.rule}: {finding.message}")
43
+ else:
44
+ lines.append("")
45
+ lines.append("No findings.")
46
+
47
+ return "\n".join(lines)
48
+
49
+
50
+ def exit_code(result: LintResult, warnings_as_errors: bool) -> int:
51
+ if result.error_count:
52
+ return 1
53
+ if warnings_as_errors and result.warning_count:
54
+ return 1
55
+ return 0
56
+
57
+
58
+ def main(argv: list[str] | None = None) -> int:
59
+ parser = build_parser()
60
+ args = parser.parse_args(argv)
61
+ result = lint_path(Path(args.path), max_chars=args.max_chars)
62
+
63
+ if args.format == "json":
64
+ print(result.to_json())
65
+ else:
66
+ print(render_text(result))
67
+
68
+ return exit_code(result, warnings_as_errors=args.warnings_as_errors)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ import fnmatch
6
+ import json
7
+ import re
8
+ from typing import Iterable
9
+
10
+
11
+ DEFAULT_PATTERNS = (
12
+ "AGENTS.md",
13
+ "CLAUDE.md",
14
+ "GEMINI.md",
15
+ ".cursorrules",
16
+ ".cursor/rules/*.md",
17
+ ".github/copilot-instructions.md",
18
+ ".github/instructions/*.md",
19
+ )
20
+
21
+ DEFAULT_IGNORE_DIRS = {
22
+ ".git",
23
+ ".hg",
24
+ ".svn",
25
+ ".venv",
26
+ "venv",
27
+ "node_modules",
28
+ "__pycache__",
29
+ "dist",
30
+ "build",
31
+ }
32
+
33
+ SECRET_PATTERNS = (
34
+ re.compile(r"\bghp_[A-Za-z0-9_]{20,}\b"),
35
+ re.compile(r"\bgho_[A-Za-z0-9_]{20,}\b"),
36
+ re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b"),
37
+ re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b"),
38
+ re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
39
+ re.compile(r"-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----"),
40
+ )
41
+
42
+ RISKY_COMMAND_PATTERNS = (
43
+ re.compile(r"\brm\s+-rf\s+[/~$*]", re.IGNORECASE),
44
+ re.compile(r"\bgit\s+reset\s+--hard\b", re.IGNORECASE),
45
+ re.compile(r"\bgit\s+clean\s+-fdx\b", re.IGNORECASE),
46
+ re.compile(r"\bRemove-Item\b.+\b-Recurse\b.+\b-Force\b", re.IGNORECASE),
47
+ re.compile(r"\bcurl\b.+\|\s*(bash|sh)\b", re.IGNORECASE),
48
+ re.compile(r"\bwget\b.+\|\s*(bash|sh)\b", re.IGNORECASE),
49
+ )
50
+
51
+ PROMPT_INJECTION_PATTERNS = (
52
+ re.compile(r"ignore (all )?(previous|prior|above) instructions", re.IGNORECASE),
53
+ re.compile(r"reveal (the )?(system|developer) (prompt|message)", re.IGNORECASE),
54
+ re.compile(r"bypass (safety|policy|guardrails)", re.IGNORECASE),
55
+ )
56
+
57
+ SECTION_HINTS = {
58
+ "purpose": ("purpose", "goal", "objective", "what this does", "overview", "目标", "目的"),
59
+ "scope": ("scope", "when to use", "applies to", "boundaries", "范围", "适用"),
60
+ "commands": ("commands", "scripts", "verification", "tests", "test", "build", "命令", "验证", "测试"),
61
+ "safety": ("safety", "security", "secrets", "do not", "never", "安全", "密钥", "不要"),
62
+ "style": ("style", "formatting", "conventions", "tone", "风格", "格式", "约定"),
63
+ }
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class Finding:
68
+ file: str
69
+ severity: str
70
+ rule: str
71
+ message: str
72
+ line: int | None = None
73
+
74
+ def to_dict(self) -> dict:
75
+ data = {
76
+ "file": self.file,
77
+ "severity": self.severity,
78
+ "rule": self.rule,
79
+ "message": self.message,
80
+ }
81
+ if self.line is not None:
82
+ data["line"] = self.line
83
+ return data
84
+
85
+
86
+ @dataclass
87
+ class LintResult:
88
+ root: str
89
+ files_checked: list[str] = field(default_factory=list)
90
+ findings: list[Finding] = field(default_factory=list)
91
+
92
+ @property
93
+ def error_count(self) -> int:
94
+ return sum(1 for finding in self.findings if finding.severity == "error")
95
+
96
+ @property
97
+ def warning_count(self) -> int:
98
+ return sum(1 for finding in self.findings if finding.severity == "warning")
99
+
100
+ @property
101
+ def info_count(self) -> int:
102
+ return sum(1 for finding in self.findings if finding.severity == "info")
103
+
104
+ def to_dict(self) -> dict:
105
+ return {
106
+ "root": self.root,
107
+ "files_checked": self.files_checked,
108
+ "summary": {
109
+ "errors": self.error_count,
110
+ "warnings": self.warning_count,
111
+ "info": self.info_count,
112
+ },
113
+ "findings": [finding.to_dict() for finding in self.findings],
114
+ }
115
+
116
+ def to_json(self) -> str:
117
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
118
+
119
+
120
+ def discover_instruction_files(root: Path, patterns: Iterable[str] = DEFAULT_PATTERNS) -> list[Path]:
121
+ root = root.resolve()
122
+ matches: list[Path] = []
123
+ normalized_patterns = tuple(pattern.replace("\\", "/") for pattern in patterns)
124
+
125
+ for path in root.rglob("*"):
126
+ if not path.is_file():
127
+ continue
128
+ if any(part in DEFAULT_IGNORE_DIRS for part in path.relative_to(root).parts):
129
+ continue
130
+ relative = path.relative_to(root).as_posix()
131
+ if any(fnmatch.fnmatch(relative, pattern) for pattern in normalized_patterns):
132
+ matches.append(path)
133
+
134
+ return sorted(matches, key=lambda item: item.relative_to(root).as_posix())
135
+
136
+
137
+ def lint_path(root: Path, max_chars: int = 12000) -> LintResult:
138
+ root = root.resolve()
139
+ result = LintResult(root=str(root))
140
+ files = discover_instruction_files(root)
141
+
142
+ if not files:
143
+ result.findings.append(
144
+ Finding(
145
+ file=".",
146
+ severity="warning",
147
+ rule="no-instruction-files",
148
+ message="No known agent instruction files found.",
149
+ )
150
+ )
151
+ return result
152
+
153
+ for path in files:
154
+ relative = path.relative_to(root).as_posix()
155
+ result.files_checked.append(relative)
156
+ result.findings.extend(lint_file(path, root, max_chars=max_chars))
157
+
158
+ return result
159
+
160
+
161
+ def lint_file(path: Path, root: Path, max_chars: int = 12000) -> list[Finding]:
162
+ relative = path.relative_to(root).as_posix()
163
+ text = path.read_text(encoding="utf-8", errors="replace")
164
+ lines = text.splitlines()
165
+ findings: list[Finding] = []
166
+
167
+ if not text.strip():
168
+ findings.append(Finding(relative, "error", "empty-file", "Instruction file is empty."))
169
+ return findings
170
+
171
+ if len(text) > max_chars:
172
+ findings.append(
173
+ Finding(
174
+ relative,
175
+ "warning",
176
+ "too-long",
177
+ f"Instruction file is {len(text)} characters; consider splitting or tightening it.",
178
+ )
179
+ )
180
+
181
+ if not re.search(r"^#\s+\S+", text, flags=re.MULTILINE):
182
+ findings.append(Finding(relative, "warning", "missing-title", "Add a top-level heading."))
183
+
184
+ present_sections = detect_sections(text)
185
+ for section in ("purpose", "scope", "commands", "safety"):
186
+ if section not in present_sections:
187
+ findings.append(
188
+ Finding(
189
+ relative,
190
+ "info",
191
+ f"missing-{section}",
192
+ f"Consider adding a clear {section} section.",
193
+ )
194
+ )
195
+
196
+ findings.extend(scan_patterns(relative, lines, SECRET_PATTERNS, "error", "secret-like-value", "Looks like a secret or private key."))
197
+ findings.extend(scan_patterns(relative, lines, RISKY_COMMAND_PATTERNS, "warning", "risky-command", "Risky command should include explicit safeguards or approval rules."))
198
+ findings.extend(scan_patterns(relative, lines, PROMPT_INJECTION_PATTERNS, "warning", "prompt-injection", "Instruction resembles prompt-injection language."))
199
+ findings.extend(scan_weak_language(relative, lines))
200
+ findings.extend(scan_absolute_claims(relative, lines))
201
+ findings.extend(scan_conflicts(relative, lines))
202
+
203
+ return findings
204
+
205
+
206
+ def detect_sections(text: str) -> set[str]:
207
+ lowered = text.lower()
208
+ present: set[str] = set()
209
+ for section, hints in SECTION_HINTS.items():
210
+ if any(hint in lowered for hint in hints):
211
+ present.add(section)
212
+ return present
213
+
214
+
215
+ def scan_patterns(
216
+ relative: str,
217
+ lines: list[str],
218
+ patterns: Iterable[re.Pattern[str]],
219
+ severity: str,
220
+ rule: str,
221
+ message: str,
222
+ ) -> list[Finding]:
223
+ findings: list[Finding] = []
224
+ for line_number, line in enumerate(lines, start=1):
225
+ if any(pattern.search(line) for pattern in patterns):
226
+ findings.append(Finding(relative, severity, rule, message, line_number))
227
+ return findings
228
+
229
+
230
+ def scan_weak_language(relative: str, lines: list[str]) -> list[Finding]:
231
+ findings: list[Finding] = []
232
+ pattern = re.compile(r"\b(try to|maybe|if possible|do your best|尽量|可能的话)\b", re.IGNORECASE)
233
+ for line_number, line in enumerate(lines, start=1):
234
+ if pattern.search(line):
235
+ findings.append(
236
+ Finding(
237
+ relative,
238
+ "info",
239
+ "weak-language",
240
+ "Vague language can make agent behavior inconsistent.",
241
+ line_number,
242
+ )
243
+ )
244
+ return findings
245
+
246
+
247
+ def scan_absolute_claims(relative: str, lines: list[str]) -> list[Finding]:
248
+ findings: list[Finding] = []
249
+ pattern = re.compile(r"\b(always|never|must|禁止|必须|永远|绝不)\b", re.IGNORECASE)
250
+ for line_number, line in enumerate(lines, start=1):
251
+ if pattern.search(line) and len(line) > 180:
252
+ findings.append(
253
+ Finding(
254
+ relative,
255
+ "info",
256
+ "long-absolute-rule",
257
+ "Long absolute rules are hard to follow; split into short, testable bullets.",
258
+ line_number,
259
+ )
260
+ )
261
+ return findings
262
+
263
+
264
+ def scan_conflicts(relative: str, lines: list[str]) -> list[Finding]:
265
+ text = "\n".join(lines).lower()
266
+ findings: list[Finding] = []
267
+ conflict_pairs = (
268
+ ("always ask", "never ask"),
269
+ ("always commit", "never commit"),
270
+ ("always browse", "never browse"),
271
+ ("must use python", "never use python"),
272
+ ("必须询问", "不要询问"),
273
+ )
274
+ for left, right in conflict_pairs:
275
+ if left in text and right in text:
276
+ findings.append(
277
+ Finding(
278
+ relative,
279
+ "warning",
280
+ "possible-conflict",
281
+ f"Possible conflicting instructions: '{left}' and '{right}'.",
282
+ )
283
+ )
284
+ return findings
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agent-rules-lint"
7
+ version = "0.1.1"
8
+ description = "Lint AI agent instruction files such as AGENTS.md, CLAUDE.md, Cursor rules, and Copilot instructions."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Ukyo" }
14
+ ]
15
+ keywords = ["ai-agents", "llm", "developer-tools", "lint", "claude-code", "cursor", "copilot"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Quality Assurance",
28
+ "Topic :: Utilities"
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.scripts]
33
+ agent-rules-lint = "agent_rules_lint.cli:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/Uky0Yang/agent-rules-lint"
37
+ Repository = "https://github.com/Uky0Yang/agent-rules-lint"
38
+ Issues = "https://github.com/Uky0Yang/agent-rules-lint/issues"
39
+ Changelog = "https://github.com/Uky0Yang/agent-rules-lint/blob/main/CHANGELOG.md"
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ import unittest
5
+ from contextlib import redirect_stdout
6
+ from io import StringIO
7
+ from pathlib import Path
8
+
9
+ from agent_rules_lint.cli import main
10
+
11
+
12
+ class CliTests(unittest.TestCase):
13
+ def test_clean_file_exits_zero(self) -> None:
14
+ with tempfile.TemporaryDirectory() as directory:
15
+ root = Path(directory)
16
+ (root / "AGENTS.md").write_text(
17
+ "# Rules\n\n"
18
+ "## Purpose\n\nHelp agents.\n\n"
19
+ "## Scope\n\nThis repo.\n\n"
20
+ "## Commands\n\nRun tests.\n\n"
21
+ "## Safety\n\nDo not commit secrets.\n",
22
+ encoding="utf-8",
23
+ )
24
+
25
+ with redirect_stdout(StringIO()):
26
+ self.assertEqual(main([str(root)]), 0)
27
+
28
+ def test_warnings_as_errors_exits_nonzero(self) -> None:
29
+ with tempfile.TemporaryDirectory() as directory:
30
+ root = Path(directory)
31
+ (root / "AGENTS.md").write_text("# Rules\n\nRun `git reset --hard`.", encoding="utf-8")
32
+
33
+ with redirect_stdout(StringIO()):
34
+ self.assertEqual(main([str(root), "--warnings-as-errors"]), 1)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ unittest.main()
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ from agent_rules_lint.linter import discover_instruction_files, lint_path
8
+
9
+
10
+ class LinterTests(unittest.TestCase):
11
+ def test_discovers_common_instruction_files(self) -> None:
12
+ with tempfile.TemporaryDirectory() as directory:
13
+ root = Path(directory)
14
+ (root / "AGENTS.md").write_text("# Agent\n\nPurpose section.", encoding="utf-8")
15
+ (root / ".cursor" / "rules").mkdir(parents=True)
16
+ (root / ".cursor" / "rules" / "ui.md").write_text("# UI\n\nScope section.", encoding="utf-8")
17
+ (root / "README.md").write_text("# Ignore", encoding="utf-8")
18
+
19
+ resolved_root = root.resolve()
20
+ discovered = [path.relative_to(resolved_root).as_posix() for path in discover_instruction_files(root)]
21
+
22
+ self.assertEqual(discovered, [".cursor/rules/ui.md", "AGENTS.md"])
23
+
24
+ def test_flags_secret_like_values(self) -> None:
25
+ with tempfile.TemporaryDirectory() as directory:
26
+ root = Path(directory)
27
+ (root / "CLAUDE.md").write_text("# Rules\n\nUse token sk-abcdefghijklmnopqrstuvwxyz123456.", encoding="utf-8")
28
+
29
+ result = lint_path(root)
30
+
31
+ self.assertEqual(result.error_count, 1)
32
+ self.assertTrue(any(finding.rule == "secret-like-value" for finding in result.findings))
33
+
34
+ def test_flags_risky_commands_without_failing_by_default(self) -> None:
35
+ with tempfile.TemporaryDirectory() as directory:
36
+ root = Path(directory)
37
+ (root / "AGENTS.md").write_text("# Rules\n\nRun `git reset --hard` when stuck.", encoding="utf-8")
38
+
39
+ result = lint_path(root)
40
+
41
+ self.assertEqual(result.error_count, 0)
42
+ self.assertTrue(any(finding.rule == "risky-command" for finding in result.findings))
43
+
44
+ def test_warns_when_no_instruction_files(self) -> None:
45
+ with tempfile.TemporaryDirectory() as directory:
46
+ result = lint_path(Path(directory))
47
+
48
+ self.assertEqual(result.warning_count, 1)
49
+ self.assertEqual(result.findings[0].rule, "no-instruction-files")
50
+
51
+
52
+ if __name__ == "__main__":
53
+ unittest.main()