Skip to content
Canopy is in pre-release. These docs describe the product at its public launch — commands, tool names, and integration examples reflect what you'll see once binaries ship. Join the waitlist →

Plugin API

Canopy’s plugin system lets you define custom health checks for your codebase as YAML files. Each file is a pack of one or more checks that run alongside the built-in health checks and report findings at configurable severity levels.

Plugins work with all tiers, including Community.


Place YAML plugin files in .canopy/plugins/ inside your repository:

<repo>/.canopy/plugins/
no-todo-comments.yaml
require-error-handling.yaml
asset-counts.yaml

The health runtime discovers and loads all *.yaml files in this directory automatically on the next canopy index or canopy ci run. No registration step is required. If the directory does not exist, nothing happens — plugins are purely opt-in.

Canopy also ships four first-party packs (React, Rust, Python, Supabase) embedded in the binary. canopy plugins list shows them, canopy plugins install <pack> copies one into .canopy/plugins/<pack>.yaml, and canopy plugins disable <pack> removes it. Installed packs are ordinary plugin files — edit them freely.


Each plugin file defines a named pack of checks:

.canopy/plugins/no-console-log.yaml
# Pack name. Appears in finding check_ids as plugin:<name>:<check-index>.
# Required.
name: no-console-log
# Semantic version of the plugin definition (informational, not enforced).
# Required.
version: "1.0"
# Default severity applied to any check that does not specify its own.
# Optional. Values: P0 | P1 | P2 | info. Default: P1.
severity: P1
# Ordered list of checks to execute.
# Required.
checks:
- # The check variant. Values: file_exists | pattern_present |
# pattern_absent | file_count | script
type: pattern_absent
# Override severity for this individual check. Optional.
severity: P2
# Message emitted in the finding when the check fails. Required.
message: "console.log found — use the structured logger"
# pattern_absent fields: glob selects the files, pattern is a regex
# that must NOT match any line, exclude lists path substrings to skip.
glob: "src/**/*.ts"
pattern: 'console\.log\s*\('
exclude:
- ".test."
- ".spec."

Pack-level fields:

FieldTypeRequiredDescription
namestringyesPack name. Findings carry check_id: plugin:<name>:<index>.
versionstringyesInformational semver for the plugin definition.
severitystringnoDefault severity for checks without their own: P0, P1, P2, or info. Defaults to P1.
checksarrayyesOrdered list of checks.

Every check takes type, message (required), and severity (optional override), plus its type-specific fields:

typeAssertsType-specific fields
file_existsEvery listed file exists on diskfiles — paths relative to the repo root
pattern_presentRegex matches at least one line of a filefile — single file; pattern — regex
pattern_absentRegex matches no line in any selected fileglob — file glob (** for any depth); pattern — regex; exclude — optional path substrings to skip
file_countDirectory holds at least N files with an extensiondirectory; extension (with leading dot, e.g. ".sql"); minimum
scriptShell command exits with code 0command — passed verbatim to /bin/sh -c

YAML severityFinding levelCI impact
P0Criticalcanopy ci exits non-zero with --fail-on-p0
P1ErrorAnnotated, build passes
P2WarningAnnotated, build passes
infoInfoAnnotated, build passes

In canopy ci --format json output, finding severities appear as critical, error, warning, and info.


pattern fields are regular expressions (Rust regex syntax), evaluated one line at a time — a construct spanning multiple lines cannot be matched by a single pattern. Escape regex metacharacters when matching literal code.

TypeScript / JavaScript examples:

# Match any console.log call
pattern: 'console\.log\s*\('
# Match explicit any annotations
pattern: ':\s*any\b'
# Match process.exit calls
pattern: 'process\.exit\s*\('

Rust examples:

# Match any unwrap() call
pattern: '\.unwrap\s*\(\s*\)'
# Match panic! invocations
pattern: 'panic!\s*\('

Python examples:

# Match bare except clauses
pattern: '^\s*except\s*:'
# Match print() calls
pattern: '^\s*print\s*\('

name: no-todo-prod
version: "1.0"
checks:
- type: pattern_absent
severity: P2
message: "TODO comment — resolve before merging to main"
glob: "src/**/*.ts"
pattern: '//\s*TODO'
exclude:
- ".test."

Flag direct database access outside the data layer

Section titled “Flag direct database access outside the data layer”
name: no-direct-db-access
version: "1.0"
checks:
- type: pattern_absent
severity: P1
message: "Direct DB call outside src/data/ — use a repository function"
glob: "src/**/*.ts"
pattern: 'db\.query\s*\('
exclude:
- "src/data/"

Require migrations and project scaffolding

Section titled “Require migrations and project scaffolding”
name: project-shape
version: "1.0"
severity: P2
checks:
- type: file_exists
message: "Missing supabase/config.toml"
files:
- supabase/config.toml
- type: file_count
message: "No migrations under supabase/migrations/"
directory: "supabase/migrations"
extension: ".sql"
minimum: 1
name: lint-gate
version: "1.0"
checks:
- type: script
severity: P0
message: "eslint failed"
command: "npx eslint src --max-warnings 0"

You can adjust a check’s severity for your team without editing the plugin file. Add overrides to .canopy/team.yml:

health_checks:
enabled: [broken_imports, dead_exports, circular_deps, secrets]
severity_overrides:
no_console_log: P2

The override applies to both built-in checks and plugin checks; the key must match the finding’s check id.


A malformed plugin never blocks canopy index or canopy ci. If a plugin file has a YAML syntax error — or its pattern is not a valid regex — Canopy logs a warning, reports a P0 finding naming that plugin, and continues running the remaining plugins.