A configurable, rule-based linter for Claude Code plugins and plugin marketplaces.
✨ Context-Aware - Automatically detects single plugin vs marketplace repositories
🎯 Rule-Based - Enable/disable individual rules with configurable severity levels
🔌 Extensible - Load custom rules from Python files
📋 Comprehensive - Validates plugin structure, metadata, command format, and more
🐳 Containerized - Run via Docker for consistent, isolated linting
⚡ Fast - Efficient validation with clear, actionable output
# From git (works before PyPI release)
uvx --from 'git+https://github.com/stbenjam/claudelint' claudelint
# Once published to PyPI, simply:
uvx claudelint
# With specific path
uvx --from 'git+https://github.com/stbenjam/claudelint' claudelint /path/to/pluginpip install claudelintgit clone https://github.com/stbenjam/claudelint.git
cd claudelint
pip install -e .docker pull ghcr.io/stbenjam/claudelint:latest
# Run on current directory
docker run -v $(pwd):/workspace ghcr.io/stbenjam/claudelint# Lint current directory
claudelint
# Lint specific directory
claudelint /path/to/plugin
# Verbose output
claudelint -v
# Strict mode (warnings as errors)
claudelint --strict
# Generate default config
claudelint --init
# List all available rules
claudelint --list-rulesclaudelint automatically detects your repository structure:
my-plugin/
├── .claude-plugin/
│ └── plugin.json
├── commands/
│ └── my-command.md
└── README.md
claudelint supports multiple marketplace structures per the Claude Code specification:
marketplace/
├── .claude-plugin/
│ └── marketplace.json
└── plugins/
├── plugin-one/
│ ├── .claude-plugin/
│ └── commands/
└── plugin-two/
├── .claude-plugin/
└── commands/
marketplace/
├── .claude-plugin/
│ └── marketplace.json # source: "./"
├── commands/ # Plugin components at root
│ └── my-command.md
└── skills/
└── my-skill/
marketplace/
├── .claude-plugin/
│ └── marketplace.json # source: "./custom/my-plugin"
└── custom/
└── my-plugin/
├── commands/
└── skills/
Plugins from plugins/, custom paths, and remote sources can coexist in one marketplace. Only local sources are validated.
claudelint understands all plugin source types and validates any sources that resolve to local paths:
- Relative paths:
"source": "./"(flat structure),"source": "./custom/path" - GitHub repositories:
"source": {"source": "github", "repo": "owner/repo"} - Git URLs:
"source": {"source": "url", "url": "https://..."}
Remote sources (GitHub, git URLs) are logged and skipped during local validation. They are valid per spec but cannot be checked until the plugin is fetched locally.
The strict field in marketplace entries controls validation behavior:
{
"name": "my-plugin",
"source": "./",
"strict": false, // plugin.json becomes optional
"description": "Plugin description can be in marketplace.json"
}When strict: false:
plugin.jsonis optional- Marketplace entry serves as the complete plugin manifest
- Plugin metadata is validated from marketplace.json
- Skills, commands, and other components work normally
When strict: true (default):
plugin.jsonis required- Marketplace entry supplements plugin.json metadata
Create .claudelint.yaml in your repository root:
# Enable/disable rules
rules:
plugin-json-required:
enabled: true
severity: error
plugin-naming:
enabled: true
severity: warning
command-sections:
enabled: true
severity: warning
# 'auto' enables only for marketplace repos
marketplace-registration:
enabled: auto
severity: error
# Load custom rules
custom-rules:
- ./my-custom-rules.py
# Exclude patterns
exclude:
- "**/node_modules/**"
- "**/.git/**"
# Treat warnings as errors
strict: falseclaudelint --initThis creates .claudelint.yaml with all builtin rules enabled.
| Rule ID | Description | Default Severity | Notes |
|---|---|---|---|
plugin-json-required |
Plugin must have .claude-plugin/plugin.json |
error | Skipped when strict: false in marketplace |
plugin-json-valid |
Plugin.json must be valid with required fields | error | |
plugin-naming |
Plugin names should use kebab-case | warning | |
commands-dir-required |
Plugin should have a commands directory | warning (disabled by default) | |
commands-exist |
Plugin should have at least one command file | info (disabled by default) | |
plugin-readme |
Plugin should have a README.md file | warning |
| Rule ID | Description | Default Severity |
|---|---|---|
command-naming |
Command files should use kebab-case | warning |
command-frontmatter |
Command files must have valid frontmatter | error |
command-sections |
Commands should have Name, Synopsis, Description, Implementation sections | warning |
command-name-format |
Command Name section should be plugin:command format |
warning |
| Rule ID | Description | Default Severity | Notes |
|---|---|---|---|
marketplace-json-valid |
Marketplace.json must be valid JSON | error (auto) | |
marketplace-registration |
Plugins must be registered in marketplace.json | error (auto) | Supports flat structures, custom paths, and remote sources |
| Rule ID | Description | Default Severity |
|---|---|---|
skill-frontmatter |
SKILL.md files should have frontmatter | warning |
| Rule ID | Description | Default Severity |
|---|---|---|
agent-frontmatter |
Agent files must have valid frontmatter with description and capabilities | error |
| Rule ID | Description | Default Severity |
|---|---|---|
hooks-json-valid |
hooks.json must be valid JSON with proper hook configuration structure | error |
| Rule ID | Description | Default Severity | Notes |
|---|---|---|---|
mcp-valid-json |
MCP configuration must be valid JSON with proper mcpServers structure | error | Validates both .mcp.json and mcpServers in plugin.json |
mcp-prohibited |
Plugins should not enable MCP servers | error (disabled by default) | Security/policy rule - enable to prohibit MCP usage |
Create custom validation rules by extending the Rule base class:
# my_custom_rules.py
from pathlib import Path
from typing import List
from claudelint import Rule, RuleViolation, Severity, RepositoryContext
class NoTodoCommentsRule(Rule):
@property
def rule_id(self) -> str:
return "no-todo-comments"
@property
def description(self) -> str:
return "Command files should not contain TODO comments"
def default_severity(self) -> Severity:
return Severity.WARNING
def check(self, context: RepositoryContext) -> List[RuleViolation]:
violations = []
for plugin_path in context.plugins:
commands_dir = plugin_path / "commands"
if not commands_dir.exists():
continue
for cmd_file in commands_dir.glob("*.md"):
with open(cmd_file, 'r') as f:
content = f.read()
if 'TODO' in content:
violations.append(
self.violation(
"Found TODO comment in command file",
file_path=cmd_file
)
)
return violationsThen reference it in .claudelint.yaml:
custom-rules:
- ./my_custom_rules.py
rules:
no-todo-comments:
enabled: true
severity: warningname: Lint Claude Plugins
on: [pull_request, push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install claudelint
run: pip install claudelint
- name: Run linter
run: claudelint --strictlint-plugins:
image: python:3.11
script:
- pip install claudelint
- claudelint --strictdocker run -v $(pwd):/workspace -w /workspace ghcr.io/stbenjam/claudelint --strict0- Success (no errors, or warnings only in non-strict mode)1- Failure (errors found, or warnings in strict mode)
Linting Claude plugins in: /path/to/marketplace
Errors:
✗ ERROR [plugins/git/.claude-plugin/plugin.json]: Missing plugin.json
✗ ERROR [.claude-plugin/marketplace.json]: Plugin 'new-plugin' not registered
Warnings:
⚠ WARNING [plugins/utils]: Missing README.md (recommended)
⚠ WARNING [plugins/jira/commands/solve.md]: Missing recommended section '## Implementation'
Summary:
Errors: 2
Warnings: 2
rules:
plugin-readme:
enabled: false # Don't require README files
command-sections:
enabled: false # Don't check for specific sectionsrules:
plugin-naming:
severity: error # Make naming violations errors instead of warnings
command-name-format:
severity: info # Downgrade to info levelpytest tests/docker build -t claudelint .claudelint/
├── src/
│ ├── rule.py # Base Rule class
│ ├── context.py # Repository detection
│ ├── config.py # Configuration management
│ └── linter.py # Main linter orchestration
├── rules/
│ └── builtin/ # Builtin validation rules
├── tests/ # Test suite
├── examples/ # Example configs and custom rules
├── claudelint # CLI entry point
├── Dockerfile # Container image
└── pyproject.toml # Package metadata
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
Apache 2.0 - See LICENSE for details.
- Claude Code Documentation
- Claude Code Plugins Reference
- AI Helpers Marketplace - Example repository using claudelint