oplint is standalone — it runs independently, whatever linter you use in your JS/TS toolchain. No ESLint plugin to install, no toolchain lock-in.
Modern linters like Biome and Oxlint are faster than ESLint, but none of them cover Obsidian plugin guidelines. ESLint plugins only work inside ESLint. oplint fills that gap: it's linter-aware and the roadmap includes rule generators to export oplint's ruleset to ESLint, Biome, Oxlint, and other top linters — so you can enforce the same Obsidian rules everywhere you lint.
oplint doesn't replace ESLint — it complements it. Keep your code-quality linting in ESLint if you use it, and let oplint handle Obsidian plugin compliance.
| Area | What OPLint checks |
|---|---|
| Security | innerHTML, outerHTML, insertAdjacentHTML, global app access |
| Manifest | Required fields, valid plugin ID, description length, isDesktopOnly flag |
| Mobile | Lookbehind regex, global timers, document/window globals, navigator API |
| Resources | onunload implementation, leaf detachment, MarkdownRenderer component misuse |
| Vault API | vault.modify vs vault.process, adapter bypass, FileManager.trashFile |
| Workspace | activeLeaf usage, stored view references |
| Commands | Default hotkeys, callback types, ID/name prefixes |
| UI | setHeading, sentence case, redundant headings, hardcoded styles |
| TypeScript | var usage, raw Promise chains, as TFile/as TFolder casts |
| General | console.log, placeholder class names, bare app global |
50+ rules, tree-sitter AST-based, accuracy-tagged (exact / approximate).
One-liner (recommended — macOS & Linux, no Rust needed):
curl -fsSL https://raw.githubusercontent.com/kodaskills/oplint/main/install.sh | shVia cargo (requires Rust 1.74+):
cargo install oplintPre-built binary — download manually for your platform from GitHub Releases (Linux, macOS, Windows).
Or build from source:
git clone https://github.com/kodaskills/oplint
cd oplint
cargo build --release
./target/release/oplint --help# Lint a plugin directory (table format by default)
oplint lint /path/to/my-obsidian-plugin
# HTML report — open in browser
oplint lint /path/to/my-obsidian-plugin -f html > report.html
# JSON output — pipe into CI scripts
oplint lint /path/to/my-obsidian-plugin -f json | jq '.summary'
# List all available rules
oplint rules
# Generate a config file
oplint init| Format | Flag | Best for |
|---|---|---|
| Table | table (default) |
Terminal — box-drawing table with color |
| Terminal | terminal |
CI — compact, one line per violation |
| HTML | html |
Reports — interactive, filterable, expandable |
| Markdown | markdown / md |
GitHub PRs, wikis |
| JSON | json |
CI/CD pipelines, scripting |
| YAML | yaml |
Config tooling |
| TOML | toml |
Rust tooling integration |
All formats include a compliance score (0–100), grade (A–F), and performance stats (total / avg / min / max per file).
Rules are tagged with an accuracy level indicating reliability:
| Tag | Meaning |
|---|---|
exact |
Fully reliable — semantic analysis, no false positives/negatives |
approximate |
Best-effort — may miss edge cases or have false positives |
Approximate rules display a note in all report formats explaining the limitation.
For the full rule reference (IDs, descriptions, severities, accuracy levels), see oplint.kodaskills.co/#rules.
Rules are battle-tested against a wide range of real-world plugins, but static analysis is never perfect. If you hit a false positive, a missed violation, or any unexpected behavior, please kindly open a Rule Feedback issue — it helps improve accuracy for everyone.
Generate a starter config:
oplint init # creates .oplint.yaml
oplint init -f json # creates .oplint.json
oplint init -f toml # creates .oplint.tomlOPLint searches for .oplint.yaml, .oplint.json, or .oplint.toml walking up from the target directory.
# File exclusions
exclude:
use_gitignore: true # respect .gitignore files (default: true)
patterns: [] # gitignore-style glob patterns
# - node_modules # exclude a directory by name
# - dist/ # trailing slash = directory only
# - coverage/
# - "**/*.generated.ts" # wildcard patterns
rules:
enabled: all # "all" or list of rule IDs: [SEC001, RES001]
disabled: [] # rule IDs to disable
skip_accuracy: # skip all rules at a given accuracy level
- approximate # options: approximate | exact
# Override severity for a specific rule
RES001:
severity: warning # error | warning | info
disabled: false
# Add custom rules (tree-sitter queries)
custom_rules:
- id: CUSTOM001
name: No TODO in production
category: General
severity: warning
message: "TODO comment found"
query: '(comment) @c (#match? @c "TODO")'
expect: match
path_filter: "**/*.ts"
except_in:
- "**/tests/**"By default OPLint reads the project's .gitignore and skips everything it ignores. Add patterns for extra exclusions using the same gitignore glob syntax:
exclude:
use_gitignore: true # set to false to ignore .gitignore entirely
patterns:
- node_modules # directory name — excludes node_modules/ anywhere in the tree
- dist/ # trailing slash — directory only
- "**/*.min.js" # wildcard — skip minified files
- coverage/| Pattern | What it excludes |
|---|---|
node_modules |
any directory named node_modules at any depth |
dist/ |
directory named dist (not files) |
**/*.generated.ts |
all .generated.ts files recursively |
src/vendor/ |
vendor/ directory inside src/ only |
# Disable all approximate rules (reduce noise, fewer false positives)
rules:
skip_accuracy: [approximate]
# Only run exact rules (strictest, no false positives)
rules:
skip_accuracy: [approximate]Rules use tree-sitter queries against the TypeScript or JSON AST.
custom_rules:
- id: MY001
name: No eval usage
category: Security
severity: error
message: "Avoid eval() — it executes arbitrary code"
suggestion: "Rewrite without eval"
query: '(call_expression function: (identifier) @f (#eq? @f "eval"))'
expect: match # "match" = flag when found | "not-match" = flag when absent
accuracy: exact # approximate | exact
accuracy_note: "Detects direct eval() calls only. Aliased eval is not detected."| Value | Fires when |
|---|---|
match (default) |
Query matches — pattern found in code |
not-match |
Query does not match — required pattern is absent |
| Value | Target |
|---|---|
| (omitted) | TypeScript / JavaScript files |
manifest |
manifest.json |
Every lint run produces a score from 0 to 100 and a grade. Here is exactly how it is computed — no magic numbers hidden.
The score answers: "what fraction of the active rules is this codebase violating, and how badly?"
It does not measure project size. A 1-file plugin and a 100-file plugin are judged by the same standard.
Each rule has a severity. Every violation of that rule costs penalty points:
| Severity | Weight |
|---|---|
error |
10 |
warning |
5 |
info |
1 |
A rule firing 50 times in one file is worse than the same rule firing once in 50 files — the first signals a systemic problem in one place, the second is one team convention to fix everywhere.
We capture this with a logarithm. For each violated rule we look at the maximum number of times it fires in a single file (max_occ), then compute:
penalty(rule) = weight × (1 + log₂(max_occ))
Examples:
| max_occ | multiplier | error penalty | warning penalty |
|---|---|---|---|
| 1 | 1.0× | 10 | 5 |
| 2 | 2.0× | 20 | 10 |
| 4 | 3.0× | 30 | 15 |
| 8 | 4.0× | 40 | 20 |
| 50 | ≈ 6.6× | 66 | 33 |
Rules that never fire contribute zero penalty regardless of how many times they could fire.
total_active_weight = Σ weight(severity) for every enabled rule
This is the theoretical maximum penalty if every active rule were violated at least once. It scales with your rule configuration, not your file count.
total_penalty = Σ penalty(rule) for every rule violated ≥ once
score = round(100 × (1 − total_penalty / total_active_weight))
score = clamp(score, 0, 100)
| Score | Grade | Label |
|---|---|---|
| 100 | A+ | Perfect |
| 90–99 | A | Excellent |
| 80–89 | B | Good |
| 70–79 | C | Fair |
| 60–69 | D | Poor |
| 0–59 | F | Critical |
A+ means zero violations — no rounding, no grace margin. A score of 99 is grade A.
| Choice | Reason |
|---|---|
| Log scale for concentration | Linear accumulation would let one widespread pattern (e.g. innerHTML in every file) dominate the entire score and make it unreadable. Log dampens repetition while still penalising it. |
| Denominator = rule weights, not file count | File count is a project size metric, not a quality metric. Two projects with the same violation pattern should get the same score. |
| A+ only at 100 | A linter score of "A+" should mean clean — not "clean enough". Any violation, however minor, is a real finding. |
| Warnings weight 5 (not 3) | Obsidian community guidelines treat warnings seriously — most are patterns that will cause problems at scale or during review. A single unfixed warning should visibly affect the grade. |
- Severity of impact beyond the three tiers — a SQL injection and a missing
onunloadboth count aserror. - Category weighting — a security violation and a style violation of the same severity cost the same.
- Accuracy —
approximaterules (which may have false positives) count the same asexactrules. Filter approximate rules in the report if you want a stricter baseline.
These are deliberate simplifications. The score is a quick signal, not a security audit.
oplint is standalone. It runs independently — no ESLint plugin to install, no toolchain lock-in. Whatever linter you use (or none), oplint just works.
Other linters don't cover Obsidian guidelines. Biome and Oxlint are faster than ESLint, but they lack Obsidian-specific rules entirely. ESLint plugins only work inside ESLint, which locks you into that toolchain.
oplint is linter-aware. The roadmap includes rule generators to export oplint's ruleset to ESLint, Biome, Oxlint, and other top linters — so you can enforce the same Obsidian rules everywhere you lint.
It complements, doesn't replace. Keep your code-quality linting in ESLint if you use it, and let oplint handle Obsidian plugin compliance.
MIT — see LICENSE for details.
Maintained with ⚡ by the Kodaskills team