The first thing that frustrates most developers running Claude Code inside a pnpm monorepo is probably this: you ask it to fix something in apps/web, and somehow it ends up rewriting type definitions in packages/ui along the way. If that pattern sounds familiar, you're definitely not alone — it shows up in almost every monorepo I've worked on.
After getting bitten by this several times, I realized I had to teach Claude Code — explicitly and on paper — how to reach for pnpm --filter and how cautiously to treat pnpm dlx. The two flags are deceptively simple, but their combination is what actually keeps a multi-package workflow predictable. This article walks through the configuration and conventions I now use day to day, the small mistakes that wasted my afternoon at one point or another, and the file-by-file split that keeps Claude Code's context lean.
Use pnpm --filter to scope work cleanly
The --filter flag tells pnpm to run a command only against a specific workspace package. When you call it from Claude Code, the same intent applies: "please touch only this package." It's an obvious flag if you're a daily pnpm user, but treating it as the language you use to communicate scope to Claude Code is what unlocks consistent results.
# Run tests only for apps/web
pnpm --filter @myorg/web test
# Run tests for every package that depends on packages/ui
pnpm --filter ...@myorg/ui test
# Type-check everything under apps/
pnpm --filter "./apps/**" typecheck
# Run lint only on packages that changed since main
pnpm --filter "...[origin/main]" lintThe trick is the ... operator: prefix it (...@myorg/ui) to walk upstream dependents, suffix it (@myorg/ui...) to walk downstream dependencies. When I want Claude Code to "run tests for every package that depends on packages/ui after the change," I make sure pnpm --filter ...@myorg/ui test lives in CLAUDE.md or CLAUDE.local.md. Showing the actual command syntax tends to reduce ambiguity more than describing the intent in prose. Claude Code is excellent at picking up patterns it has seen in writing — it's much weaker at guessing the right pnpm operator from English alone.
The [origin/main] selector is also worth surfacing. In a busy monorepo, "lint just what I changed" is one of the most useful commands you can hand to Claude Code, because it bounds the work to whatever your branch actually touched. I keep this command in CLAUDE.md right next to the broader filter examples.
The pnpm dlx trap — when it diverges from CI
pnpm dlx is the pnpm equivalent of npx: run a package without installing it locally. It's wonderfully convenient — until you realize Claude Code may reach for it in places where you really wanted the version pinned in your lockfile.
# Looks fine
pnpm dlx prettier --write "src/**/*.ts"If you let Claude Code reach for this every time, you'll eventually hit a moment where local formatting and CI formatting disagree. The reason is simple: dlx fetches the latest version, which may differ from the prettier pinned in your devDependencies. The diff shows up in code review, the team gets confused, and it takes longer than you'd think to trace the inconsistency back to dlx.
Here's the rule I settled on after a few rounds of pain:
- Throwaway tools (one-shot CLIs) —
pnpm dlxis fine. Things likepnpm dlx degit some/template my-new-appfor scaffolding fall into this category cleanly - Tools that affect your codebase (formatters, linters, code generators) — install them as
devDependenciesand run them viapnpm --filter <pkg> exec <tool>so the lockfile is the source of truth - Build tools (esbuild, swc, vite plugins) — never via dlx; pin them and rely on the workspace's binaries
A single line in CLAUDE.md — "Don't use dlx for prettier, eslint, or typescript; call the workspace-local binary instead" — eliminated most of the version drift I used to see. If you want a stricter guardrail, you can also pre-approve only pnpm dlx degit:* in your settings file and leave generic pnpm dlx un-approved, so any other invocation surfaces a confirmation prompt.
The diagnosis trick I recommend if formatting suddenly disagrees with CI: have Claude Code print the resolved version of every formatter in the repo with pnpm --filter "*" exec prettier --version. If the numbers are stable across packages, you've ruled out a dlx-induced floating version. If they drift, you've found your culprit in under a minute.
Lock the rules into .claude/settings.json
The tool permissions custom policy in Claude Code lets you pre-approve specific command patterns. In a monorepo, this is where you reclaim significant chunks of your day.
{
"permissions": {
"allow": [
"Bash(pnpm --filter *:*)",
"Bash(pnpm install:*)",
"Bash(pnpm test:*)",
"Bash(pnpm typecheck:*)",
"Bash(pnpm dlx degit:*)"
],
"deny": [
"Bash(pnpm publish:*)",
"Bash(pnpm -r publish:*)",
"Bash(rm -rf node_modules*)"
]
}
}pnpm publish lives in the deny list as a guardrail against accidentally publishing to npm — a particularly painful mistake to recover from once your packages have downstream consumers. I've never had an incident with this in place, but it's worth confirming it doesn't conflict with your release scripts before you ship it. If you delegate releases to Claude Code, I'd argue running them in a separate session with --dangerously-skip-permissions is safer than loosening this rule.
The rm -rf node_modules* deny entry is something I added after watching Claude Code "helpfully" wipe a workspace's node_modules to fix what was actually a tsconfig issue. Wiping node_modules in a pnpm setup means re-fetching everything, which can take minutes on a big repo. Worth blocking outright.
Splitting CLAUDE.md and CLAUDE.local.md
In monorepos, the second decision worth making consciously is how you split content between the root CLAUDE.md and per-package CLAUDE.local.md files. I've ended up with this division:
- Root
CLAUDE.md— conventions that apply to every package: pnpm filter syntax, commit message format, release rules, what's off-limits, the deny list above - Per-package
CLAUDE.local.md— package-specific quirks: "always update Storybook" forpackages/ui, "use the custom Tailwind preset" for the design system, "regenerate the OpenAPI types after changing schemas" for the API workspace
Stuffing everything into the root file bloats the context Claude Code has to read. Spreading rules too thin across packages causes the same line to be copy-pasted in three places, and inevitably the copies drift. My rule of thumb: "If the same rule shows up in three or more CLAUDE.local.md files, promote it to the root." A short, tight root file plus targeted local files reads better and tends to age better, too.
One more habit that pays off: review CLAUDE.md whenever you onboard a new package. The act of asking "would this make sense to a teammate seeing this repo for the first time?" tends to surface stale rules and inconsistencies that have crept in. Claude Code, in effect, is that teammate — and it benefits from the cleanup just as much as a human would.
A clean shared-package change in one shot
To make this concrete, here's the kind of workflow this setup enables. Imagine adding a new variant to a button in packages/ui.
# 1. Hop into the workspace
cd packages/ui
# 2. Have Claude Code make the edit (entirely inside the editor)
# 3. Run tests for every dependent package in one command
pnpm --filter ...@myorg/ui test
# 4. Verify builds still pass downstream
pnpm --filter ...@myorg/ui build
# 5. Check what depends on the change at all (great pre-PR sanity check)
pnpm --filter ...@myorg/ui --json list --depth -1You can simply tell Claude Code, "after editing this component, run tests for the packages that depend on it." If the --filter ... syntax is documented in CLAUDE.md, it'll naturally pick the right command and execute the verification chain on its own. The fifth step — listing dependents — isn't something you run every time, but I find it worth surfacing whenever the change has any chance of being a breaking one.
For larger codebases, pair this approach with the monorepo and Turborepo integration guide so you can cache builds across packages. And once your repo grows past what's comfortable to navigate manually, the large codebase mastery guide goes deeper into context strategy and how to let Claude Code work effectively against a directory tree it can't load all at once.
What to do next
Start by pasting that permissions block into .claude/settings.json. It takes five minutes, and the next session you start will have noticeably fewer "Allow?" interruptions. You'll save dozens of clicks per session, which translates into longer stretches of uninterrupted thinking — easily the part of this setup with the highest return per minute.
Then, add three to five lines to CLAUDE.md that capture the pnpm patterns you actually use most. After that small investment, Claude Code will behave like a teammate who genuinely understands pnpm workspaces, not a generalist guessing at scope. The next time you ask it to "fix the regression in the admin app," you'll find that the changes stay where you wanted them — and the dependent test suite runs without you having to remind it.