Skip to content

Instantly share code, notes, and snippets.

@xiaolai
Created April 16, 2026 23:41
Show Gist options
  • Select an option

  • Save xiaolai/80fe974fd090aff89ceb91759b393165 to your computer and use it in GitHub Desktop.

Select an option

Save xiaolai/80fe974fd090aff89ceb91759b393165 to your computer and use it in GitHub Desktop.
Drop-in Claude Code plugins per project — shell wrapper auto-loads repo-local plugins via --plugin-dir

Drop-in Claude Code plugins per project

A tiny shell wrapper that auto-loads any plugins living inside your project's .claude/plugins/ folder — no marketplace, no /plugin install, no settings.json edits.

Why

Claude Code plugins are normally installed via a marketplace or by editing .claude/settings.json. For personal or team-internal plugins that live inside a repo, this is clunky — you want to git clone a project and have its plugins just work.

There's an undocumented-but-official CLI flag, --plugin-dir <path>, intended for development and testing. It loads a plugin from disk for the duration of the session. This wrapper detects every plugin under ./.claude/plugins/*/ and appends one --plugin-dir flag per plugin automatically — so claude, claude -c, claude --resume, etc. all pick them up with no extra typing.

What a plugin looks like

Each plugin lives in its own folder and must contain a manifest:

.claude/plugins/
├── my-plugin/
│   ├── .claude-plugin/
│   │   └── plugin.json       ← required manifest
│   ├── .enabled              ← opt-in marker (see below)
│   ├── skills/
│   ├── agents/
│   ├── commands/
│   ├── hooks/
│   └── .mcp.json             ← optional
└── another-plugin/
    └── ...

Minimal plugin.json:

{
  "name": "my-plugin",
  "version": "0.1.0",
  "description": "What it does"
}

The wrapper (zsh — macOS default, most Linux setups)

Add to ~/.zshrc (or wherever you keep shell functions):

claude() {
  local -a plugin_args
  local plugin_root="./.claude/plugins"

  if [[ -d "$plugin_root" ]]; then
    if [[ -n "$CLAUDE_PLUGINS" ]]; then
      # Override: explicit allowlist wins (space-separated)
      for name in ${=CLAUDE_PLUGINS}; do
        [[ -f "$plugin_root/$name/.claude-plugin/plugin.json" ]] \
          && plugin_args+=(--plugin-dir "$plugin_root/$name")
      done
    else
      # Default: load every plugin with a .enabled marker
      for p in $plugin_root/*/.claude-plugin/plugin.json(N); do
        [[ -f "${p:h:h}/.enabled" ]] \
          && plugin_args+=(--plugin-dir "${p:h:h}")
      done
    fi
  fi

  command claude "${plugin_args[@]}" "$@"
}

Reload: source ~/.zshrc

The wrapper (bash)

Add to ~/.bashrc:

claude() {
  local -a plugin_args=()
  local plugin_root="./.claude/plugins"

  if [[ -d "$plugin_root" ]]; then
    if [[ -n "$CLAUDE_PLUGINS" ]]; then
      for name in $CLAUDE_PLUGINS; do
        [[ -f "$plugin_root/$name/.claude-plugin/plugin.json" ]] \
          && plugin_args+=(--plugin-dir "$plugin_root/$name")
      done
    else
      shopt -s nullglob
      for manifest in "$plugin_root"/*/.claude-plugin/plugin.json; do
        local plugin_dir="${manifest%/.claude-plugin/plugin.json}"
        [[ -f "$plugin_dir/.enabled" ]] \
          && plugin_args+=(--plugin-dir "$plugin_dir")
      done
      shopt -u nullglob
    fi
  fi

  command claude "${plugin_args[@]}" "$@"
}

Reload: source ~/.bashrc

Verify it's hooked

type claude
# → claude is a shell function

How to use

Enable a plugin in the current project (committed to the repo)

touch .claude/plugins/my-plugin/.enabled
git add .claude/plugins/my-plugin/.enabled

Now anyone who clones this repo and uses the wrapper gets my-plugin loaded automatically.

Disable without deleting

rm .claude/plugins/my-plugin/.enabled

Ad-hoc: test one plugin without touching marker files

CLAUDE_PLUGINS="wip-plugin" claude

Ad-hoc: test a specific combo

CLAUDE_PLUGINS="foo bar" claude -c

Temporarily ignore all plugins even in a plugin project

CLAUDE_PLUGINS=" " claude   # space = non-empty but matches nothing

Run raw claude without the wrapper

command claude -c

Design choices

Decision Why
Marker file (.enabled) as default Explicit opt-in avoids loading half-finished WIP plugins accidentally. Commit the marker to share defaults with teammates.
Env var (CLAUDE_PLUGINS) as override Per-invocation flexibility — test one plugin in isolation without editing files.
command claude at the bottom Bypasses the function itself; prevents infinite recursion.
Glob-based discovery No hardcoded plugin list. Add a plugin folder, touch .enabled, done.
Checks .claude-plugin/plugin.json exists Skips stray files/folders that aren't real plugins.

Caveats

  • --plugin-dir** is a dev/testing flag.** It's ephemeral — the plugin isn't registered in Claude Code's settings, it's just loaded for the session. That's exactly what we want for repo-local plugins, but means the UI might not show them under "installed plugins" the same way as marketplace installs.
  • Relative paths only. The wrapper uses ./.claude/plugins — it only activates when your CWD is the project root. cd project && claude works; claude from a sibling directory does not.
  • No lazy loading. All enabled plugins load at startup. If you have many heavy plugins, consider using the env var override instead of markers.
  • Subshells and scripts don't inherit functions by default. If you call claude from a shell script, either source ~/.zshrc inside it or export -f claude (bash only).

Alternative: the "proper" way

If you want plugins registered in Claude Code's actual settings (persistent, visible in /plugin), add a marketplace entry to .claude/settings.json:

{
  "extraKnownMarketplaces": {
    "local-plugins": {
      "source": { "source": "directory", "path": "./.claude/plugins" }
    }
  }
}

Plus a .claude/plugins/.claude-plugin/marketplace.json:

{
  "name": "local-plugins",
  "owner": { "name": "you" },
  "plugins": [{ "name": "my-plugin", "source": "./my-plugin" }]
}

Then /plugin install my-plugin@local-plugins inside Claude Code (once per plugin, per project).

The wrapper sidesteps all of that, at the cost of the dev-flag caveat above. Pick based on whether you want "zero-config drop-in" or "first-class registered plugins."

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment