Skip to content

Configure Canopy for a Monorepo

Monorepos work with Canopy out of the box — run canopy index at the repo root and Canopy indexes everything. The challenge is tuning configuration so Canopy understands which packages are entry points, which paths to ignore, and how packages relate to each other.

Run canopy index from the monorepo root:

Terminal window
canopy index /path/to/monorepo --with-search --with-git

Canopy walks the entire directory tree, parsing all supported source files. For a typical monorepo with 50–200 packages, initial indexing takes 30–120 seconds. Subsequent incremental runs are much faster.

Verify the index:

Terminal window
canopy stats

Expected output:

canopy stats: /home/you/repos/monorepo
Files indexed: 8,432
Symbols extracted: 94,217
Packages found: 47
Languages: TypeScript (6,201), JavaScript (1,891), Python (340)
Index size: 187 MB
Last indexed: 2026-04-16 14:32:01 (2 minutes ago)

Create .canopy/config.toml in the monorepo root:

[index]
# Paths to skip during indexing
ignored_paths = [
"node_modules",
"dist",
"build",
".next",
".turbo",
"coverage",
"**/*.test.ts",
"**/*.spec.ts",
]
# Entry points for reachability analysis (canopy_check_wiring)
# These are the roots of the dependency graph
entry_points = [
"apps/web/src/index.ts",
"apps/api/src/main.ts",
"apps/mobile/index.js",
]
[search]
# Max results per search query
max_results = 50
[health]
# Minimum coverage threshold for P2 warning (0 = disabled)
min_coverage = 0

After creating or editing .canopy/config.toml, re-run canopy index to apply the new configuration.

Canopy recognizes workspace configurations automatically.

If package.json at the root defines a workspaces field, Canopy uses it to identify package boundaries:

{
"workspaces": [
"packages/*",
"apps/*"
]
}

Canopy maps cross-package imports correctly — an import from @myorg/ui in apps/web resolves to packages/ui/src/index.ts, not an unresolved external.

If your monorepo uses TypeScript path aliases (e.g., @app/*, ~/*), Canopy reads them from tsconfig.json automatically. For non-standard alias resolution, specify them in .canopy/config.toml:

[resolve]
aliases = [
{ alias = "@app", target = "apps/web/src" },
{ alias = "@shared", target = "packages/shared/src" },
{ alias = "~", target = "src" },
]

Without this, imports using these aliases appear as unresolved and generate P0 broken_import findings.

After indexing, verify that cross-package imports resolve correctly:

Terminal window
canopy health

Look for broken_import findings. If you see many errors like:

P0 broken_import: apps/web/src/components/Button.tsx:3
cannot resolve '@shared/ui'

This means path aliases or workspace config isn’t set up. Add the alias to .canopy/config.toml and re-index.

You can also check a specific package’s wiring:

Terminal window
canopy check-wiring apps/web/src/index.ts

Expected output when correctly configured:

canopy check-wiring: apps/web/src/index.ts
Reachable from entry points: YES
Entry point: apps/web/src/index.ts (self)
Path: (entry)
Outbound imports: 14 files
All imports resolved: YES

For monorepos with 500K+ files (large company repos), tune the ignored paths aggressively:

[index]
ignored_paths = [
"node_modules",
"dist",
"build",
".cache",
".turbo",
"storybook-static",
"**/*.min.js",
"**/*.bundle.js",
"**/__mocks__",
"vendor",
"third_party",
]
[index]
# Limit file size (bytes) — skips generated files and large JSON
max_file_size_bytes = 512000

Duplicate symbol warnings after indexing If the same package appears multiple times (e.g., packages/ui and node_modules/packages/ui), add the duplicate path to ignored_paths. node_modules is ignored by default; if you see duplicates, a symlink or unusual path is creating a second traversal.

Entry points show as “unreachable” canopy_check_wiring returns “unreachable” only when a file has no path from any declared entry point. If the file IS an entry point, add it to entry_points in .canopy/config.toml.

Indexing takes over 5 minutes Check canopy stats for file count. If it’s unexpectedly high (>100K files), a large directory is being indexed that shouldn’t be. Run FORGE_LOG=debug canopy index . to see which paths are taking the most time, then add them to ignored_paths.