A policy-as-code CLI for repository boundaries around AI-assisted development. It detects local instruction files, tool configuration, leaked boilerplate, and configurable provenance markers before they cross into a published codebase.
For every finding, llm-lint reports what it found, where, why it matters, and a concrete remediation. Teams choose which rules and severities match their publication, disclosure, and retention policies; every built-in rule is configurable.
| ID | Severity | Detects |
|---|---|---|
LLM001 |
error | CLAUDE.md committed at any depth |
LLM002 |
error | .claude/ directory tracked in git |
LLM003 |
error | Co-authored-by: Claude commit trailer |
LLM004 |
warning | 🤖 Generated with [Claude Code] / Generated with Claude in commit messages |
LLM005 |
warning | CLAUDE_NOTES.md, CLAUDE_*.md, .claude.local.md |
LLM006 |
error | .cursorrules, .cursor/, .cursorignore |
LLM007 |
warning | .github/copilot-instructions.md, .copilotignore, .github/copilot/ |
LLM008 |
warning | .aider* (config, history, input log) |
LLM009 |
warning | .continue/, .continuerc.json |
LLM010 |
warning | .codeium/, codeium.toml |
LLM011 |
warning | .windsurfrules, .windsurf/ |
LLM012 |
warning | Co-authored-by: <Copilot/OpenAI/Cursor/Codeium/Aider> trailers |
LLM013 |
info | LLM refusal/boilerplate strings in source ("As an AI language model…", "I'm sorry, but I can't…") |
LLM014 |
info | "Generated by ChatGPT/GPT-4/Claude/Copilot" markers in comments |
LLM015 |
info | .mcp.json referencing claude-code MCP servers |
Run llm-lint rules show LLM003 for the full description and remediation of any rule.
llm-lint reports repository-policy signals; it does not determine authorship. The commit-marker rules support organizations that prohibit automated trailers or generated-by markers in published history. Disable those rules when a project requires or prefers that disclosure. Do not use auto-fix or history rewriting to remove attribution, license notices, co-author credit, or disclosures required by a project, employer, or agreement.
Run it once, no install:
npx @jadenrazo/llm-lint scanThat's the entire setup. Requires Node 18+; works on Linux, macOS (Intel + Apple Silicon), and Windows.
The npm package ships native Go binaries via the esbuild-style optionalDependencies pattern — npm pulls only the binary matching your platform (~3 MB), with no postinstall scripts and no Node runtime dependency once installed.
Other install methods
# Persistent install via npm
npm install -g @jadenrazo/llm-lint
llm-lint scan
# Native binary, no Node (Linux/macOS, AMD64/ARM64)
arch=$(uname -m); [ "$arch" = aarch64 ] && arch=arm64
curl -sSfL "https://github.com/JadenRazo/llm-lint/releases/latest/download/llm-lint_$(uname -s)_${arch}.tar.gz" \
| sudo tar -xz -C /usr/local/bin llm-lint
llm-lint scan
# Docker
docker run --rm -v "$PWD":/workspace ghcr.io/jadenrazo/llm-lint:latest scan
# Homebrew, apt, yum — see https://github.com/JadenRazo/llm-lint/releasesExit codes:
0— no findings at or above--fail-onseverity (defaulterror)1— findings exceeded threshold2— internal error (bad config, IO, etc.)3— baseline has stale entries andbaseline.stale_actionisfail(or--baseline-stale-fail)
For deterministic cleanup, run:
llm-lint scan --fixAuto-fix removes matching boilerplate/comment-marker lines, appends safe ignore patterns to .gitignore, untracks local AI/tool files with git rm --cached while keeping them in your working tree, and can remove configured trailers or markers from the latest commit message.
Preview the same plan without changing files, the git index, or history:
llm-lint scan --fix-previewCommit-message cleanup is configurable:
llm-lint scan --fix-preview --fix-git-history scanned # preview broad history cleanup
llm-lint scan --fix --fix-git-history latest # default: only HEAD
llm-lint scan --fix --fix-git-history scanned # rewrite all matching scanned commits on HEAD history
llm-lint scan --fix --fix-git-history none # leave commit findings as manualUse scanned only when repository policy permits rewriting every matching finding in the scanned history. This changes commit IDs for cleaned commits and their descendants, and rewritten commits will not retain commit signatures. Preview first, preserve required attribution, and coordinate before using it on shared branches.
In CI, --format github emits inline PR annotations and a Markdown step summary. It auto-activates when GITHUB_ACTIONS=true is set and --format is not explicitly chosen — so this just works:
- run: llm-lint scan --fail-on errorFor inline PR review comments and a sticky PR comment with the findings:
- run: llm-lint scan --fail-on error --pr-comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}The sticky comment edits in place across runs (one comment per PR, not N). PR comment failures never fail the build — they're logged to stderr and skipped.
Use --format sarif instead when you want findings to land in GitHub Code Scanning alerts:
Drop this into .github/workflows/llm-lint.yml. Findings flow into the Code Scanning tab via SARIF. Node is preinstalled on ubuntu-latest, so npx is the shortest path:
name: llm-lint
on:
pull_request:
push:
branches: [main, master]
permissions:
contents: read
security-events: write
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with: { fetch-depth: 0 } # full history; required for trailer rules
- run: npx -y @jadenrazo/llm-lint@0.4.1 scan --format sarif --output llm-lint.sarif --fail-on error
- uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
if: always()
with: { sarif_file: llm-lint.sarif }The example pins both third-party Actions and llm-lint for reproducible runs. Update those pins deliberately when adopting a newer release.
llm-lint:
stage: test
image: ghcr.io/jadenrazo/llm-lint:latest
variables:
GIT_DEPTH: "0"
script:
- llm-lint scan --format json --output llm-lint.json --fail-on error
artifacts:
when: always
paths: [llm-lint.json]The fastest path is to let llm-lint write the hook for you:
llm-lint hook installThis autodetects: if you already have .pre-commit-config.yaml, it adds an entry there; otherwise it writes a managed shell hook to .git/hooks/pre-commit. Either way, commits are gated on llm-lint scan --staged-only, which scans the git index — typically <100ms even on large repos. Run llm-lint hook status to inspect, llm-lint hook uninstall to remove.
If you prefer to manage .pre-commit-config.yaml yourself:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/JadenRazo/llm-lint
rev: v0.2.3
hooks:
- id: llm-lintThe pre-commit hook runs in --staged-only mode, so only path/content rules fire on staged blobs. Trailer/message rules require an actual commit and are skipped (they apply at scan-time, not pre-commit-time).
docker run --rm -v "$PWD":/workspace ghcr.io/jadenrazo/llm-lint:latest scanAdopting llm-lint on a repo with historical artifacts is painful if CI fails on day one. The baseline file accepts current findings without losing visibility — they're still reported, marked (baselined), but excluded from the --fail-on exit-code gate.
llm-lint baseline create # snapshots .llmlint-baseline.yaml from current findings
git add .llmlint-baseline.yaml # commit it; the file is meant to be in version controlAfter this, llm-lint scan ignores baselined findings for CI gating but still flags anything new. As the team fixes baselined findings:
llm-lint baseline status # see how many are baselined / new / stale
llm-lint baseline prune # drop entries that no longer match any finding
llm-lint baseline update # re-snapshot (alias for `create --force`)The goal is to shrink the baseline toward zero. Stale entries (an entry whose finding has been fixed) print a warning by default; set baseline.stale_action: fail in .llmlint.yaml to enforce cleanup in CI.
Fingerprints are stable across line shifts (for content findings) and severity changes (severity is config, not finding identity). Path renames invalidate path findings — that's intentional, since a rename is a deliberate change. SARIF output emits baselineState: unchanged for baselined findings, so GitHub Code Scanning treats them as suppressed-from-PR-summary natively.
Drop a .llmlint.yaml at your repo root. Every key is optional — defaults work for most projects.
version: 1
# Filter to specific categories. Omit for "all".
categories:
- claude
- cursor
- copilot
# Per-rule overrides
rules:
LLM013:
severity: warning # bump info → warning
LLM004:
enabled: false # don't care about Claude attribution comments
# Paths to ignore (gitignore-style globs)
ignore:
- "vendor/**"
- "node_modules/**"
- "**/*.min.js"
- "testdata/**"
# Scan modes
scan:
filesystem: true
git_history: true
git_history_depth: 1000 # 0 = full history
# Auto-fix policy used when `llm-lint scan --fix` runs
fix:
git_history: latest # none | latest | scanned
fail_on: error # error | warning | info | noneA more annotated example lives in examples/.llmlint.yaml.
llm-lint scan [path] # scan dir (default ".")
llm-lint scan --format sarif --output llm-lint.sarif
llm-lint scan --fail-on warning
llm-lint scan --no-git # skip history scan
llm-lint scan --since v1.0.0 # only commits newer than this ref
llm-lint scan --staged-only # scan the git index (pre-commit hook mode)
llm-lint scan --fix-preview # preview autofix changes without writing
llm-lint scan --fix --fix-git-history scanned
llm-lint scan --include LLM015 --exclude LLM004
llm-lint rules # list all rules with severity + category
llm-lint rules show LLM003 # full description + remediation
llm-lint hook install # wire up a pre-commit hook (autodetect mode)
llm-lint hook status # show current hook installation state
llm-lint hook uninstall # remove the managed hook
llm-lint baseline create # snapshot current findings into .llmlint-baseline.yaml
llm-lint baseline status # show matched / new / stale counts
llm-lint baseline prune # drop stale baseline entries
llm-lint version
Why not just .gitignore?
.gitignore is opt-in per developer and can't catch artifacts already committed before someone added the entry. llm-lint enforces at CI, so a missed .gitignore entry on one machine doesn't leak.
How do I stop Claude from adding Co-authored-by?
Edit ~/.claude/settings.json:
{ "includeCoAuthoredBy": false }Or run claude config set includeCoAuthoredBy false. Recent Claude Code versions also gate the 🤖 Generated with [Claude Code] message footer behind the same flag.
Does it slow down CI?
Typical scan is well under a second on 10k files. Filesystem walks run concurrently; git-history rules iterate commits in-process via go-git (no shell-out).
Does it work on shallow clones?
Yes — but commit-trailer rules need history. On actions/checkout use fetch-depth: 0. On a shallow clone, llm-lint still runs path/content rules and prints a note that history scanning was partial.
My repo legitimately mentions CLAUDE.md in docs/tests — how do I avoid false positives?
Add the path or pattern to your .llmlint.yaml ignore list, and/or disable noisy info-level rules (LLM013, LLM014) for repos that intentionally include LLM strings.
Can I auto-fix?
Yes — llm-lint scan --fix removes matching boilerplate lines, appends safe .gitignore entries, untracks AI/tool files with git rm --cached (keeping them in your working tree), and can clean AI trailers from commit messages (--fix-git-history none|latest|scanned). Use --fix-preview to see the plan without changing anything. See the Auto-fix section above.
Why is the npm package scoped (@jadenrazo/llm-lint)?
The bare llm-lint name on npm is squatted by an unrelated project. Scoping under @jadenrazo keeps the name unambiguous and the publish path uncomplicated.
Adding a new rule is cheap:
- Pick the next free
LLM###ID (runllm-lint rulesto see the current set). - Copy the closest existing rule struct in
internal/rules/builtin/{path,content,git_trailer}_rules.go. - Write
DescriptionandRemediationstrings — the latter must be concrete and actionable (commands, file paths, settings keys). - Add a fixture under
testdata/and a table-driven test in the matching_test.go. go test ./...— must be green before opening a PR.
Architecture overview:
cmd/llm-lint/ → internal/engine/ → internal/scanner/ (filesystem walk + path/content rules)
↳ internal/gitscan/ (commit history + trailer/message rules)
↳ internal/report/ (human / json / sarif / github)
internal/rules/ — rule definitions, registered via init()
internal/config/ — .llmlint.yaml schema + override resolution
internal/baseline/ — accepted-findings snapshots + stable fingerprints
internal/fixer/ — --fix / --fix-preview (files, .gitignore, index, commit messages)
internal/hook/ — pre-commit hook install (native + pre-commit framework)
internal/progress/ — transient TTY progress line
See CONTRIBUTING.md for the full development guide and the rule-ID stability policy.
Apache-2.0. See LICENSE.

0 comments
log in to comment.