# plugin-developer-cc > Stage, commit, and push changes to GitHub - Author: Muminur - Repository: Muminur/cc-plugin-developer-skill - Version: 20260125132536 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/Muminur/cc-plugin-developer-skill - Web: https://mule.run/skillshub/@@Muminur/cc-plugin-developer-skill~plugin-developer-cc:20260125132536 --- --- name: plugin-developer-cc description: Create production-ready Claude Code plugins with slash commands, skills, subagents, hooks, and MCP servers. Use when building, debugging, or distributing Claude Code plugins. Triggers include "create plugin", "build slash command", "make a Claude Code extension", "develop for Claude Code", "plugin marketplace", or any request involving Claude Code customization via the /plugin system. --- # Claude Code Plugin Developer Create reliable, cross-platform Claude Code plugins that work on **Windows, Linux, and macOS**. This skill covers plugin structure, all component types, validation, and distribution with full OS compatibility. ## No-AI-Attribution Policy **All git operations in plugins created with this skill follow strict rules:** 1. **NEVER** mention "Claude", "AI", "assistant", "generated", "automated", or "bot" in: - Commit messages - PR titles or descriptions - Author or co-author fields - Any git metadata 2. **NEVER** add Claude/AI as a contributor: - No `--author` flag with AI names - No `Co-authored-by:` trailers with AI - No `Signed-off-by:` with AI attribution 3. **ALWAYS** write commits/PRs as a human developer would: - Use conventional commit format - Focus on what changed, not how it was created - Natural, professional language ## Quick Start - Create Plugin ```bash # 1. Create plugin structure mkdir -p my-plugin/.claude-plugin mkdir -p my-plugin/commands my-plugin/skills my-plugin/agents my-plugin/hooks # 2. Create plugin manifest cat > my-plugin/.claude-plugin/plugin.json << 'EOF' { "name": "my-plugin", "version": "1.0.0", "description": "Brief description of what this plugin does", "author": { "name": "Your Name" } } EOF ``` ## Plugin Structure ``` my-plugin/ ├── .claude-plugin/ │ └── plugin.json # REQUIRED: Plugin manifest ├── commands/ # Slash commands (optional) │ └── my-command.md ├── skills/ # Agent skills (optional) │ └── my-skill/ │ └── SKILL.md ├── agents/ # Subagents (optional) │ └── my-agent.md ├── hooks/ # Event handlers (optional) │ └── hooks.json ├── .mcp.json # MCP servers (optional) └── scripts/ # Supporting scripts (optional) ├── my-script.js # Prefer Node.js for cross-platform └── my-script.py # Or Python for cross-platform ``` ## Cross-Platform Compatibility **CRITICAL**: All plugins must work on Windows, Linux, and macOS. ### Path Handling ```javascript // ✅ CORRECT: Use forward slashes (works everywhere) const configPath = "config/settings.json"; const filePath = path.join(__dirname, "scripts", "helper.js"); // ❌ WRONG: Hardcoded backslashes (Windows-only) const configPath = "config\\settings.json"; ``` ### Scripts - Use Node.js or Python (NOT Bash) ``` scripts/ ├── build.js # ✅ Node.js - works everywhere ├── validate.py # ✅ Python - works everywhere ├── helper.mjs # ✅ ES modules ├── build.sh # ⚠️ Bash - Linux/macOS only └── build.bat # ⚠️ Batch - Windows only ``` **Cross-platform script example** (`scripts/setup.js`): ```javascript #!/usr/bin/env node const { execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); // Works on all platforms const isWindows = process.platform === 'win32'; const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.config'); // Cross-platform command execution function runCommand(cmd) { try { return execSync(cmd, { encoding: 'utf8', shell: true }); } catch (e) { console.error(`Command failed: ${cmd}`); process.exit(1); } } ``` ### Hooks Configuration (Cross-Platform) **IMPORTANT**: Plugin hooks use a nested format with `type` field! ```json { "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/validate.js\"", "timeout": 5 } ] } ], "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/post-process.py\"", "timeout": 10 } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/startup.js\"", "timeout": 5 } ] } ] } } ``` **Hook format rules**: | Field | Required | Notes | |-------|----------|-------| | `hooks` (nested array) | Yes | Contains the actual hook definitions | | `type` | Yes | Must be `"command"` or `"prompt"` | | `command` | Yes* | String (not array!) for type: command | | `matcher` | For tool events | Pattern for PreToolUse, PostToolUse, PermissionRequest | | `timeout` | No | Seconds (not milliseconds!), default: 60 | **Cross-platform commands** (command is a STRING): | Platform | Use | Avoid | |----------|-----|-------| | All | `"node \"${CLAUDE_PLUGIN_ROOT}/script.js\""` | `["node", "script.js"]` (array format) | | All | `"python3 \"${CLAUDE_PLUGIN_ROOT}/script.py\""` | Bash/shell scripts | | All | `"npx tool-name"` | Platform-specific commands | ### Environment Variables ```javascript // ✅ Cross-platform env access const home = process.env.HOME || process.env.USERPROFILE; const pathSep = process.platform === 'win32' ? ';' : ':'; // In plugin.json or .mcp.json, use env: prefix // Works on all platforms "env": { "API_KEY": "env:MY_API_KEY" } ``` ### Line Endings - **Always use LF** (`\n`) in all files - Configure Git: `git config core.autocrlf input` - Add `.gitattributes`: ``` * text=auto eol=lf *.md text eol=lf *.json text eol=lf *.js text eol=lf *.py text eol=lf ``` ### MCP Servers (Cross-Platform) ```json { "mcpServers": { "my-server": { "command": "npx", "args": ["-y", "@my-org/my-mcp-server"], "env": { "CONFIG_PATH": "env:CONFIG_PATH" } } } } ``` **Note**: Use `npx` instead of direct paths for Node.js MCP servers. ## Built-in Git Commands The skill includes ready-to-use git commands. **Important**: These commands never mention AI/Claude in commits or contributor lists. ### /git-push Push changes to GitHub with a clean commit message. ```markdown --- description: Stage, commit, and push changes to GitHub argument-hint: [commit message] allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*), Bash(git push:*) --- ## Current State - Branch: !`git branch --show-current` - Status: !`git status --short` - Diff: !`git diff --stat` ## Task Stage all changes, create a commit, and push to origin. **CRITICAL RULES - ALWAYS FOLLOW:** 1. NEVER mention "Claude", "AI", "assistant", "generated", or "automated" in commit messages 2. NEVER add Claude/AI as author or co-author 3. Write commit messages as if a human developer wrote them 4. Use conventional commit format: type(scope): description Commit message: $ARGUMENTS If no message provided, generate one based on the changes. Steps: 1. git add -A 2. git commit -m "" 3. git push origin ``` ### /git-commit Create a commit without pushing. ```markdown --- description: Stage and commit changes with a clean message argument-hint: [commit message] allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git diff:*), Bash(git commit:*) --- ## Context - Status: !`git status --short` - Staged: !`git diff --cached --stat` - Unstaged: !`git diff --stat` ## Task Create a commit with the provided or generated message. **CRITICAL RULES:** 1. NEVER mention "Claude", "AI", "assistant", "generated", or "automated" 2. NEVER use --author flag with AI/Claude 3. NEVER add Co-authored-by with AI/Claude 4. Write natural, human-style commit messages Message: $ARGUMENTS If no message, analyze changes and write an appropriate conventional commit message. ``` ### /git-pr Create a pull request without AI attribution. ```markdown --- description: Create a GitHub pull request argument-hint: [base-branch] [title] allowed-tools: Bash(git log:*), Bash(git diff:*), Bash(gh pr create:*) --- ## Context - Branch: !`git branch --show-current` - Commits: !`git log origin/main..HEAD --oneline 2>/dev/null || git log -5 --oneline` ## Task Create a PR using GitHub CLI. **CRITICAL RULES:** 1. NEVER mention Claude/AI in PR title or description 2. Write PR description as a human developer would 3. Focus on what changed and why, not how it was created Base branch: $1 (default: main) Title: $2 (or generate from commits) Use: gh pr create --base --title "" --body "<description>" ``` ## Component Reference ### 1. Plugin Manifest (plugin.json) - REQUIRED ```json { "name": "plugin-name", "version": "1.0.0", "description": "What the plugin does - shown in /plugin menu", "author": { "name": "Author Name", "email": "optional@email.com" }, "repository": "https://github.com/user/repo", "license": "MIT", "keywords": ["keyword1", "keyword2"] } ``` ### 2. Slash Commands (commands/*.md) Slash commands are Markdown files with optional YAML frontmatter. Filename becomes command name. **Basic command** (`commands/greet.md` → `/greet`): ```markdown --- description: Greet the user with a custom message argument-hint: [name] --- Say hello to $ARGUMENTS in a friendly way. ``` **Advanced command with tools** (`commands/commit.md` → `/commit`): ```markdown --- description: Create a git commit with conventional message argument-hint: [message] allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*) model: claude-3-5-haiku-20241022 --- ## Context - Status: !`git status` - Diff: !`git diff --cached` ## Task Create commit with message: $ARGUMENTS Follow Conventional Commits format. ``` **Frontmatter options**: | Field | Purpose | Default | |-------|---------|---------| | `description` | Brief description (REQUIRED for SlashCommand tool) | First line | | `argument-hint` | Shows expected arguments: `[file] [options]` | None | | `allowed-tools` | Tools command can use | Inherits | | `model` | Specific model string | Inherits | | `disable-model-invocation` | Prevent Claude from auto-invoking | false | **Special syntax**: - `$ARGUMENTS` - All arguments passed to command - `$1`, `$2`, `$3` - Positional arguments - `` !`command` `` - Execute bash, include output in context - `@path/to/file` - Include file contents ### 3. Skills (skills/skill-name/SKILL.md) Skills are model-invoked capabilities. Claude uses them automatically based on context. **Basic skill** (`skills/code-explainer/SKILL.md`): ```markdown --- name: code-explainer description: Explains code with diagrams and analogies. Use when user asks "how does this work?" or wants code explained. --- # Code Explainer When explaining code: 1. Start with an everyday analogy 2. Draw ASCII diagram showing flow 3. Walk through step-by-step 4. Highlight common gotchas ``` **Skill with supporting files**: ``` skills/api-builder/ ├── SKILL.md ├── templates/ │ └── openapi-template.yaml ├── references/ │ └── rest-conventions.md └── scripts/ └── validate-spec.py ``` Reference files from SKILL.md: "See `references/rest-conventions.md` for REST naming patterns." ### 4. Subagents (agents/*.md) Specialized agents for specific tasks. Invoked via `/agents` command. **Agent definition** (`agents/security-reviewer.md`): ```markdown --- description: Security-focused code review agent allowed-tools: Read, Grep, Glob --- # Security Review Agent You are a security specialist. Review code for: - SQL injection vulnerabilities - XSS attack vectors - Exposed credentials - Insecure configurations Provide findings with severity (HIGH/MEDIUM/LOW) and remediation steps. ``` ### 5. Hooks (hooks/hooks.json or inline in plugin.json) Event handlers that run at specific points in Claude's workflow. **Use Node.js/Python for cross-platform compatibility.** **CRITICAL**: Hooks require a nested structure with `type` field! ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/validate-command.js\"", "timeout": 5 } ] } ], "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/format-file.js\"", "timeout": 10 } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/on-complete.py\"", "timeout": 30 } ] } ] } } ``` **Hook events**: `PreToolUse`, `PostToolUse`, `Stop`, `Notification`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PermissionRequest`, `Setup`, `SubagentStop`, `PreCompact` **Hook schema**: ```json { "matcher": "ToolPattern", // Required for tool events, regex or exact match "hooks": [ // Required nested array { "type": "command", // Required: "command" or "prompt" "command": "string", // Command string (not array!) "timeout": 30 // Optional: seconds (default 60) } ] } ``` **Environment variables**: - `${CLAUDE_PLUGIN_ROOT}` - Absolute path to plugin directory - `${CLAUDE_PROJECT_DIR}` - Project root directory ### 6. MCP Servers (.mcp.json) Connect external tools and data sources via Model Context Protocol. ```json { "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "env:GITHUB_TOKEN" } }, "postgres": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-postgres"], "env": { "DATABASE_URL": "env:DATABASE_URL" } } } } ``` ## Plugin Installation Commands Users install plugins via `/plugin` command in Claude Code: ```bash # Add marketplace /plugin marketplace add username/repo-name # Install from marketplace /plugin install my-plugin@marketplace-name # Install with scope /plugin install my-plugin@marketplace --scope project # Shared with team /plugin install my-plugin@marketplace --scope local # Gitignored /plugin install my-plugin@marketplace --scope user # Personal (default) ``` ## Creating a Marketplace To distribute plugins, create a marketplace repository with `.claude-plugin/marketplace.json`: ```json { "$schema": "https://raw.githubusercontent.com/anthropics/claude-code/main/.claude-plugin/marketplace.schema.json", "name": "my-marketplace", "version": "1.0.0", "author": { "name": "Your Name" }, "plugins": [ { "name": "my-plugin", "description": "What the plugin does", "version": "1.0.0", "source": "./plugins/my-plugin", "author": { "name": "Your Name" }, "category": "development" } ] } ``` **Categories**: `development`, `devops`, `security`, `documentation`, `testing`, `utilities`, `productivity`, `workflow`, `ai-tools` ## Version Management **CRITICAL: Keep versions in sync across all manifest files!** Claude Code uses `marketplace.json` to determine which version to install. If it's out of sync with `plugin.json`, users will receive the wrong version with potentially broken code. ### Version Files | File | Purpose | |------|---------| | `.claude-plugin/plugin.json` | **Source of truth** - primary version | | `.claude-plugin/marketplace.json` | **Distribution version** - MUST match plugin.json | | `package.json` | Optional - for npm compatibility | ### Auto-Sync Versions **After any version bump, run:** ```bash node ~/.claude/skills/plugin-developer-cc/scripts/sync-version.js ./my-plugin ``` **Bump and sync in one command:** ```bash # Bump patch version (1.0.0 -> 1.0.1) and sync node sync-version.js ./my-plugin --bump patch # Bump minor version (1.0.1 -> 1.1.0) and sync node sync-version.js ./my-plugin --bump minor # Bump major version (1.1.0 -> 2.0.0) and sync node sync-version.js ./my-plugin --bump major ``` **Check for mismatches:** ```bash node sync-version.js ./my-plugin --check ``` ### Pre-Commit Hook (Recommended) Add to your plugin's git hooks to prevent version mismatches: ```bash # .git/hooks/pre-commit #!/bin/sh node ~/.claude/skills/plugin-developer-cc/scripts/sync-version.js . --check ``` ## Validation Checklist Before distributing, verify: **Required**: - [ ] `.claude-plugin/plugin.json` exists with valid JSON - [ ] `name` and `description` fields present in plugin.json - [ ] **Versions in sync**: plugin.json = marketplace.json = package.json - [ ] All commands have `description:` in frontmatter - [ ] All skills have `name:` and `description:` in frontmatter - [ ] All referenced files exist - [ ] No paths traverse outside plugin directory (`../`) **Cross-Platform**: - [ ] No `.sh` or `.bat` scripts in hooks (use `.js` or `.py`) - [ ] All paths use forward slashes `/` - [ ] `.gitattributes` with `* text=auto eol=lf` - [ ] Scripts use `#!/usr/bin/env node` or `#!/usr/bin/env python3` - [ ] No hardcoded OS-specific paths - [ ] Hook commands use `["node", ...]` or `["python3", ...]` ## Common Issues & Solutions | Issue | Cause | Fix | |-------|-------|-----| | Command not appearing | Missing description | Add `description:` to frontmatter | | Plugin not installing | Invalid JSON | Validate syntax with `jq . plugin.json` | | Skills not triggering | Vague description | Make description specific with triggers | | Scripts failing | Path issues | Use paths relative to plugin root | | Hooks not running | Invalid hooks.json | Check JSON syntax and matchers | | **Windows failure** | Bash scripts in hooks | Replace with Node.js/Python scripts | | **Path errors** | Backslashes in paths | Use forward slashes everywhere | | **Line ending issues** | CRLF in files | Add `.gitattributes`, convert to LF | | **Old version installed** | marketplace.json version mismatch | Run `node sync-version.js ./plugin` | | **Update not working** | marketplace.json has old version | Sync versions, push, clear user cache | ## Testing Plugins Locally ```bash # Method 1: Project scope (recommended for testing) mkdir -p .claude/plugins/my-plugin cp -r my-plugin/* .claude/plugins/my-plugin/ # Method 2: Symlink for active development ln -s $(pwd)/my-plugin .claude/plugins/my-plugin # Verify loaded components claude --debug # Look for "Loaded commands:" and "Loaded skills:" in output ``` ## Best Practices ### Cross-Platform (CRITICAL) 1. **Scripts**: Use Node.js or Python, never bash/batch 2. **Paths**: Always use forward slashes `/`, use `path.join()` in code 3. **Line endings**: Always LF, add `.gitattributes` 4. **Hooks**: Use `["node", "script.js"]` or `["python3", "script.py"]` 5. **Env vars**: Use `process.env.HOME || process.env.USERPROFILE` ### General 1. **Commands**: Keep descriptions under 100 chars; use `argument-hint` 2. **Skills**: Make descriptions trigger-focused ("Use when...") 3. **Agents**: Single responsibility; specific tool permissions 4. **Hooks**: Always set timeouts; handle failures gracefully 5. **Paths**: All paths relative to plugin root; never use `../` ## Resources - `scripts/init-plugin.js` - Initialize new plugin structure (cross-platform) - `scripts/sync-version.js` - Sync versions across plugin.json, marketplace.json, package.json - `scripts/validate-plugin.py` - Validate plugin before distribution (includes version check) - `references/complete-plugin-example.md` - Full working plugin example - `references/cross-platform-scripts.md` - Cross-platform script templates - `assets/plugin-template/` - Starter template with all components ## Lessons Learned (Real-World Debugging) These lessons come from debugging actual plugin issues in production: ### 1. Hook Commands Must Use `${CLAUDE_PLUGIN_ROOT}` **Problem**: Hook commands that reference scripts with hardcoded paths fail because the plugin lives in the cache directory. **Solution for hooks**: Use the `${CLAUDE_PLUGIN_ROOT}` environment variable (available in hook context): ```json { "hooks": { "SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/startup.js\"" }] }] } } ``` **⚠️ IMPORTANT**: This variable is ONLY available in hook context. For command files (commands/*.md), see Lesson #9 for auto-discovery approach. ### 2. Plugin Update Mechanism **How it works**: 1. User runs `/plugin update my-plugin@marketplace` 2. Claude Code pulls latest from GitHub into `~/.claude/plugins/marketplaces/` 3. Compares `marketplace.json` version with installed version 4. If different, copies new version to cache **Common issues**: - Changes made locally but NOT pushed to GitHub → update fails - Version bumped in plugin.json but NOT in marketplace.json → wrong version installed - Marketplace cache is stale → run `git pull` in `~/.claude/plugins/marketplaces/your-marketplace/` ### 3. Plugin Cache Architecture ``` ~/.claude/plugins/ ├── marketplaces/ # Git clones of marketplace repos │ └── my-marketplace/ # Pulled from GitHub │ └── .claude-plugin/marketplace.json ├── cache/ # Installed plugin copies │ └── my-marketplace/ │ └── my-plugin/ │ └── 1.0.0/ # Version-specific install └── installed_plugins.json # Tracks what's installed ``` **Key insight**: The installed cache (`cache/`) is SEPARATE from the marketplace cache (`marketplaces/`). Updating marketplace.json doesn't affect already-installed plugins until user runs update. ### 4. Command Files Are Instructions, Not Executables Command `.md` files are **prompts for Claude to follow**, not scripts that run directly. When user invokes `/my-plugin:my-command`: 1. Claude reads `commands/my-command.md` 2. Interprets the instructions 3. Uses Bash tool to execute the documented commands **Implication**: Commands should be clear instructions with exact commands to run, not abstract descriptions. ### 5. Command Flag Consistency If your daemon accepts positional commands (`config`, `status`), also add dashed aliases (`--config`, `--status`) for consistency with CLI conventions: ```javascript // In your daemon's command parser switch (command) { case '--config': case 'config': showConfig(); break; case '--status': case 'status': showStatus(); break; } ``` ### 6. Hook Format Gotchas **Hooks must use inline JSON in plugin.json**, not file references: ```json { "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/startup.js\"", "timeout": 5 } ] } ] } } ``` **Common mistakes**: - Using `"hooks": "./hooks/hooks.json"` (file reference) instead of inline - Missing the nested `hooks` array inside each event - Missing `"type": "command"` field - Using array format `["node", "script.js"]` instead of string `"node script.js"` ### 7. Debugging Plugin Installation Issues **Check these in order**: 1. **Marketplace version**: ```bash cat ~/.claude/plugins/marketplaces/your-marketplace/.claude-plugin/marketplace.json | grep version ``` 2. **Installed version**: ```bash cat ~/.claude/plugins/installed_plugins.json | grep -A5 "your-plugin" ``` 3. **GitHub version** (source of truth): ```bash curl -s https://raw.githubusercontent.com/user/repo/main/.claude-plugin/marketplace.json | grep version ``` 4. **Sync marketplace cache**: ```bash cd ~/.claude/plugins/marketplaces/your-marketplace && git pull ``` 5. **Force reinstall**: ```bash claude plugin uninstall my-plugin@marketplace claude plugin install my-plugin@marketplace ``` ### 8. Testing Commands Without Reinstalling For quick iteration, you can patch the installed plugin directly: ```bash # Copy updated files to installed cache cp ./my-plugin/auto-resume-daemon.js ~/.claude/plugins/cache/my-marketplace/my-plugin/1.0.0/ # Test immediately without reinstalling ``` **Warning**: These changes are lost on next update. Always push to GitHub for permanent fixes. ### 9. `CLAUDE_PLUGIN_ROOT` Only Available in Hook Context **Problem**: Commands documented in skill files that use `${CLAUDE_PLUGIN_ROOT}` fail when Claude Code executes them via the Bash tool. **Root cause**: The `CLAUDE_PLUGIN_ROOT` environment variable is **only set during plugin hook execution** (SessionStart, PreToolUse, etc.). When Claude reads a command file and runs bash commands directly, that variable is NOT set. **Example failure**: ``` # Command file says: node "${CLAUDE_PLUGIN_ROOT}/auto-resume-daemon.js" --notify-test # Claude Code runs this via Bash tool # Result: Error: Cannot find module '/usr/local/auto-resume-daemon.js' # (CLAUDE_PLUGIN_ROOT was empty or defaulted to wrong path) ``` **Solutions**: 1. **Auto-discovery pattern** (recommended for command files): ```bash DAEMON_PATH=$(find ~/.claude/plugins/cache -name "auto-resume-daemon.js" 2>/dev/null | head -1) && node "$DAEMON_PATH" --notify-test ``` 2. **Create a wrapper script** that finds the daemon regardless of context: ```javascript // scripts/run-daemon-command.js const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); function findDaemonPath() { // Check CLAUDE_PLUGIN_ROOT first (hook context) if (process.env.CLAUDE_PLUGIN_ROOT) { const pluginDaemon = path.join(process.env.CLAUDE_PLUGIN_ROOT, 'daemon.js'); if (fs.existsSync(pluginDaemon)) return pluginDaemon; } // Fall back to searching plugin cache const pluginCache = path.join(require('os').homedir(), '.claude', 'plugins', 'cache'); // ... recursive search for daemon.js } const daemon = findDaemonPath(); spawn('node', [daemon, ...process.argv.slice(2)], { stdio: 'inherit' }); ``` 3. **Use hooks for operations that need plugin root**: If an operation truly needs `CLAUDE_PLUGIN_ROOT`, trigger it via a hook instead of a command file. **Key insight**: - **Hooks** (plugin.json hooks section) → `CLAUDE_PLUGIN_ROOT` IS available - **Commands** (commands/*.md executed via Bash tool) → `CLAUDE_PLUGIN_ROOT` is NOT available **When to use which approach**: | Scenario | Approach | |----------|----------| | Startup tasks | Hooks (SessionStart) - env var available | | User-invoked commands | Auto-discovery in command files | | Background daemons | Wrapper script with fallback logic | | Pre/Post tool validation | Hooks - env var available | ### 10. Version Bumping is Critical for Plugin Updates **Problem**: After pushing code changes, `/plugin update` doesn't detect any updates. **Root cause**: The plugin system compares version numbers to detect updates. If `plugin.json` and `marketplace.json` have the same version as the installed plugin, no update is triggered—even if the code has changed. **Solution**: Always bump the version after making code changes. **Create a version bump script** (`scripts/bump-version.js`): ```javascript #!/usr/bin/env node const fs = require('fs'); const path = require('path'); const PLUGIN_JSON = path.join(__dirname, '..', '.claude-plugin', 'plugin.json'); const MARKETPLACE_JSON = path.join(__dirname, '..', '.claude-plugin', 'marketplace.json'); function bumpVersion(version, type = 'patch') { const parts = version.split('.').map(Number); if (type === 'major') { parts[0]++; parts[1] = 0; parts[2] = 0; } else if (type === 'minor') { parts[1]++; parts[2] = 0; } else { parts[2]++; } return parts.join('.'); } const pluginJson = JSON.parse(fs.readFileSync(PLUGIN_JSON, 'utf8')); const newVersion = bumpVersion(pluginJson.version, process.argv[2] || 'patch'); // Update plugin.json pluginJson.version = newVersion; fs.writeFileSync(PLUGIN_JSON, JSON.stringify(pluginJson, null, 2) + '\n'); // Update marketplace.json const marketplaceJson = JSON.parse(fs.readFileSync(MARKETPLACE_JSON, 'utf8')); marketplaceJson.plugins[0].version = newVersion; fs.writeFileSync(MARKETPLACE_JSON, JSON.stringify(marketplaceJson, null, 2) + '\n'); console.log(`Version bumped to ${newVersion}`); ``` **Usage**: ```bash node scripts/bump-version.js # 1.2.3 -> 1.2.4 (patch) node scripts/bump-version.js minor # 1.2.3 -> 1.3.0 node scripts/bump-version.js major # 1.2.3 -> 2.0.0 ``` **Workflow after code changes**: ```bash node scripts/bump-version.js && git add -A && git commit -m "feat: Your change" && git push ``` ### 11. Add CLAUDE.md for Version Bump Policy **Problem**: Developers (including AI assistants) forget to bump versions after code changes. **Solution**: Add a `CLAUDE.md` file at project root with clear instructions: ```markdown # Project Instructions ## Version Bump Policy **IMPORTANT: Always bump the version after making code changes!** After modifying any code files: 1. Run: `node scripts/bump-version.js` 2. Include version files in commit 3. Push to GitHub ### Why This Matters - Plugin system uses version numbers to detect updates - If version doesn't change, `/plugin update` won't see new code - Users expect updates immediately after pushing ``` This ensures consistent behavior across all development sessions. ### 12. Plugin Commands Cannot Be Invoked Programmatically **Problem**: Trying to run `/plugin update` via the Skill tool fails. **Root cause**: `/plugin` is a built-in Claude Code CLI command, not a user-defined skill. It requires interactive user input and cannot be invoked programmatically. **Error when attempting**: ``` Skill plugin is not a prompt-based skill ``` **Solution**: Instruct the user to run plugin commands manually: ```markdown Please run this command yourself: /plugin update my-plugin@my-marketplace ``` **Built-in commands that cannot be invoked programmatically**: - `/plugin` - Plugin management - `/help` - Help system - `/clear` - Clear conversation - `/config` - Configuration - `/doctor` - Diagnostics **User-defined skills CAN be invoked** via the Skill tool - only built-in CLI commands are restricted. ### 13. Full Plugin Update Workflow **After making code changes to a plugin**: 1. **Bump version**: ```bash node scripts/bump-version.js ``` 2. **Commit and push**: ```bash git add -A && git commit -m "feat: Description" && git push origin main ``` 3. **Update marketplace cache** (for testing): ```bash cd ~/.claude/plugins/marketplaces/your-marketplace && git pull ``` 4. **Verify versions**: ```bash echo "Installed:" && cat ~/.claude/plugins/installed_plugins.json | grep -A5 "your-plugin@" echo "Marketplace:" && cat ~/.claude/plugins/marketplaces/your-marketplace/.claude-plugin/marketplace.json | grep version ``` 5. **User runs update** (must be done manually): ``` /plugin update your-plugin@your-marketplace ``` **Key insight**: Steps 1-4 can be automated/scripted, but step 5 requires user interaction. **IMPORTANT**: After `git push`, always update the local marketplace cache: ```bash git push origin main && cd ~/.claude/plugins/marketplaces/your-marketplace && git pull ``` The marketplace cache is a LOCAL git clone - pushing to GitHub doesn't auto-sync it. Without `git pull`, `/plugin update` won't see your changes. ### 14. WebSocket Handler Registration Pattern for GUI Communication **Problem**: WebSocket server returns "Unknown message type" errors for GUI-specific messages (status, config, analytics). **Root cause**: The WebSocket server has a hardcoded switch statement handling only internal messages (ping, subscribe, unsubscribe). GUI-specific message types aren't recognized. **Solution**: Add a handler registration system to make the WebSocket server extensible: ```javascript // In WebSocketServer constructor this.messageHandlers = new Map(); // Register handler method registerHandler(messageType, handler) { this.messageHandlers.set(messageType, handler); } // Public send method for handlers to respond send(ws, message) { this._send(ws, message); } // In _handleMessage default case default: if (this.messageHandlers.has(message.type)) { const handler = this.messageHandlers.get(message.type); try { handler(ws, state, message); } catch (err) { this._send(ws, { type: 'error', data: { message: 'Handler error' } }); } } else { this._send(ws, { type: 'error', data: { message: 'Unknown message type' } }); } ``` **Register handlers in the integration layer** (keeps WebSocket server as generic transport): ```javascript // In DashboardIntegration._registerMessageHandlers() this.wsServer.registerHandler('status', (ws, state, message) => { const sessions = this.statusBridge.getAllStatuses(); this.wsServer.send(ws, { type: 'status', sessions, stats: this._getDaemonStats() }); }); this.wsServer.registerHandler('config', (ws, state, message) => { this.wsServer.send(ws, { type: 'config', config: this.configManager.getConfig() }); }); this.wsServer.registerHandler('analytics', (ws, state, message) => { const analytics = this.statusBridge.getAnalytics(); this.wsServer.send(ws, { type: 'analytics', data: analytics.chartData || [] }); }); ``` **Benefits**: - WebSocket server stays generic and reusable - Business logic lives in the integration layer (proper separation of concerns) - Easy to test each layer independently - New message types can be added without modifying core server ### 15. Testing Async WebSocket Message Flows with Jest **Problem**: WebSocket message handling is asynchronous - tests need to wait for events. **Pattern for testing handler registration**: ```javascript describe('message handler registration', () => { it('should call registered handler for matching message type', (done) => { const server = new WebSocketServer({ port: 3847, enableHeartbeat: false }); const handler = jest.fn(); server.registerHandler('status', handler); server.start(); const ws = simulateConnection(server); setTimeout(() => { // Simulate receiving message ws.emit('message', JSON.stringify({ type: 'status' })); setTimeout(() => { expect(handler).toHaveBeenCalled(); done(); }, 10); // Allow handler to execute }, 10); // Allow connection to establish }); it('should pass ws, state, and message to handler', (done) => { let receivedArgs = null; const handler = jest.fn((ws, state, message) => { receivedArgs = { ws, state, message }; }); server.registerHandler('config', handler); // ... rest of test }); }); ``` **Key testing patterns**: 1. **Disable heartbeat** in test config (`enableHeartbeat: false`) to prevent timer loops 2. **Use nested setTimeout** for async event flows 3. **Use `done()` callback** for async tests instead of async/await 4. **Capture arguments** with jest.fn() to verify handler receives correct data 5. **Test error handling** by throwing in handler and verifying error response sent ### 16. TDD Workflow for Feature Implementation **Red-Green-Refactor cycle applied to WebSocket handlers**: 1. **Write tests first (Red)**: - Test handler registration (`registerHandler` method exists) - Test handler invocation (handler called for matching type) - Test parameter passing (ws, state, message passed correctly) - Test error handling (handler errors don't crash server) - Test integration (handlers registered on server start) 2. **Implement to pass tests (Green)**: - Add `messageHandlers` Map to constructor - Add `registerHandler()` method - Modify `_handleMessage()` default case - Add `send()` public method - Add `_registerMessageHandlers()` in integration layer 3. **Verify manually**: - Restart daemon - Open GUI - Check no error toasts - Check console for proper message handling **Task breakdown for tracking**: ``` #1 Write WebSocket handler registration tests #2 Write Dashboard message handler tests #3 Implement WebSocket handler registration #4 Implement Dashboard message handlers #5 Verify and commit ``` This systematic approach ensures all code is covered by tests before implementation. ### 17. Windows Toast Notifications May Silently Fail **Problem**: Windows toast notifications via `node-notifier` (snoretoast) appear to succeed (exit code 0) but the user never sees the notification. **Root causes**: - `ToastEnabled` registry key set to 0 (notifications globally disabled) - Focus Assist / Do Not Disturb mode active - App-specific notification permissions not granted - Notifications going to Action Center but not showing as popups **Diagnosis**: ```powershell # Check if toast notifications are enabled Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\PushNotifications' | Select-Object ToastEnabled # ToastEnabled = 0 means disabled ``` **Solution**: Add a `preferMessageBox` config option to bypass toast and use PowerShell MessageBox: ```javascript // In notification manager if (this.config.preferMessageBox && process.platform === 'win32') { return await this._showWindowsFallback(title, message); } // Windows PowerShell MessageBox fallback async _showWindowsFallback(title, message) { const command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message}', '${title}', 'OK', 'Information')"`; // ... execute command } ``` **CLI flag**: Add `-m` or `--prefer-messagebox` to notification test commands: ```javascript case 'notify': const preferMessageBox = args.includes('--prefer-messagebox') || args.includes('-m'); testNotification({ preferMessageBox }); break; ``` **Key insight**: Always provide a fallback notification method. Toast notifications are unreliable across different Windows configurations. ### 18. Windows Installer Must Run npm install **Problem**: Plugin dependencies (like `node-notifier`) not available after installation on Windows. **Root cause**: The Windows PowerShell installer (`install.ps1`) copied `package.json` but never ran `npm install`. **Solution**: Add `Install-Dependencies` function to `install.ps1`: ```powershell function Install-Dependencies { Write-Info "Installing npm dependencies..." if (-not (Test-Path $PACKAGE_JSON_DEST)) { Write-Warning "package.json not found, skipping npm install" return } try { $npmVersion = npm --version 2>$null if (-not $npmVersion) { Write-Warning "npm not found, please run 'npm install' manually" return } } catch { Write-Warning "npm not found, please run 'npm install' manually" return } $currentDir = Get-Location try { Set-Location $AUTO_RESUME_DIR npm install --production Write-Success "Dependencies installed" } finally { Set-Location $currentDir } } ``` **Call it during installation** after copying package.json: ```powershell # Copy package.json Copy-Item -Path $PACKAGE_JSON_SOURCE -Destination $PACKAGE_JSON_DEST # Install dependencies Install-Dependencies ``` **Also create cross-platform install script** (`scripts/install.js`): ```javascript #!/usr/bin/env node async function installDependencies(options = {}) { const dir = options.dir || getInstallDir(); return new Promise((resolve) => { const proc = spawn('npm', ['install', '--production'], { cwd: dir, shell: true, stdio: options.silent ? 'pipe' : 'inherit' }); proc.on('close', (code) => resolve({ success: code === 0 })); }); } ``` ### 19. CLI Command Tests Must Account for Log File Content **Problem**: CLI tests for `logs` command fail because the log file contains "Unknown command" entries from previous test runs. **Example failure**: ```javascript // Test checks output doesn't contain "Unknown command" expect(result.output).not.toContain('Unknown command'); // But logs command shows log file which contains old entries: // [ERROR] Unknown command: nonexistent-command // (from when 'unknown command' test ran earlier) ``` **Solution**: Test for CLI error format, not just any occurrence: ```javascript test('should display recent log entries', () => { const result = runCommand('logs'); // Check for valid output format, not absence of substring expect(result.output).toMatch(/Daemon Logs|No log file found|Log file is empty/); // Check for CLI error specifically (not log file content) expect(result.output).not.toMatch(/\[ERROR\].*Unknown command: logs/); }); ``` **Key insight**: When testing CLI output that includes file contents, distinguish between: - CLI errors (command itself failed) - File content that happens to contain similar text ### 20. GUI Command Should Open HTTP URL, Not Local File **Problem**: `/plugin:gui` command opens `file:///path/to/gui/index.html` instead of `http://localhost:3737`. **Why it matters**: - Local file can't use WebSocket for live updates - CORS restrictions may block API calls - User sees stale data without auto-refresh **Fix**: Change fallback from file path to HTTP URL: ```javascript // OLD (wrong): const guiPath = path.join(__dirname, 'gui', 'index.html'); command = `start "" "${guiPath}"`; // Opens file:// // NEW (correct): const guiUrl = 'http://localhost:3737'; command = `start "" "${guiUrl}"`; // Opens HTTP server ``` **Full cross-platform implementation**: ```javascript const guiUrl = 'http://localhost:3737'; const platform = os.platform(); let command; if (platform === 'win32') { command = `start "" "${guiUrl}"`; } else if (platform === 'darwin') { command = `open "${guiUrl}"`; } else { command = `xdg-open "${guiUrl}"`; } exec(command, (error) => { if (error) { log('error', `Failed to open GUI: ${error.message}`); log('info', `You can manually open: ${guiUrl}`); } }); ``` ### 21. Always Implement All Commands Referenced in command/*.md Files **Problem**: Commands defined in `commands/*.md` reference daemon commands that don't exist. **Example**: `commands/logs.md` documents running `node daemon.js logs` but the daemon didn't have a `logs` case in its command switch. **Audit checklist**: 1. List all `commands/*.md` files 2. For each, identify the daemon command it invokes 3. Verify that command exists in daemon's switch statement 4. Test each command manually: `node daemon.js <command>` **CLI regression test template**: ```javascript describe('CLI Commands', () => { const DAEMON_PATH = path.join(__dirname, '..', 'auto-resume-daemon.js'); function runCommand(args) { try { return { success: true, output: execSync(`node "${DAEMON_PATH}" ${args}`, { encoding: 'utf8' }) }; } catch (err) { return { success: false, output: err.stdout || '', exitCode: err.status }; } } test('logs command should work', () => { const result = runCommand('logs'); expect(result.output).toMatch(/Daemon Logs|No log file/); }); test('status command should work', () => { const result = runCommand('status'); expect(result.output).toMatch(/running|not running/); }); // ... test all documented commands }); ``` **Key insight**: Create automated tests for all CLI commands to catch missing implementations early. ### 22. Complete Plugin Update Debugging Workflow **Problem**: `/plugin update` shows "no updates available" or doesn't detect new changes. **Diagnosis checklist** - Check version in ALL locations: ```powershell # 1. Check GitHub (source of truth) curl -s https://raw.githubusercontent.com/USER/REPO/main/.claude-plugin/marketplace.json | Select-String version # 2. Check local source (your development directory) cat L:\Path\To\Your\Plugin\.claude-plugin\marketplace.json | Select-String version # 3. Check marketplace cache (LOCAL git clone - often stale!) cat ~/.claude/plugins/marketplaces/YOUR-MARKETPLACE/.claude-plugin/marketplace.json | Select-String version # 4. Check installed version cat ~/.claude/plugins/installed_plugins.json | Select-String "your-plugin" -Context 0,5 ``` **Common issue**: All versions show the same number = **already at latest version**. **Version sync verification**: | Location | Expected | |----------|----------| | GitHub | Your new version (e.g., 1.4.12) | | Local source | Same as GitHub | | Marketplace cache | Same as GitHub (requires `git pull`!) | | Installed | Previous version (e.g., 1.4.11) | **Complete workflow after making code changes**: ```bash # 1. Bump version in your plugin directory cd L:\Path\To\Your\Plugin node scripts/bump-version.js # or manually edit marketplace.json # 2. Commit and push to GitHub git add -A && git commit -m "feat: Your change" && git push origin main # 3. CRITICAL: Sync the local marketplace cache cd ~/.claude/plugins/marketplaces/your-marketplace git pull origin main # 4. Verify versions now differ echo "Installed:" && cat ~/.claude/plugins/installed_plugins.json | grep -A5 "your-plugin" echo "Marketplace:" && cat .claude-plugin/marketplace.json | grep version # 5. Now /plugin update will detect the new version # User runs: /plugin update your-plugin@your-marketplace ``` **Why step 3 is critical**: The marketplace cache (`~/.claude/plugins/marketplaces/`) is a LOCAL git clone that does NOT auto-sync with GitHub. After `git push`, the local cache still has the old version until you run `git pull`. **Debugging "no update found"**: 1. If marketplace cache version = installed version → Run `git pull` in marketplace directory 2. If GitHub version = installed version → You forgot to bump the version before pushing 3. If marketplace cache is outdated → The git clone is stale, run `git pull` **Force reinstall** (nuclear option): ```bash # Uninstall and reinstall fresh /plugin uninstall your-plugin@your-marketplace /plugin install your-plugin@your-marketplace ``` **Key insight**: The plugin update mechanism compares `marketplace.json` version in the LOCAL marketplace cache (not GitHub directly) against the installed version. Both must be updated for `/plugin update` to work. ### 23. MANDATORY: Update Command Files on Every Version Release **CRITICAL RULE**: When releasing a new plugin version, you MUST update all `commands/*.md` files to reference the new version. **This step is NOT optional and MUST NOT be skipped.** **Why this matters**: Command files (e.g., `commands/status.md`, `commands/start.md`) contain hardcoded version paths like: ```bash node ~/.claude/plugins/cache/my-marketplace/my-plugin/1.4.4/daemon.js status ``` If version is bumped to `1.4.5` but commands still reference `1.4.4`, the commands will fail because the old version directory no longer exists after update. **Version Release Checklist** (ALL steps mandatory): ```bash # 1. Update all command files to new version find commands/ -name "*.md" -exec grep -l "1.4.11" {} \; # Find files with old version # Then update each file: 1.4.11 → 1.4.12 # 2. Bump version in manifests node scripts/bump-version.js # or manually edit plugin.json + marketplace.json # 3. Verify versions match everywhere grep -r "1.4.12" commands/ # Should show new version in all files grep "version" .claude-plugin/*.json # Should show 1.4.12 # 4. Commit and push git add -A && git commit -m "chore: release v1.4.12" && git push origin main # 5. Sync marketplace cache git -C ~/.claude/plugins/marketplaces/your-marketplace pull origin main ``` **Automated version update script** (`scripts/update-command-versions.js`): ```javascript #!/usr/bin/env node const fs = require('fs'); const path = require('path'); const COMMANDS_DIR = path.join(__dirname, '..', 'commands'); const MARKETPLACE_JSON = path.join(__dirname, '..', '.claude-plugin', 'marketplace.json'); // Get current version from marketplace.json const marketplace = JSON.parse(fs.readFileSync(MARKETPLACE_JSON, 'utf8')); const newVersion = marketplace.plugins[0].version; // Find all .md files in commands/ const commandFiles = fs.readdirSync(COMMANDS_DIR) .filter(f => f.endsWith('.md')) .map(f => path.join(COMMANDS_DIR, f)); // Version pattern: matches X.Y.Z in path context const versionPattern = /\/(\d+\.\d+\.\d+)\//g; let updatedCount = 0; commandFiles.forEach(file => { let content = fs.readFileSync(file, 'utf8'); const originalContent = content; // Replace all version references in paths content = content.replace(versionPattern, `/${newVersion}/`); if (content !== originalContent) { fs.writeFileSync(file, content); console.log(`Updated: ${path.basename(file)}`); updatedCount++; } }); console.log(`\nUpdated ${updatedCount} command file(s) to version ${newVersion}`); ``` **Pre-commit hook** (`.git/hooks/pre-commit`): ```bash #!/bin/sh # Verify command files match current version VERSION=$(grep -o '"version": "[^"]*"' .claude-plugin/marketplace.json | head -1 | cut -d'"' -f4) MISMATCHED=$(grep -rL "$VERSION" commands/*.md 2>/dev/null | grep -v "^$" || true) if [ -n "$MISMATCHED" ]; then echo "ERROR: Command files have outdated version references!" echo "Run: node scripts/update-command-versions.js" echo "Mismatched files:" echo "$MISMATCHED" exit 1 fi ``` **Integration with bump-version.js**: ```javascript // At the end of bump-version.js, auto-update commands const { execSync } = require('child_process'); console.log('Updating command files...'); execSync('node scripts/update-command-versions.js', { stdio: 'inherit' }); ``` **Key insight**: Command files are instructions for Claude to execute. If they reference old versions, commands will break immediately after update. Always keep command version references in sync with the actual plugin version. --- ### Lesson #24: Plugin Cache Dependencies Are NOT Auto-Installed **Date**: 2026-01-25 **Plugin**: auto-resume **Issue**: Dashboard servers fail to start with ERR_CONNECTION_REFUSED after plugin install **Problem Discovery**: After installing a plugin via `/plugin install`, the dashboard showed `ERR_CONNECTION_REFUSED` on localhost:3737. The daemon was running (verified via PID file and process list), but the HTTP, WebSocket, and API servers weren't listening on their ports. **Root Cause**: The `/plugin install` command copies files to the plugin cache (`~/.claude/plugins/cache/`) but does **NOT** run `npm install`. This means any npm dependencies declared in `package.json` are missing: ``` ~/.claude/plugins/cache/your-marketplace/your-plugin/1.4.13/ ├── package.json ← Dependencies declared here ├── auto-resume-daemon.js ← Requires 'ws', 'node-notifier' └── node_modules/ ← DOES NOT EXIST after /plugin install ``` **Symptoms**: - Daemon runs fine (core functionality works) - Optional features using npm dependencies fail silently - `require('ws')` in try-catch returns null instead of throwing - Dashboard servers don't start, ports not listening - `ERR_CONNECTION_REFUSED` when opening dashboard URLs **Debug Commands**: ```bash # Check if ports are listening # Windows: netstat -ano | findstr ":3737 :3847 :3848" # macOS/Linux: lsof -i :3737 -i :3847 -i :3848 # Verify node_modules exists in plugin cache ls ~/.claude/plugins/cache/*/auto-resume/*/node_modules/ ``` **Solution 1: Auto-Install in SessionStart Hook** Add dependency checking and auto-installation to your SessionStart hook: ```javascript // In ensure-daemon-running.js (SessionStart hook) const REQUIRED_DEPS = ['ws', 'node-notifier']; function getMissingDeps(dir) { const missing = []; for (const dep of REQUIRED_DEPS) { const depPath = path.join(dir, 'node_modules', dep); if (!fs.existsSync(depPath)) { missing.push(dep); } } return missing; } function installMissingDeps(dir, deps) { if (deps.length === 0) return true; try { execSync(`npm install ${deps.join(' ')} --save --legacy-peer-deps`, { cwd: dir, stdio: 'pipe', timeout: 60000 }); return true; } catch (e) { return false; // Don't block daemon start } } // In main(): const daemonDir = path.dirname(daemonPath); const missingDeps = getMissingDeps(daemonDir); if (missingDeps.length > 0) { installMissingDeps(daemonDir, missingDeps); } ``` **Solution 2: Manual Fix Commands** ```bash # Windows PowerShell: cd "$env:USERPROFILE\.claude\plugins\cache\your-marketplace\your-plugin\*" npm install ws node-notifier --save # macOS/Linux: cd ~/.claude/plugins/cache/your-marketplace/your-plugin/*/ npm install ws node-notifier --save # Then restart daemon: node auto-resume-daemon.js stop node auto-resume-daemon.js start ``` **Solution 3: Update Install Scripts** Modify platform installers to explicitly install optional dependencies: ```bash # install.sh (Linux/macOS) install_plugin_cache_dependencies() { CACHE_DIR=$(find ~/.claude/plugins/cache -name "your-plugin" -type d 2>/dev/null | head -1) if [ -n "$CACHE_DIR" ]; then cd "$CACHE_DIR" && npm install ws node-notifier --save 2>/dev/null || true fi } ``` ```powershell # install.ps1 (Windows) function Install-PluginCacheDependencies { $cacheDir = Get-ChildItem "$env:USERPROFILE\.claude\plugins\cache" -Recurse -Directory | Where-Object { $_.Name -eq "your-plugin" } | Select-Object -First 1 if ($cacheDir) { Set-Location $cacheDir.FullName npm install ws node-notifier --save 2>$null } } ``` **Key Insights**: 1. **Plugin install ≠ npm install** - The Claude Code plugin system copies files but doesn't handle npm dependencies 2. **Silent failures are dangerous** - Always log when optional dependencies fail to load 3. **SessionStart hooks are powerful** - Use them for self-healing, auto-installing missing deps 4. **Document the manual fix** - Add troubleshooting section to README for users who hit this issue 5. **Core vs optional features** - Design plugins so core functionality works without optional npm deps **Prevention Checklist**: - [ ] Document required npm dependencies in README - [ ] Add auto-install logic to SessionStart hook - [ ] Add manual fix commands to troubleshooting docs - [ ] Update all platform installers to install deps - [ ] Log clearly when optional features fail to load