ensure-claude-plugins
v1.0.1
Published
Install the Claude Code plugins a repository declares in .claude/settings.json — silently, idempotently, never failing the caller.
Downloads
517
Maintainers
Readme
ensure-claude-plugins
Make a repository's declared Claude Code plugins actually installed on this machine — silently and idempotently.
It reads the repo's committed .claude/settings.json, and for every enabled entry it
shells out to the claude CLI to add the marketplace and install the plugin. A
gitignored stamp file makes every later run a no-op. It never fails its caller, so it can
sit in a prepare/postinstall script without ever breaking npm install.
$ npx ensure-claude-plugins
Setting up 3 Claude Code plugin(s) at local scope…
✔ context7@context7-marketplace
✔ efp@expofp
✔ nx@nx-claude-plugins
Claude Code plugins ready. Restart Claude Code to pick them up.
$ npx ensure-claude-plugins # second run
$ # silent no-opSecurity, up front, because this runs during npm install: it installs only what
the repository's own committed .claude/settings.json declares, adds no registry or
index of its own, and does nothing a developer running claude plugin install by hand
wouldn't do. Details in Security.
Why this exists
.claude/settings.json can declare enabledPlugins and extraKnownMarketplaces, and
the official docs say teammates then get prompted to install. Verbatim, from
Discover plugins § Configure team marketplaces
(fetched 2026-07-29, archived in
docs/reference/discover-plugins.md):
Team admins can set up automatic marketplace installation for projects by adding marketplace configuration to
.claude/settings.json. When team members trust the repository folder, Claude Code prompts them to install these marketplaces and plugins.As of Claude Code v2.1.195, this install step applies on every path that loads plugins. A plugin that only the project's
.claude/settings.jsonenables, and that comes from an external source such as a GitHub repository or npm package, doesn't load until the team member installs it. Until then, Claude Code reports the plugin as not installed and shows theclaude plugin installcommand to run.
Measured on Claude Code v2.1.220 (2026-07-29), this is what actually happens. A
session started in a checkout with all three plugins declared but none installed prompts
to trust the folder, and never prompts to install. /plugin reports:
❯ context7 (project)
Plugin "context7" not cached at /Users/…/.claude/plugins/cache/context7-marketplace/context7/1.0.2
Run /plugin to refresh the plugin cache
✘ nx (project)
Plugin "nx" is enabled in project settings but isn't installed here
Run `claude plugin install nx@nx-claude-plugins --scope project` to install it for this project
✘ efp (project)
Plugin "efp" is enabled in project settings but isn't installed here
Run `claude plugin install efp@expofp --scope project` to install it for this projectSo the first sentence of the docs does not hold; only the second does. Every developer
must run the install commands by hand, in every checkout. Worse, the command Claude Code
suggests uses --scope project, which rewrites the committed .claude/settings.json —
see Scopes. And the claude plugin CLI (checked on v2.1.220) has no bulk
command that reads the project settings: no sync, no install --all.
This package automates exactly that gap, and nothing else. The claim is dated on purpose — Claude Code moves fast. If it ships real auto-install, see Retirement plan.
Install
Add it as a devDependency and run it from prepare (works with npm, pnpm and yarn):
{
"devDependencies": { "ensure-claude-plugins": "^1.0.0" },
"scripts": { "prepare": "ensure-claude-plugins" }
}Already using husky? Chain it:
{ "scripts": { "prepare": "husky && ensure-claude-plugins" } }Install it as a devDependency; don't put a bare npx ensure-claude-plugins in
prepare. The tool's defining property is that it can never fail its caller, and a
bare npx in an install hook adds an unpinned registry fetch inside npm install — a
new failure mode for the one script that must not have any. npx ensure-claude-plugins
stays the documented one-off for ad-hoc use in a checkout that doesn't have it installed.
Requirements: Node ≥ 20. The claude CLI on PATH — if it isn't, the tool says so in
one line and exits 0.
Usage
ensure-claude-plugins [options]| flag | behaviour |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| --scope user\|project\|local | Install scope, default local — see Scopes |
| --force | Ignore the stamp file, reinstall everything |
| --check | Report what is missing, install nothing, write nothing, exit 1 if anything is missing — for CI |
| --dir <path> | Project root, default the current directory |
| --quiet | Suppress the success lines; failures still print |
| --help, --version | The usual |
Normal runs are a silent no-op when CI is set, when ENSURE_CLAUDE_PLUGINS_SKIP=1, or
when the claude binary isn't on PATH. --check deliberately ignores all three — its
whole purpose is to run on CI.
Scopes
The claude CLI defaults to user scope. This tool defaults to local, and the
difference matters more than it looks:
| scope | recorded in | consequence |
| ----------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| user | ~/.claude/settings.json | The plugin's skills, commands and hooks load into every Claude Code session on the machine. A repo-specific hook then fires in repositories that have nothing to do with it. |
| project | .claude/settings.json (committed) | Confined to the project — but the CLI rewrites the committed file in its own formatting while installing, even when the change is a semantic no-op. Every developer gets a dirty working tree from npm install. |
| local (default) | .claude/settings.local.json (gitignored) | Confined to the checkout, invisible to git. |
The project rewrite is not hypothetical. Measured diff on a repo whose settings were
Prettier-formatted, after an install whose enabledPlugins entry was already present
(a semantic no-op):
"permissions": {
- "deny": ["Read(docs/_old/**)"]
+ "deny": [
+ "Read(docs/_old/**)"
+ ]
},If a previous run installed at a different scope, the tool installs at the new scope and
then prints the exact claude plugin uninstall <id> --scope <previous> commands for the
copies left behind. It never uninstalls anything itself — at user scope that set is
shared with every other repo on the machine, where a plugin may have been wanted on its
own merits.
What to gitignore
.claude/.plugin-setup
.claude/settings.local.json.plugin-setup is this tool's stamp file; settings.local.json is where the claude
CLI records local-scope installs (and where your own local overrides live).
Forgetting this is the one mistake that bites quietly: a committed stamp tells every
fresh checkout that the plugins are already installed, so nothing installs and nobody is
told why. So a run that installs something asks git whether these files are ignored, and
prints the lines to add — plus git rm --cached if one is already committed. It never
edits .gitignore itself; that file is yours, and a tool that rewrites committed files
from inside pnpm install is the problem it set out to avoid. Runs that install nothing
skip the check entirely, so the everyday no-op stays silent.
--check in CI
--check verifies that everything the repo declares is actually installed on the machine
it runs on — for a container image or self-hosted runner that is supposed to have the
plugins baked in:
- name: Claude Code plugins present
run: npx ensure-claude-plugins --checkIt installs nothing, writes nothing, exits 1 listing whatever is missing, and runs even
where CI would make a normal run a no-op.
How it works
- Reads
<dir>/.claude/settings.json, then merges<dir>/.claude/settings.local.jsonover it — local wins key by key, including an explicitfalsethat disables an entry the committed settings enable. - For every
<plugin>@<marketplace>id enabled: one `claude plugin marketplace add - Writes
.claude/.plugin-setupwith one line per installed plugin:sha256(id|source|scope)truncated to 12 hex chars. Per-plugin, so one plugin nobody can install (a private marketplace, no credentials) retries alone instead of dragging the whole set through a retry-and-report on every run. Rotating a marketplace URL or changing scope changes the fingerprint and re-provisions everyone automatically. - Never hangs, never fails the caller: stdin ignored, output captured (shown only for
the plugin that failed),
GIT_TERMINAL_PROMPT=0, 120 s timeout per CLI call, and exit 0 on every path except--check.
Security
Claude Code plugins execute code with your privileges — Anthropic's own docs tell you to install plugins only from sources you trust. This tool does not change that calculus:
- It installs only what the repository's own committed
.claude/settings.json(plus your own gitignored.claude/settings.local.json) declares. - It has no registry, index or marketplace of its own, and zero runtime dependencies.
- It does nothing a developer following the repo's README and running
claude plugin installby hand wouldn't do — it just does it consistently.
Trusting a repo's declared plugins is a decision you make when you adopt the repo; this tool only executes that decision.
Troubleshooting
A GitHub-sourced plugin fails with Permission denied (publickey)
A plugin whose marketplace entry is {"source": "github", "repo": "owner/repo"} is
cloned by the claude CLI over SSH ([email protected]:owner/repo.git) — even for a
public repo, and even though the marketplace itself was just fetched over HTTPS. On a
machine with no GitHub SSH key the install fails outright, and the CLI has no flag to
pick the transport.
This tool fixes that by injecting url.https://github.com/[email protected]:
through git's environment config into the spawned CLI only — your own git config is
untouched. The one trade-off: a plugin hosted in a private GitHub repo then needs an
HTTPS credential helper (gh auth setup-git) where an SSH key would otherwise have
worked.
Plugin "x" not cached at … Run /plugin to refresh the plugin cache
Plugin content lives at ~/.claude/plugins/cache/<marketplace>/<plugin>/<version> and is
shared across scopes and projects. Uninstalling the last install of a plugin deletes
that directory, and any other install record still pointing at it then reports
not cached. Reinstalling repairs it: claude plugin install <id>, or
ensure-claude-plugins --force in the affected checkout.
Retirement plan
This package exists to work around a gap: Claude Code documents install-on-trust but doesn't do it, and its CLI has no bulk install that reads the project settings. If Claude Code ships that for real, this package will be deprecated with a pointer to the built-in mechanism, not defended.
License
MIT © ExpoFP
