Every command, every flag, every caveat.
This page is generated from the project's
docs/
folder. New to the tool? Start with the
overview and quick start instead — come back here for the full
reference.
Installation
npm install -g claude-modules # global CLI
Or run it without installing: npx claude-modules --help.
Building from source is covered in the README; see Configuration below for the environment variables.
Configuration
| Variable | Default | What it points at |
|---|---|---|
CLAUDE_MODULES_HOME |
~/.claude-modules |
Where your modules and the marketplace registry live |
CLAUDE_CONFIG_DIR |
~/.claude |
Claude Code's own home — read for the user scope and its plugin caches |
How claude-modules stores things, how scopes resolve, and how composition is computed.
Modules
A module is a directory under $CLAUDE_MODULES_HOME/modules/<name>/. Today it holds a single settings.json; whole directories are archived by export so this stays correct as modules grow.
$CLAUDE_MODULES_HOME/ # ~/.claude-modules by default
├── settings.json # the global marketplace registry (see below)
├── user.modules # the user scope's module list
└── modules/
├── base/settings.json
├── backend/settings.json
└── frontend/settings.jsonA module's settings.json has exactly four fields:
{
"version": "1.2.0",
"enabledPlugins": {
"typescript-lsp@claude-plugins": true,
"postgres-mcp@claude-plugins": true
},
"extraKnownMarketplaces": {
"claude-plugins": {
"source": { "source": "github", "repo": "anthropics/claude-plugins" }
}
},
"composedModules": ["base"]
}enabledPluginsandextraKnownMarketplacesmatch the exact shape Claude Code itself uses in itssettings.json, so a module is a portable slice of a real settings file.composedModulesis this tool's own addition — see Composition. It is omitted from disk when empty, and written last when present.versionis this tool's own addition too, and is advisory: nothing compares, gates, or migrates on it. It is printed bylistandinfoso you can tell two copies of a module apart. See Versioning.
Unknown keys are not preserved — the four fields above are reconstructed on every write, so a hand-added fifth key is dropped the next time any command touches the module. (Claude Code's own settings files are different: there, every unknown key is preserved untouched.)
Module names may not be empty and may not contain /, \, or ...
Two unrelated files are both called settings.json. A module's settings.json uses the four-field schema above. The registry's settings.json at $CLAUDE_MODULES_HOME/settings.json uses {"marketplaces": {...}}. Claude Code's own settings.json is a third thing again. They share a filename and nothing else.
extraKnownMarketplaces is double-nested
Note the repeated source key:
"claude-plugins": { "source": { "source": "github", "repo": "anthropics/claude-plugins" } }The outer wrapper is deliberate — it mirrors Claude Code's own entry shape, minus the machine-specific installLocation and lastUpdated fields, which are stripped when a source is copied out of Claude Code's cache.
Recognized source shapes:
| Shape | JSON |
|---|---|
| GitHub | {"source": {"source": "github", "repo": "owner/repo"}} |
| Git URL | {"source": {"source": "git", "url": "https://..."}} |
| Local path | {"source": {"source": "local", "path": "/path/to/mp"}} |
Anything else is treated as unrecognized, and therefore as not portable — export warns about it. See Marketplaces for the caveat on the git and local shapes.
Scopes
claude-modules writes into the same three settings files Claude Code reads:
| Scope | File | Needs a git repo? |
|---|---|---|
user | ~/.claude/settings.json, or $CLAUDE_CONFIG_DIR/settings.json if set | no |
project | <repo_root>/.claude/settings.json | yes |
local | <repo_root>/.claude/settings.local.json, or <cwd>/.claude/settings.local.json outside a repository | no |
--scope defaults to local everywhere it appears. The repository root is found by walking up from the current directory looking for a .git entry — file or directory, so worktrees and submodules both work.
Outside a repository, local falls back to the current directory and prints a warning saying where it landed. project has no such fallback; it errors.
Precedence
Claude Code resolves enabledPlugins with local > project > user. claude-modules models the same order, which is why its reports annotate an entry (<scope> — overridden by <scope>) when a more-specific scope explicitly disables it.
Managed settings outrank all three and can force a plugin on or off. That is invisible to this tool — status --verify exists to catch it.
What gets written
- Plugin keys are never deleted from a settings file, only flipped to
true/false. - Marketplaces are purely additive — existing entries are never removed or overwritten.
- Every other setting in the file (
permissions,theme, ...) is left untouched, including keys this tool doesn't recognize.
Composition
There are two different ways modules combine. Same word, two mechanisms.
Ad-hoc union (CLI-time)
Name several modules on one call and they're unioned for that call only:
claude-modules enable backend frontendNothing is recorded. Do it again next time you want the same combination.
Declared composition (persistent)
A module's own composedModules array names other modules it builds on. This is part of the module itself, so every future enable / disable / status / export against it transitively pulls in whatever those modules currently contribute:
claude-modules create fullstack --compose backend --compose frontend
claude-modules enable fullstack # gets backend, frontend, and anything they composeSet it at creation with --compose, or change it later with compose add / compose remove.
Resolution rules
- Children are resolved depth-first, then unioned as siblings.
- A module's own declarations win over anything it composes — both for
enabledPluginsand forextraKnownMarketplaces. - A composed child contributes only its enabled plugin keys. A child's
falsenever propagates upward; but the parent's ownfalsedoes survive, and suppresses a plugin the child enables. That is whatplugin uninstall --disablewrites. - Cycles are rejected —
Composition cycle detected: a -> b -> a.A module cannot compose itself, directly or transitively.
Marketplace conflicts behave two different ways
This distinction matters:
| Where | Behavior |
|---|---|
| Two sibling modules declare the same marketplace name with different sources | Error. The whole call is rejected before anything is written. |
A module's marketplace differs from one already in the target settings.json | Warning. The existing value is kept and the write proceeds. |
The first protects you from an incoherent module set. The second refuses to clobber a settings file you may have edited deliberately.
Plugin keys are never conflict-checked — the union is a plain OR.
Validation timing
create --compose and compose add both fully resolve the hypothetical result before writing, so a self-reference, a missing module, a cycle, or a sibling marketplace conflict rejects the call atomically.
Hand-edits are not validated when you make them. Edit composedModules directly and a typo, a cycle, or a conflict surfaces only on the next command that resolves the module.
Known limit: overrides don't propagate past one level
plugin uninstall --disable only beats a directly composed child — resolve it into a grandparent's composition and the override evaporates. See Composing modules → The one-level override limit for the full detail, including the related silent-resolution behavior for disagreeing siblings.
Versioning
version is a semver string bumped automatically whenever a command changes what a module declares.
| Bump | Commands |
|---|---|
| Patch — the module gained something | plugin install, marketplace add --module, compose add |
Minor (patch reset to 0) — the module lost something | plugin uninstall (with or without --disable), marketplace remove --module, compose remove |
| None | marketplace add / remove without --module (global registry only); export / import; any no-op path that logs a warning |
- A new module always starts at
1.0.0—create, with or without--from-scope/--compose, regardless of how much it's seeded with. - Major is never bumped by the tool. There's no CLI command to set a version directly: hand-edit
versionand the next bump increments from whatever you set, so a hand-edited2.0.0becomes2.0.1after the nextplugin install. - An unparseable version silently falls back to
1.0.0before bumping.
The marketplace registry
$CLAUDE_MODULES_HOME/settings.json is a small global lookup of marketplace name → source:
{
"marketplaces": {
"claude-plugins": {
"source": { "source": "github", "repo": "anthropics/claude-plugins" }
}
}
}It's populated by marketplace add and consulted by plugin install, so you don't have to repeat --source every time. Unknown top-level keys in this file are preserved.
Sources resolved out of the registry are copied onto the module as a snapshot, not referenced. Later changes to the registry don't propagate.
Module lists
An optional file — one module name per line — that remembers which modules you applied, so reload can reapply them and status can tell you when a scope has drifted. One list per scope, each living with the thing it describes:
| Scope | Module list | |
|---|---|---|
user | $CLAUDE_MODULES_HOME/user.modules | global; not tied to any repository |
project | <repo_root>/.claude-modules | shared with the team — commit it |
local | <repo_root>/.claude-modules.local, or <cwd>/.claude-modules.local outside a repository | personal to your checkout — gitignore it |
Blank lines and # comments are ignored on read.
Repo-scoped lists sit at the repository root and are found by walking up from the current directory, so reload works from any subdirectory. The user list has one fixed location, so reload --scope user works anywhere, including outside a repository.
Each scope reads only its own file. There is no cross-scope fallback, so a list is never applied at a scope it wasn't written for, and enable --scope X --save, reload --scope X, and status --scope X always agree.
Context budget
A secondary benefit: every enabled plugin adds tools Claude has to choose between on every turn, and Claude's own documentation is explicit that this has a cost:
"Tool selection accuracy degrades with more than 30-50 tools loaded at once." — Claude Code docs: Scale to many tools with tool search
Scoping your tools to the role you're currently performing is the fix, and modules make that practical.
The caveat: Claude Code's MCP tool search — on by default — already defers MCP tool schemas from context and loads them on demand. So the context-budget argument is weaker for MCP-heavy plugins specifically than it is for skills, slash commands, subagent definitions, hooks, and LSP servers, which tool search doesn't touch.
The reproducibility, composability, and drift-detection value of modules is unaffected either way — which is why this README leads with those instead.
No token-cost preview
claude-modules reports plugin counts, not tokens. Claude Code knows the number — claude plugin details <plugin> prints a "Projected token cost" — but there's no way to aggregate it per module yet: the command has no --json output, and it only resolves plugins that are already enabled, so it can't cost a module you haven't applied.
list
claude-modules listLists every module under $CLAUDE_MODULES_HOME/modules, with its version, its count of enabled plugins, and its count of known marketplaces.
backend (v1.2.0) — 2 plugin(s) enabled, 1 marketplace(s)
base (v1.0.1) — 1 plugin(s) enabled, 1 marketplace(s)
frontend (v1.0.0) — 1 plugin(s) enabled, 1 marketplace(s)Counts are the module's own declarations, not what it effectively contributes once composed modules are resolved in. Plugins explicitly set to false are not counted as enabled.
With no modules yet:
No modules found. Create one with 'claude-modules create <name>'.This command never writes.
info
claude-modules info <module>Shows a module's full detail: what it composes, every plugin it declares (with whether that declaration is enabled or disabled), and every marketplace it knows about with its raw source JSON.
backend (v1.2.0): composes 1 module(s):
base
backend (v1.2.0): 2 plugin(s):
typescript-lsp@claude-plugins (enabled)
postgres-mcp@claude-plugins (enabled)
backend: 1 marketplace(s):
claude-plugins: {"source":{"source":"github","repo":"anthropics/claude-plugins"}}The composes block is printed only when composedModules is non-empty.
Like marketplace list --module, this reads the module's own file directly — it shows what the module declares, not what it effectively contributes once composition is resolved. To see the resolved result, apply it and read the report, or use status.
This command never writes.
create
claude-modules create <module> [--from-scope user|project|local] [--compose <module>]... [--dry-run]Creates a new module. Errors if the name is already taken.
| Option | Default | Effect |
|---|---|---|
--from-scope <scope> | unset — empty module | Seed from that scope's live settings.json |
--compose <module> | none | Build on another module (repeatable) |
--dry-run | off | Report what would be created, write nothing |
claude-modules create backend
claude-modules create backend --from-scope local
claude-modules create frontend --compose base
claude-modules create fullstack --compose backend --compose frontendNew modules always start at version 1.0.0, however much they're seeded with.
--from-scope
A shortcut for capturing a scope you've already configured by hand, or through /plugin, without re-typing every <plugin>@<marketplace> through plugin install:
cd ~/projects/api-service
claude-modules create backend --from-scope localIt's a shortcut, not the recommended way to build a module: a scope tends to accumulate plugins for whatever you were doing at the time, so seeding from it can just as easily capture a bundle that mixes unrelated concerns (backend and frontend plugins together, say) as it can a clean one. Building a module deliberately, one plugin install at a time, keeps it scoped to a single concern and easier to compose later — see the example workflow in the README.
Only plugins actually enabled (true) there are captured. An explicit false is an override — that's how enable --only suppresses a plugin inherited from a broader scope — not a member of the set that scope uses, so it's skipped rather than recorded as disabled.
The scope is only read, never written.
A marketplace with a non-github source is warned about, since it won't mean anything on another machine.
--compose
Declares a persistent relationship, stored on the new module as composedModules — distinct from the ad-hoc union enable backend frontend performs at CLI time. See Composition.
--compose is independent of --from-scope; combine both to seed from a live scope and declare a composition.
Before anything is written, a self-reference, a missing --compose target, a composition cycle, or a marketplace conflict between two composed modules is rejected. --compose only sets the composition at creation time — use compose add / compose remove to change it afterward.
Created module 'fullstack' with 0 plugin(s), 0 marketplace(s), and 2 composed module(s).remove
claude-modules remove <module> [--dry-run]Deletes a module's directory, recursively. Idempotent — if the module doesn't exist, this logs a warning rather than failing.
It only removes the module itself. It does not:
- touch any scope's
settings.json— plugins already enabled stay enabled until the nextenable/reloadthat omits this module - clean up references to the module in any module list
If the module is still listed in one of this machine's saved module lists, a warning names the file. That can't reach lists on other machines or other repos, but it catches the common case. reload also names the specific list file if it later hits a module that was removed out from under it.
Changing which modules a module builds on, after it's been created. For how composition is resolved, see Concepts → Composition.
compose add
claude-modules compose add <module> <composed...> [--dry-run]Adds one or more composed modules to an existing module's composedModules — the CLI path to change composition after create, short of hand-editing settings.json.
claude-modules compose add frontend base
claude-modules compose add frontend base shared-toolsEvery future enable / disable / status / export against <module> transitively pulls in whatever the newly-composed module(s) contribute.
Validated atomically. A self-reference, a composition cycle, a missing composed module, or a sibling marketplace conflict rejects the entire call — nothing is written, even if some of the named modules would have been fine on their own. This is the same validation create --compose runs.
Idempotent per name. A composed module already present is skipped and noted in the output. If every named module is already composed, this is a no-op that logs a warning rather than failing.
Bumps the module's patch version once per call, regardless of how many modules were added. A no-op call bumps nothing.
compose remove
claude-modules compose remove <module> <composed...> [--dry-run]The reciprocal: removes one or more composed modules from an existing module's composedModules.
claude-modules compose remove frontend shared-toolsIdempotent per name. A module not currently composed is skipped and noted. If none of the named modules are composed, this is a no-op that logs a warning rather than failing.
Never re-validates composition afterward — removing entries can only shrink the effective set, never introduce a new cycle or conflict.
Bumps the module's minor version (patch reset to 0) once per call.
Hand-editing
The only way to bypass validation is to edit a module's settings.json directly. claude-modules does not validate a hand-edit when it's made: a typo, a cycle, or a conflicting marketplace surfaces as an error only on the next command that resolves the module — not at edit time.
Note also that composedModules must be an array of strings. A hand-edited "composedModules": "base" is rejected with an invalid-shape error rather than being coerced.
The one-level override limit
plugin uninstall --disable writes an explicit false into a module, which suppresses a plugin that a directly composed child enables.
That override does not propagate past one level. Resolve the overriding module into a grandparent's composition and it evaporates — the grandparent sees no opinion either way, not an explicit false.
In the same vein, two composed siblings that disagree on a plugin key resolve silently rather than erroring the way a marketplace conflict does: the enabling sibling wins, without warning.
Neither case is common enough today to justify reworking composition's internal representation to track a real enabled / disabled / unmentioned tri-state. If you build a deep composition tree that depends on multi-level exclusion, know the limit.
Running claude-modules plugin with no subcommand prints this group's usage.
plugin install
claude-modules plugin install <module> <plugin>@<marketplace> [--source '<json>'] [--dry-run]Enables a plugin inside a module.
claude-modules plugin install backend typescript-lsp@claude-pluginsBumps the module's patch version.
Marketplace resolution
If the plugin's marketplace isn't already known to the module, its source is resolved in this order:
- an explicit
--source '<json>'on this call — stored on this module, and also registered with Claude Code directly (claude plugin marketplace add, best-effort, same semantics as cache warming below) if it isn't already inknown_marketplaces.json - the global marketplace registry
- Claude Code's own local
known_marketplaces.jsoncache — so if you've already added the marketplace via/plugininside Claude Code, this picks it up automatically - otherwise the command fails, naming the remedies above
Whichever wins, the source is copied onto the module as a snapshot, not referenced. A non-github source is warned about, since it won't mean anything on another machine.
claude-modules plugin install backend foo@custom-mp \
--source '{"source":{"source":"github","repo":"me/custom"}}'Cache warming
Enabling a plugin in settings.json isn't enough on its own — Claude Code also needs the plugin's content materialized in its own cache ($CLAUDE_CONFIG_DIR/plugins/), or a new session in a repo using it fails with Plugin "<name>" not cached at <marketplace-dir>.
To prevent that, plugin install also runs claude plugin install <plugin>@<marketplace> --scope user -y on your behalf — but only if the plugin isn't cached yet and its marketplace is already known to Claude Code.
This never happens silently. A plugin that was missing and got installed is logged explicitly, and any failure (no claude on PATH, no network, unknown marketplace) is a warning, not a hard error — the module is saved either way.
-y accepts a marketplace-declared "run a command to install" plugin's command without showing it to you first. Fine for the official marketplace; worth knowing for third-party ones.
Because caching is best-effort, a plugin can end up enabled without actually being cached. Every command that writes enabledPlugins reports this inline, annotating the plugin (not cached by Claude Code — run ...), and status answers the same question on demand without writing anything.
enable --install and reload --install re-check this too, so a plugin enabled by hand-editing a module file, or one synced in from another machine, still gets cached before you hit the error in a real session. Plain enable / reload do not attempt it; they only report what's missing.
plugin uninstall
claude-modules plugin uninstall <module> <plugin>@<marketplace> [--disable] [--dry-run]The reciprocal of plugin install: disables a plugin inside a module.
claude-modules plugin uninstall backend typescript-lsp@claude-pluginsBy default it deletes the key from the module's enabledPlugins. It deliberately does not:
- run
claude plugin uninstall— Claude Code's plugin cache is shared across modules, so other modules on this machine may still need the plugin cached - remove the plugin's marketplace from
extraKnownMarketplaces, even if it was the last plugin referencing it
If the plugin isn't currently enabled in the module, this is a no-op that logs a warning rather than failing, and bumps nothing. Otherwise it bumps the module's minor version (patch reset to 0).
--disable
claude-modules plugin uninstall backend typescript-lsp@claude-plugins --disableSets the key to false instead of deleting it.
| Meaning | |
|---|---|
plain uninstall | "this module says nothing about the plugin" |
--disable | "this module explicitly turns it off" |
The difference only matters under composition: a module's own explicit false takes precedence over what a composed module contributes, but a merely-absent key has no such effect.
Unlike plain uninstall, --disable isn't gated on the plugin already being enabled in this module — that's the point, since the plugin it's meant to override typically isn't declared here at all. The only true no-op is a plugin that's already explicitly false.
Also bumps the minor version.
The override only survives one level of composition. See the one-level override limit.
Note that a module's false removes the plugin from the computed union — it doesn't actively turn the plugin off in a settings file that already has it true. Only enable --only resets that.
Marketplaces live in two places: the global registry at $CLAUDE_MODULES_HOME/settings.json, and each module's own extraKnownMarketplaces. Every command here targets the global registry by default, and a module instead when given --module.
Running claude-modules marketplace with no subcommand prints this group's usage.
marketplace add
claude-modules marketplace add <spec> [--name <name>] [--source '<json>'] [--module <module>] [--dry-run]Registers a marketplace, mirroring /plugin marketplace add <spec>, so plugin install can resolve it without repeating --source.
claude-modules marketplace add anthropics/claude-plugins
claude-modules marketplace add anthropics/claude-plugins --name official
claude-modules marketplace add anthropics/claude-plugins --module backend| Option | Default | Effect |
|---|---|---|
--name <name> | inferred from the spec | Override the marketplace name |
--source '<json>' | auto-detected | Skip detection; store this source verbatim |
--module <module> | the global registry | Register onto this module instead |
--dry-run | off | Resolve and report, write nothing |
With --module, bumps that module's patch version. Without it, only the global registry changes and no module's version is touched.
Syncing to Claude Code
If the marketplace isn't already known to Claude Code ($CLAUDE_CONFIG_DIR/plugins/known_marketplaces.json), this also runs claude plugin marketplace add <spec> --scope user on your behalf — the same best-effort cache-warming plugin install already does for plugins. A freshly-added marketplace is logged explicitly; a failure (no claude on PATH, no network) is a warning, not a hard error — the registry write above still succeeds either way.
Spec detection
| Spec form | Detected as |
|---|---|
owner/repo | GitHub shorthand |
owner/repo@v2.0 | GitHub shorthand pinned to a branch/tag — the name is still inferred from owner/repo alone |
https://... or git@... | git URL (pin with a trailing #ref) |
| anything else | local path |
A local path ending in marketplace.json infers its name from the parent directory; a trailing / or .git is stripped.
owner/repo#ref is an error, not a silent misparse — GitHub shorthand doesn't support #ref pinning. The message steers you to owner/repo@ref or the full git URL.
Caveat. Only the GitHub shorthand has a documented settings.json representation. The git-URL and local-path shapes this tool writes are unverified against real Claude Code output. Check the registered entry with marketplace list, or bypass detection entirely with --source '<json>'.
marketplace remove
claude-modules marketplace remove <name> [--module <module>] [--dry-run]The reciprocal of marketplace add.
claude-modules marketplace remove official
claude-modules marketplace remove official --module backendIdempotent — if the marketplace isn't registered on the selected target, this logs a warning rather than failing.
Without --module, it only edits the global registry. It does not touch any module's extraKnownMarketplaces (those were copied as a snapshot, not a live reference) and does not touch Claude Code's own known_marketplaces.json. A future plugin install that needs this marketplace and can't fall back to Claude Code's cache will fail until it's registered again.
With --module, it only edits that module's own map. If the module inherited the marketplace from a composed module, it isn't in the module's own map, so this reports it as not registered rather than reaching into the composed module.
Version: --module bumps that module's minor version on a successful removal. The idempotent no-op path bumps nothing, and neither does the global-registry-only form.
marketplace list
claude-modules marketplace list [--module <module>]Lists registered marketplaces — the global registry by default, or a single module's own extraKnownMarketplaces with --module. Each line shows the name and its raw source JSON.
claude-plugins: {"source":{"source":"github","repo":"anthropics/claude-plugins"}}Like the other --module forms, this shows the module's own map, not marketplaces it inherits from a composed module.
This command never writes.
These are the commands that write Claude Code's own settings.json. For which file each scope maps to, see Concepts → Scopes.
Changes apply to the next session. Claude Code reads enabledPlugins at session start, so these commands have no effect on a session that's already open — which looks exactly like the command having done nothing. Run /reload-plugins in that session (add --force if it warns about the prompt cache), or start a new one.
enable
claude-modules enable <module...> [--scope user|project|local] [--only] [--install] [--save[=<path>]] [--dry-run]Computes the union of enabled plugins and known marketplaces across the given modules — resolving each module's composition transitively — and writes it into the target scope.
claude-modules enable backend
claude-modules enable backend shared-tools --scope project --save
claude-modules enable backend --save=./config/team-modules.list
claude-modules enable backend --scope project --only
claude-modules enable backend --installDefault (additive) behavior
- Every plugin in the union is enabled. Every other plugin is left exactly as it was, so a later
enablefor a different module adds to what's already active in that scope rather than replacing it. - Marketplaces in the union are added if missing; existing entries are never removed or overwritten. A differing pre-existing source is warned about and kept.
- Every other setting in the file is untouched.
Enabled module(s) [backend] in project scope (/repo/.claude/settings.json); 1 marketplace(s) known.
Enabled plugin(s):
- code-review@claude-plugins (project)
- postgres-mcp@claude-plugins (project — not cached by Claude Code — run 'claude plugin install postgres-mcp@claude-plugins --scope user -y')
- typescript-lsp@claude-plugins (project)
Plugin(s) not cached by Claude Code:
claude plugin install postgres-mcp@claude-plugins --scope user -y
Run with --install to attempt these automatically, or install manually with the command(s) above.
Already running a Claude Code session? It won't pick this up on its own — run /reload-plugins in it (add --force if it warns about the prompt cache), or start a new session.
Saved module selection to /repo/.claude-modules.
Modules active in project scope: [backend].(code-review@claude-plugins is there because backend composes base, which declares it.)
The report
The Enabled plugin(s): block covers every scope in effect — local/project/user inside a repository, local/user outside one — each plugin tagged with its scope, colour-coded, and annotated (<scope> — overridden by <scope>) when a more-specific scope explicitly disables it. It always covers the whole chain regardless of --scope, since that's what decides which plugins a session actually loads — enable --scope user inside a repository still shows what local there contributes on top.
The Modules active in <scope> scope: line comes from the scope's saved list (updated first, if --save was given), or this run's own module names — noted as not persisted — when no list exists.
Options
| Option | Default | Effect |
|---|---|---|
--scope user|project|local | local | Which settings.json to write |
--only | off | Make the scope exactly these modules instead of adding to it |
--install | off | Attempt to add missing marketplaces and install missing plugins into Claude Code's caches |
--save[=<path>] | off | Update the scope's saved module list for reload |
--dry-run | off | Compute and print the full report, write nothing |
project requires a git repository — pass --scope user or --scope local outside of one. local itself needs no repository: outside one it resolves against the current directory instead, and a warning is printed so it's clear where it landed.
--only
Makes the target scope exactly the given modules:
- every plugin not in the union is explicitly disabled in this scope's own file
- for
local/project, any plugin enabled in a broader scope but not in the union is also disabled here (projectanduserforlocal; justuserforproject)
The broader scope's file is only ever read, never written. The output names which plugins were overridden and which scope they came from. At user scope there is nothing broader to override, but this scope's own plugins are still made exact.
This is the flag to reach for when you want an exclusive role switch.
--install
Attempts, in order:
- to add to Claude Code's
known_marketplaces.jsonany union marketplace it doesn't know — oneclaude plugin marketplace add <source> --scope userper marketplace - to install into Claude Code's plugin cache any union plugin it hasn't cached — one
claude plugin install <plugin> --scope user -yper plugin, attempted only when the plugin's marketplace is already known to Claude Code
Marketplaces are always attempted first, so a plugin from a marketplace added in the same run still gets a chance to install. Scope is always user for both, since Claude Code's caches are shared across scopes.
Off by default: a plain enable never shells out to claude; it only reports what's missing.
If a marketplace or plugin is still missing after the attempt, enable exits with code 2 — unless --dry-run was also given, in which case nothing was attempted and the exit code stays 0.
--save
Updates the scope's saved module list, one name per line, for later use by reload. The selected modules are merged into whatever the list already had, matching the additive default. (--only --save replaces the list instead, matching --only's exact-set semantics.)
- Bare
--savewrites to that scope's own list — see the module-list table. Saving at one scope never overwrites another's. --save=<path>writes to<path>instead; relative paths resolve against the current directory, not the repository root. A custom path is only ever read back withreload --file <path>— barereloadlooks only at the per-scope locations and will not find it. The merge base is still read from the scope's canonical list, if one exists.- The value must be given with
=(--save=path), not as a following argument (--save path) —enabletakes a variadic list of module names, so a space-separated value would be ambiguous with the next one.
disable
claude-modules disable <module...> [--scope user|project|local] [--save] [--dry-run]The reciprocal of enable: computes the union of enabled plugins across the given modules and flips each one to false in the target scope.
claude-modules disable backend --scope project
claude-modules disable backend --save- Only plugin keys already present in that scope's
settings.jsonare touched — a plugin never enabled there has nothing to disable, and no phantomfalseis created. - A touched key is kept and set to
false, never deleted. - Marketplaces and every other setting in the file are untouched.
- A module that enables no plugins is a no-op that logs a warning rather than failing.
--save removes the given modules from the scope's saved list, if one exists. A name not in the list, or no list at all, is a no-op with a warning. Unlike enable --save, it always targets the scope's own canonical list and does not take a path — --save=<path> is an explicit error.
Afterward it prints the same report enable does, plus the Modules active in <scope> scope: line.
disable-all
claude-modules disable-all [--scope user|project|local] [--dry-run]Disables every plugin key currently known to the target scope's settings.json, without needing a module — the bulk equivalent of disable.
claude-modules disable-all --scope localKeys are kept and set to false, never deleted. Marketplaces and every other setting are untouched. Afterward it prints the same report enable does.
reload
claude-modules reload [--scope user|project|local] [--file <path>] [--install] [--dry-run]Re-applies a previously-saved list of module names — equivalent to running enable with that same list.
claude-modules reload --scope project
claude-modules reload --file ./config/team-modules.list
claude-modules reload --install- Reads the list belonging to
--scope— exactly whatenable --scope <same> --savewrote. - Repo-scoped lists are found by walking up from the current directory to the repository root, so this works from any subdirectory. The user list has one fixed location and needs no repository.
localoutside a repository looks only at the current directory itself. - Each scope reads only its own file. There is no cross-scope fallback, so a list is never applied at a scope it wasn't written for.
--file <path>reads the list from<path>directly, skipping the search; relative paths resolve against the current directory.--installattempts the same best-effort cachingenable --installdoes — same flag, same semantics, same exit-code-2 contract.
If a listed module no longer exists, the error names the specific list file the stale name came from.
Behavior change. reload used to attempt the caching step unconditionally, with no way to turn it off. It now defaults to off and only attempts it when --install is passed, for symmetry with enable. If you relied on reload silently re-caching plugins, add --install.
update
claude-modules update [module...] [--scope user|project|local] [--dry-run]Resolves the union of enabled plugins and known marketplaces across the given modules — the same transitive composition enable/disable/reload compute — and asks Claude Code itself to bring each one up to date, marketplaces first:
claude plugin marketplace update <name>, one per union marketplaceclaude plugin update <plugin> --scope <scope> -y, one per union plugin
claude-modules update backend-dev
claude-modules update backend-dev frontend-dev
claude-modules update --scope userUnlike every other command on this page, update never writes a settings.json — a module's own file or any scope's. It doesn't change what's enabled, only what version of it Claude Code has installed.
With no module name given, it updates whatever modules are currently active for --scope instead — the same saved list reload would read back. It errors if that scope has no saved list.
--scope (default local) serves two purposes: which saved module list to read (when no name is given), and the --scope passed to claude plugin update — but the two claude subcommands this drives treat it differently:
claude subcommand | --scope's effect |
|---|---|
claude plugin marketplace update | not passed — marketplaces aren't scoped |
claude plugin update | passed through; defaults to user when run directly, not local |
If your plugins actually live at user scope (the common case for something installed once and shared across repos), pass --scope user explicitly.
Best-effort per item, like enable --install: one marketplace or plugin failing to update (network error, nothing installed at that scope, ...) is logged as a warning and doesn't stop the rest of the run. If anything failed and this wasn't a --dry-run, update exits with code 2 afterward — same contract as enable --install and status.
An already-open Claude Code session doesn't pick up an updated plugin's new code on its own — claude plugin update itself notes "restart required to apply" — so restart the session (or start a new one) afterward. This is distinct from the /reload-plugins hint above, which is about enabledPlugins changes, not plugin version changes.
status
claude-modules status [--scope user|project|local] [--verify] [--json]Read-only audit of a scope's live settings.json against two independent sources of truth — three with --verify. Every check always runs and reports, so none hides another, and status writes nothing.
claude-modules status
claude-modules status --scope project
claude-modules status --verify
claude-modules status --json--scope defaults to local. project requires a git repository; local does not.
Check 1 — Claude Code's plugin cache
Prints the same report enable does, additionally cross-checking every effectively enabled plugin (one not overridden by a more-specific scope) against Claude Code's cache and annotating it when missing.
This is the check plugin install / enable --install / reload --install can't give you after the fact: their caching step is best-effort by design, so a caching failure never blocks the settings write and their own success output can't tell you whether it actually happened.
Check 2 — the scope's module list
Found the same way reload finds it, so status --scope X always grades itself against whatever enable --scope X --save wrote — never another scope's list. Unlike reload, a missing file isn't an error; it just means there's nothing to compare against.
When found, its listed modules are resolved exactly like enable would, and diffed against the scope's own settings.json:
- Missing — a listed module wants a plugin enabled, but it isn't.
- Stale — a plugin is enabled, but no listed module declares it. Explicit-
falseentries left byenable --onlyare never reported as stale, since they were never "enabled" to begin with.
Check 3 — Claude Code's own resolution (--verify only)
Runs claude plugin list --json and diffs its enabled set against the one computed above.
Worth the subprocess because precedence can be decided somewhere this tool can't see: it models scope precedence as local > project > user, but managed settings outrank all three and can force a plugin on or off. On a machine with an administrator policy, the report from checks 1–2 can be confidently wrong, and this is the only way to notice.
Off by default, so plain status keeps its guarantee of running nothing external. A --verify that can't run — no claude on PATH, unparseable output — is a warning, not drift: it never changes the exit code.
--verify behaves identically at all three scopes. Both sides describe the same thing — what a session started in this directory would load — because the report always covers the full local > project > user chain regardless of --scope.
Example
Enabled plugin(s):
- code-review@claude-plugins (project)
- postgres-mcp@claude-plugins (project — not cached by Claude Code — run 'claude plugin install postgres-mcp@claude-plugins --scope user -y')
- typescript-lsp@claude-plugins (project)
Module list /repo/.claude-modules (modules: backend) vs project scope (/repo/.claude/settings.json):
In sync — every enabled plugin matches the listed module(s).
Error: 1 plugin(s) not cached by Claude Code: postgres-mcp@claude-plugins. Run 'claude-modules enable --install' or 'reload' to re-cache them, or install manually with 'claude plugin install <plugin>@<marketplace> --scope user'.With no module list for the scope, check 2 reports that and skips instead:
No module list found for local scope (/repo/.claude/settings.local.json) — skipping module-drift check.Exit codes
The exit code distinguishes "ran fine" from "found a problem" from "couldn't even check", so status is usable as a CI or pre-session gate, not just a log line to notice in the moment.
| Code | Meaning |
|---|---|
0 | Clean: every effectively-enabled plugin is cached, settings.json matches the listed module(s) if any, and — with --verify — Claude Code's own resolution agrees |
1 | status itself couldn't run (bad --scope, no repo root, ...) |
2 | It ran fine, but found a problem — uncached plugins, missing/stale plugins relative to the listed modules, unresolvable listed modules, or a --verify disagreement |
--json
Suppresses the human-readable report and prints one JSON object to stdout instead. The exit-code contract is unchanged, so it stays usable as a CI gate while also being scriptable.
{
"ok": false,
"scope": "project",
"settingsPath": "/repo/.claude/settings.json",
"checks": {
"cache": { "uncachedPluginKeys": ["postgres-mcp@claude-plugins"] },
"moduleList": {
"listFilePath": "/repo/.claude-modules",
"resolutionFailed": false,
"missingPluginKeys": [],
"stalePluginKeys": []
},
"verify": null
}
}checks.moduleList.listFilePathisnullwhen the scope has no module list.checks.verifyisnullunless--verifywas passed; otherwise{ "ran": true, "unavailable": false, "unexpectedlyEnabled": [], "unexpectedlyDisabled": [] }.unavailable: truemeans theclaude plugin list --jsoncross-check itself couldn't run — the warning that would normally explain why is suppressed under--json, since that signal relocates into this field.okmirrors the exit-code decision:falseexactly when the command would exit2.
In that case the human-readable problem summary still prints on stderr, so a script reading only stdout always sees clean JSON.
export
claude-modules export <module> [--output <path>] [--dry-run]Packages a module's directory — and the directory of every module it transitively composes — into a single .tar.gz, so the whole composition chain travels together and works out of the box after import on another machine.
claude-modules export backend
claude-modules export fullstack --output ~/backups/fullstack.tar.gzWithout --output, the archive is written to <module>-<YYYY-MM-DD>.tar.gz in the current directory, date-stamped in local time.
[dry-run] Would export module 'fullstack' and 3 composed module(s) (backend, base, frontend) to '/repo/fullstack-2026-08-21.tar.gz'.Note that the composed set is the full transitive closure — fullstack composes only backend and frontend, but base comes along because both of those compose it.
A composition cycle among the modules being archived is rejected before anything is written.
If any archived module has a marketplace with a non-github source, a warning is logged — same as create --from-scope and plugin install — since that marketplace won't mean anything on another machine.
export does not bump any version: transferring a module isn't changing it.
Archive format
manifest.json
modules/<root>/settings.json
modules/<composed>/settings.json
...The modules/ tree mirrors $CLAUDE_MODULES_HOME/modules/ — one directory per module, whatever it contains. manifest.json records which module is the root and which are composed:
{
"version": 1,
"rootModule": "fullstack",
"composedModules": ["backend", "base", "frontend"],
"exportedAt": "2026-08-21T12:00:00.000Z"
}That manifest is what lets import --name / --composed-prefix rename modules on the way in without losing track of which directory is which. It's an implementation detail, not something you need to read or edit. An archive with a different manifest version is rejected with a message telling you to upgrade claude-modules.
import
claude-modules import <archive> [--name <name>] [--composed-prefix <prefix>] [--dry-run]Unpacks a module — and its composed modules — from an export archive. The reciprocal of export.
claude-modules import backend-2026-08-21.tar.gz
claude-modules import backend-2026-08-21.tar.gz --name backend-imported
claude-modules import backend-2026-08-21.tar.gz --composed-prefix teammate-The root module is named from --name if given, otherwise from the name it was exported under.
Composed modules keep their original names unless --composed-prefix is given, in which case every composed module — at every level of the composition tree, not just the root's immediate children — is renamed with that prefix, and every module's composedModules references are rewritten to match, so composition keeps working after the rename.
Collisions
If any module being imported — root or composed — would collide with a module that already exists on this machine, nothing is written. The error lists every collision, and for each recommends either renaming the existing module, or re-running with --name (for a root collision) or --composed-prefix (for a composed-module collision).
--name / --composed-prefix are also rejected up front if they'd land two imported modules on the same final name.
--dry-run validates the archive and checks for collisions entirely in memory, writing nothing at all.
After the write
Modules are written leaves-first, root last, so a crash mid-import leaves the result reading as failed rather than as a broken visible root.
Post-write, composition is re-resolved. If the imported composition doesn't resolve — say two composed modules were hand-edited to declare the same marketplace differently — the import still succeeds with a warning rather than being rolled back.
import carries each module's version through unchanged.
What a module deliberately does not carry
Per-plugin configuration
Claude Code's per-plugin userConfig — set via claude plugin install --config or /plugin configure, and where an MCP server's API key or endpoint typically lives — is not captured by create --from-scope, export, or import.
This is deliberate, not an oversight. A module is designed to be shared and committed (the project-scope module list is meant to go into version control), and capturing userConfig by default would turn that into an accidental secret-exfiltration path.
A plugin that needs configuration to function arrives enabled but unconfigured after any of the above. Configure it separately with Claude Code's own tooling.
Plugin versions
A module records which plugins to enable, not which versions. Syncing one to another machine reproduces the plugin set, not the exact versions — Claude Code updates plugins on its own cadence, and pinning is the marketplace's job.
Treat modules as portable role definitions, not lockfiles.
Non-portable marketplace sources
When plugin install resolves a marketplace from the global registry or from Claude Code's own cache, it copies the source onto the module as a snapshot. A non-github source — a local path, say — won't mean anything on another machine. Both plugin install and export warn when they see one.
Plugin-level dependencies
Claude Code's marketplace plugin manifests support a dependencies field (auto-installed, semver-constrained; native enable/disable won't break them), but this tool only ever reasons about the literal plugin keys a module names — it has no awareness of a plugin's declared dependencies.
As of this writing no plugin in either official marketplace declares one, so this has no real-world impact today. Worth revisiting if that changes.
completions
claude-modules completions bash|zshPrints a tab-completion script for the given shell to stdout — completions itself writes nothing to disk; you choose how to load the output.
claude-modules completions bash
claude-modules completions zshCompletes:
- top-level commands (
list,enable,marketplace, ...) marketplace/plugin/composesubcommands (add,remove,list,install,uninstall)- every command's own flags (
--from-scope,--output,--save,--verify,--json, ...) --scope/--from-scopevalues (user,project,local), in both the--scope valueand--scope=valueforms
Positionals like <module>, <plugin>@<marketplace>, and <name> are not completed — the generated script never reads $CLAUDE_MODULES_HOME or shells out to anything, so it's safe to eval in any shell's startup file.
Bash
Add to ~/.bashrc:
eval "$(claude-modules completions bash)"Or write it out once instead of evaluating on every shell start:
claude-modules completions bash > /etc/bash_completion.d/claude-modulesZsh
Add to ~/.zshrc, after compinit has already run — the generated function only registers itself when compdef is defined, so an eval placed above compinit silently does nothing rather than erroring at shell startup:
autoload -Uz compinit && compinit
eval "$(claude-modules completions zsh)"If completions don't appear, check that ordering first.
Alternatively, skip the eval entirely: save the output as a file named _claude-modules somewhere in your $fpath, and compinit's normal autoloading picks it up on the next new shell.
Global options
| Flag | Effect |
|---|---|
-h, --help |
Show help. Works after any command: claude-modules plugin install --help |
-v, --version | Print the installed version |
--verbose | Enable debug logging |
--dry-run |
Preview a mutating command's effect — writes nothing, runs no external commands |
--dry-run is supported by all 15 mutating commands, and is a no-op on the five that
never write (list, info, status,
marketplace list, completions). Running a bare group name —
claude-modules plugin, marketplace, or compose — prints
that group's usage.
Known limitations
- No token-cost preview. Modules report plugin counts, not tokens. Claude Code knows the number, but there's no way to aggregate it per module yet. See Context budget.
- No version pinning. A module records which plugins to enable, not which versions. Treat modules as portable role definitions, not lockfiles.
- Marketplace sources are snapshots. Resolved sources are copied onto the module; later registry changes don't propagate.
-
Plugin configuration isn't captured. Per-plugin
userConfigis deliberately excluded so a committed module can't become a secret-exfiltration path. -
Composition overrides stop after one level. An explicit
falsebeats a directly composed child, but not a grandchild. See the one-level override limit.
This page is generated from
the project's docs/ folder
and the README. Found a mismatch?
Open an issue.