@softspark/ai-toolkit 4.25.1 → 4.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,75 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.27.0 — Doctor sees the Claude app plugin (2026-08-21)
11
+
12
+ ### Added
13
+
14
+ - **Doctor check 11: plugin double-load.** `ai-toolkit doctor` now reads the
15
+ Claude Code plugin registry (`~/.claude/plugins/installed_plugins.json`) and
16
+ `enabledPlugins` in `~/.claude/settings.json`. When an uploaded Claude app
17
+ plugin is active next to the global install, both feed the same session:
18
+ Claude Code merges plugin hooks with user hooks without deduplication, so every
19
+ toolkit hook fires twice per event and skills and agents load twice. The check
20
+ warns with the hook count, and `--fix` disables the plugin for Claude Code
21
+ while leaving the global install authoritative. Previously this passed as a
22
+ healthy install with no signal at all.
23
+ - `kb/troubleshooting/plugin-double-load.md` documents the symptom, the debug-log
24
+ evidence, and the fix.
25
+
26
+ ---
27
+
28
+ ## v4.26.0 — Python floor is declared and enforced (2026-08-21)
29
+
30
+ ### Added
31
+
32
+ - **Python preflight in the CLI.** `bin/ai-toolkit.js` now verifies `python3`
33
+ exists and reports at least 3.11 before spawning any script, and prints a
34
+ platform-specific fix (`brew install python@3.13` on macOS) instead of a
35
+ traceback from deep inside `scripts/`. The check is lazy and memoized, so
36
+ `help` and `--version` still run without a Python interpreter.
37
+ - **Matching floor check in `scripts/_common.py`.** Every entry point imports
38
+ `_common` first, so `python3 scripts/install.py` run directly fails with the
39
+ same message rather than a `TypeError` from whichever module happens to use
40
+ 3.11+ syntax.
41
+ - **CI import sweep.** The `python-syntax` job now runs on a `['3.11', '3.13']`
42
+ matrix and imports every module under `scripts/` on each. `py_compile` only
43
+ catches syntax, which is exactly why a 3.10-only construct shipped: version-
44
+ gated runtime features surface on import, not on compile.
45
+
46
+ ### Fixed
47
+
48
+ - **`ai-toolkit update --local` crashed on macOS system Python.** `/usr/bin/python3`
49
+ is 3.9 on every Mac, and `scripts/mcp_editors.py` uses
50
+ `@dataclass(frozen=True, slots=True)` (3.10+), so the install path died with
51
+ `TypeError: dataclass() got an unexpected keyword argument 'slots'` while
52
+ importing `install_steps/ai_tools.py`. The interpreter is now rejected up
53
+ front with instructions.
54
+
55
+ ### Changed
56
+
57
+ - **`scripts/check_deps.py` declares Python >= 3.11**, up from a `min_version`
58
+ of `3.8` that never matched what the scripts actually needed, and its reason
59
+ string calls out the macOS 3.9 trap.
60
+ - **Requirements are documented.** README gained a Requirements note above the
61
+ install snippet; `CLAUDE.md` records the three places the floor is declared
62
+ (`bin/ai-toolkit.js`, `scripts/_common.py`, `scripts/check_deps.py`) and the
63
+ CI matrix that guards it.
64
+
65
+ ### Ecosystem
66
+
67
+ - Snapshot refreshed: 11 tools drifted, all class A (content reworded, no
68
+ heading delta). Claude Code 2.1.235 → 2.1.238 and Codex CLI 0.147.0 → 0.148.0
69
+ are upstream patch bumps with no new surface, so no generator changed.
70
+
71
+ ### Tests
72
+
73
+ - Three CLI regression tests: a stale `python3` shim exits 1 with the version
74
+ message, a missing `python3` exits 1 instead of crashing, and `help` still
75
+ works with no interpreter on PATH. Test count: 1637 → 1640.
76
+
77
+ ---
78
+
10
79
  ## v4.25.1 — npm advisories count again (2026-08-19)
11
80
 
12
81
  ### Fixed
package/README.md CHANGED
@@ -6,18 +6,23 @@
6
6
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-109-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-1637%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-1645%20passing-success)](tests/)
10
10
 
11
- ## What's New in v4.25.1
11
+ ## What's New in v4.27.0
12
12
 
13
- **v4.25.1** fixes the CVE scanner path exercised by the published-package smoke test:
13
+ **v4.27.0** teaches `doctor` about a collision it used to report as healthy:
14
14
 
15
- - Legacy npm audit `advisories` are normalized alongside the modern
16
- `vulnerabilities` response.
17
- - HIGH advisories now produce findings and the documented non-zero exit code
18
- instead of a false clean result.
19
- - The post-release scanner fixture now exercises accessibility, SEO, HIPAA, and
20
- a deliberately vulnerable npm dependency with their real CLI flags.
15
+ - Uploading the Claude app plugin ZIP registers it under `~/.claude/plugins`,
16
+ which Claude Code reads too. The plugin then carries the same skills, agents,
17
+ and hooks as the global install.
18
+ - Claude Code merges plugin hooks with user hooks without deduplication, so
19
+ every toolkit hook fired twice per event and skills and agents loaded twice.
20
+ `doctor` printed `HEALTH CHECK PASSED` through all of it.
21
+ - New check 11 reports the collision with the hook count; `doctor --fix`
22
+ disables the plugin for Claude Code and leaves the global install
23
+ authoritative.
24
+ - `kb/troubleshooting/plugin-double-load.md` documents the debug-log evidence
25
+ and the verification steps.
21
26
 
22
27
  See [CHANGELOG.md](CHANGELOG.md) for full history.
23
28
 
@@ -40,6 +45,10 @@ See [CHANGELOG.md](CHANGELOG.md) for full history.
40
45
 
41
46
  ## Install
42
47
 
48
+ **Requirements:** Node.js >= 18 and Python >= 3.11.
49
+
50
+ > **macOS:** `/usr/bin/python3` is Python 3.9 and will not run the toolkit. Install a supported one with `brew install python@3.13` and make sure `which python3` no longer points at `/usr/bin/python3`.
51
+
43
52
  ```bash
44
53
  # Option A: install globally (once per machine)
45
54
  npm install -g @softspark/ai-toolkit
@@ -183,7 +192,7 @@ ai-toolkit/
183
192
  │ └── ARCHITECTURE.md # Full system design
184
193
  ├── kb/ # Reference docs, procedures, plans
185
194
  ├── scripts/ # Validation, install, evaluation scripts
186
- ├── tests/ # Bats and Python test suite (1637 tests)
195
+ ├── tests/ # Bats and Python test suite (1645 tests)
187
196
  └── CHANGELOG.md
188
197
  ```
189
198
 
@@ -3,7 +3,7 @@
3
3
  "name": "ai-toolkit",
4
4
  "displayName": "AI Toolkit",
5
5
  "description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
6
- "version": "4.25.1",
6
+ "version": "4.27.0",
7
7
  "author": {
8
8
  "name": "SoftSpark",
9
9
  "url": "https://github.com/softspark"
@@ -1,5 +1,5 @@
1
1
  {
2
- "last_run": "2026-08-19T14:01:09Z",
2
+ "last_run": "2026-08-21T15:52:27Z",
3
3
  "schema_version": 1,
4
4
  "tools": {
5
5
  "aider": {
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "augment": {
27
- "docs_hash": "0f7df1fdf4962938",
27
+ "docs_hash": "516d8db28cb3ec63",
28
28
  "headings": [
29
29
  "Admin",
30
30
  "Auggie CLI",
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  "claude-app": {
69
- "docs_hash": "5bba2a625b109b69",
69
+ "docs_hash": "cacc8b579baa5e86",
70
70
  "headings": [
71
71
  "Add global and folder instructions",
72
72
  "Availability",
@@ -107,7 +107,7 @@
107
107
  }
108
108
  },
109
109
  "claude-code": {
110
- "docs_hash": "3d4394d0bf5dd8c1",
110
+ "docs_hash": "2b3d6b11c4be766f",
111
111
  "headings": [
112
112
  "Core concepts",
113
113
  "Documentation Index",
@@ -173,10 +173,10 @@
173
173
  "userConfig": false,
174
174
  "workflows": true
175
175
  },
176
- "version": "2.1.235 (Claude Code)"
176
+ "version": "2.1.238 (Claude Code)"
177
177
  },
178
178
  "cline": {
179
- "docs_hash": "bf058b01d03e0455",
179
+ "docs_hash": "f93d3a8437c1165f",
180
180
  "headings": [
181
181
  "API Reference",
182
182
  "Best Practices",
@@ -223,7 +223,7 @@
223
223
  }
224
224
  },
225
225
  "codex-cli": {
226
- "docs_hash": "61aea7a06c8c6355",
226
+ "docs_hash": "df3b133739abfa24",
227
227
  "headings": [
228
228
  "API",
229
229
  "API Reference",
@@ -362,10 +362,10 @@
362
362
  "plugin marketplace": false,
363
363
  "sandbox": true
364
364
  },
365
- "version": "codex-cli 0.147.0"
365
+ "version": "codex-cli 0.148.0"
366
366
  },
367
367
  "cursor": {
368
- "docs_hash": "793ca4e25e676e17",
368
+ "docs_hash": "2abfadb030c12d07",
369
369
  "headings": [
370
370
  "Agent",
371
371
  "CLI",
@@ -397,7 +397,7 @@
397
397
  }
398
398
  },
399
399
  "gemini-cli": {
400
- "docs_hash": "f343c5f7c60fefcf",
400
+ "docs_hash": "c66bb0647b70c7cf",
401
401
  "headings": [
402
402
  "Breadcrumbs",
403
403
  "Directory actions",
@@ -438,7 +438,7 @@
438
438
  "version": "0.55.1"
439
439
  },
440
440
  "github-copilot": {
441
- "docs_hash": "79d741f4e3273f8d",
441
+ "docs_hash": "2e595a058e906644",
442
442
  "headings": [
443
443
  "About Copilot auto model selection",
444
444
  "About Copilot automations",
@@ -502,7 +502,7 @@
502
502
  }
503
503
  },
504
504
  "opencode": {
505
- "docs_hash": "b112229db465d2e4",
505
+ "docs_hash": "3e9f45631e71a28c",
506
506
  "headings": [
507
507
  "Add features",
508
508
  "Ask questions",
@@ -564,7 +564,7 @@
564
564
  }
565
565
  },
566
566
  "windsurf": {
567
- "docs_hash": "fddce54b82ac479e",
567
+ "docs_hash": "b08c4f883c276129",
568
568
  "headings": [
569
569
  "Accounts",
570
570
  "Advanced",
package/bin/ai-toolkit.js CHANGED
@@ -12,6 +12,9 @@ const fs = require('fs');
12
12
  const TOOLKIT_DIR = path.dirname(__dirname);
13
13
  const CWD = process.cwd();
14
14
 
15
+ /** Minimum Python the toolkit scripts require: [major, minor]. */
16
+ const PYTHON_MIN = [3, 11];
17
+
15
18
  if (!process.env.HOME) {
16
19
  console.error('Error: HOME environment variable is not set');
17
20
  process.exit(1);
@@ -139,6 +142,60 @@ function scriptPath(scriptName) {
139
142
  return path.join(TOOLKIT_DIR, 'scripts', scriptName);
140
143
  }
141
144
 
145
+ /** Memoized result of requirePython(); null until the first check runs. */
146
+ let pythonOk = null;
147
+
148
+ /**
149
+ * Verify `python3` exists and is >= PYTHON_MIN before any script is spawned.
150
+ * Without this the caller sees a raw traceback from deep inside scripts/
151
+ * (e.g. `dataclass() got an unexpected keyword argument 'slots'` on Python
152
+ * 3.9, which is what macOS ships as /usr/bin/python3).
153
+ * Exits with an actionable message when the interpreter is missing or too old.
154
+ */
155
+ function requirePython() {
156
+ if (pythonOk) return;
157
+ const probe = spawnSync('python3', ['-c', 'import sys; sys.stdout.write("%d.%d.%d" % sys.version_info[:3])'], {
158
+ encoding: 'utf8',
159
+ });
160
+ const want = PYTHON_MIN.join('.');
161
+
162
+ if (probe.error || probe.status !== 0) {
163
+ console.error(`Error: python3 not found on PATH — ai-toolkit requires Python >= ${want}`);
164
+ console.error(pythonHint());
165
+ process.exit(1);
166
+ }
167
+
168
+ const found = probe.stdout.trim();
169
+ const [major, minor] = found.split('.').map(Number);
170
+ if (major < PYTHON_MIN[0] || (major === PYTHON_MIN[0] && minor < PYTHON_MIN[1])) {
171
+ console.error(`Error: ai-toolkit requires Python >= ${want}, found ${found}`);
172
+ console.error(pythonHint());
173
+ process.exit(1);
174
+ }
175
+ pythonOk = true;
176
+ }
177
+
178
+ /**
179
+ * Platform-specific instructions for installing a supported Python.
180
+ * @returns {string} Multi-line hint text
181
+ */
182
+ function pythonHint() {
183
+ const lines = [];
184
+ if (process.platform === 'darwin') {
185
+ lines.push('macOS ships Python 3.9 as /usr/bin/python3. Install a newer one:');
186
+ lines.push(' brew install python@3.13');
187
+ lines.push('Then make sure it comes first on PATH (`which python3` should not be /usr/bin/python3).');
188
+ } else if (process.platform === 'win32') {
189
+ lines.push('Install Python 3.11+ and make sure `python3` resolves to it:');
190
+ lines.push(' winget install Python.Python.3.13');
191
+ } else {
192
+ lines.push('Install Python 3.11+ via your package manager, e.g.:');
193
+ lines.push(' sudo apt install python3.13 # Debian/Ubuntu');
194
+ lines.push(' sudo dnf install python3.13 # Fedora/RHEL');
195
+ }
196
+ return lines.join('\n');
197
+ }
198
+
142
199
  /**
143
200
  * Execute a generator script synchronously via python3, returning its stdout.
144
201
  * Exits the process on failure.
@@ -147,6 +204,7 @@ function scriptPath(scriptName) {
147
204
  * @returns {Buffer} stdout output
148
205
  */
149
206
  function runGenerator(scriptName, extraArgs = []) {
207
+ requirePython();
150
208
  try {
151
209
  return execFileSync('python3', [scriptPath(scriptName), ...extraArgs], { cwd: TOOLKIT_DIR });
152
210
  } catch (err) {
@@ -162,6 +220,7 @@ function runGenerator(scriptName, extraArgs = []) {
162
220
  * @param {{ cwd?: string }} [opts={}] - Options (cwd override)
163
221
  */
164
222
  function run(script, args = [], opts = {}) {
223
+ requirePython();
165
224
  const result = spawnSync('python3', [script, ...args], {
166
225
  stdio: 'inherit',
167
226
  cwd: opts.cwd || CWD,
@@ -178,6 +237,7 @@ function run(script, args = [], opts = {}) {
178
237
  * @param {...string} flags - Flags to pass: --rules, --hooks, --mcp
179
238
  */
180
239
  function propagateGlobal(...flags) {
240
+ requirePython();
181
241
  const result = spawnSync('python3', [scriptPath('propagate_global.py'), ...flags], {
182
242
  stdio: 'inherit',
183
243
  cwd: CWD,
@@ -4,6 +4,7 @@ service: ai-toolkit
4
4
  category: best-practices
5
5
  tags: [best-practices, guidelines]
6
6
  last_updated: "2026-03-25"
7
+ description: "Section index for ai-toolkit best-practices. No documents are filed here yet; new ones follow the best-practices template."
7
8
  ---
8
9
 
9
10
  # Best Practices
@@ -4,6 +4,7 @@ service: ai-toolkit
4
4
  category: howto
5
5
  tags: [howto, guides]
6
6
  last_updated: "2026-03-25"
7
+ description: "Section index for ai-toolkit howto. No documents are filed here yet; new ones follow the howto template."
7
8
  ---
8
9
 
9
10
  # How-To Guides
@@ -3,9 +3,12 @@ title: "Troubleshooting"
3
3
  service: ai-toolkit
4
4
  category: troubleshooting
5
5
  tags: [troubleshooting, debugging]
6
- last_updated: "2026-03-25"
6
+ last_updated: "2026-08-21"
7
+ description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
7
8
  ---
8
9
 
9
10
  # Troubleshooting
10
11
 
11
- Problem resolution guides. Guides will be added here as they are created.
12
+ Problem resolution guides.
13
+
14
+ - [Claude App Plugin Loads Twice in Claude Code](plugin-double-load.md) -- hooks fire twice after the plugin ZIP is uploaded next to a global install.
@@ -0,0 +1,97 @@
1
+ ---
2
+ title: "Claude App Plugin Loads Twice in Claude Code"
3
+ category: troubleshooting
4
+ service: ai-toolkit
5
+ tags: [plugin, claude-app, hooks, duplication, doctor]
6
+ created: "2026-08-21"
7
+ last_updated: "2026-08-21"
8
+ description: "Every toolkit hook fires twice and skills and agents load twice after the Claude app plugin ZIP is uploaded on a machine that already has the global install. Cause: the app registers the plugin under ~/.claude/plugins, which Claude Code also reads."
9
+ ---
10
+
11
+ # Claude App Plugin Loads Twice in Claude Code
12
+
13
+ ## Symptom
14
+
15
+ After uploading `ai-toolkit-claude-app.zip` through `Customize > Plugins`, Claude
16
+ Code sessions get slower and every hook side effect appears twice: duplicate rows
17
+ in `governance.log`, duplicate session state writes, two Stop gates per turn.
18
+ `ai-toolkit doctor` reports a healthy install because it only inspected
19
+ `~/.claude` and `app/plugins`.
20
+
21
+ ## Cause
22
+
23
+ The Claude app writes uploaded plugins into
24
+ `~/.claude/plugins/marketplaces/local-desktop-app-uploads/` and sets
25
+ `enabledPlugins` in `~/.claude/settings.json`. Both paths belong to Claude Code
26
+ as well, so Claude Code loads the plugin on top of the global install. The
27
+ bundle carries the same catalog as `~/.claude`, and plugin hooks merge with user
28
+ hooks without deduplication.
29
+
30
+ Confirmed in a Claude Code debug log (`claude --debug -p ...`, then read
31
+ `~/.claude/debug/latest`):
32
+
33
+ ```
34
+ Loaded 111 unique skills (... user: 111 ...)
35
+ Total plugin skills loaded: 111 (0 duplicate/user-owned entries skipped)
36
+ Total plugin agents loaded: 44
37
+ Read manifest hooks for plugin ai-toolkit (enabled=true): ./claude-app/hooks/hooks.json
38
+ ```
39
+
40
+ The `skills` manifest field adds to the default `skills/` directory instead of
41
+ replacing it, so the plugin contributes its 109 catalog skills plus the 2
42
+ app-only rule skills.
43
+
44
+ ## Diagnosis
45
+
46
+ ```bash
47
+ ai-toolkit doctor
48
+ ```
49
+
50
+ Check 11 reports the collision:
51
+
52
+ ```
53
+ ## 11. Plugin Double-Load
54
+ WARN: ai-toolkit@local-desktop-app-uploads is active next to the global install: 28 toolkit hooks fire twice per event and skills/agents load twice (run: ai-toolkit doctor --fix)
55
+ ```
56
+
57
+ To see the duplication directly, count hook invocations per source in a fresh
58
+ session log:
59
+
60
+ ```bash
61
+ grep -oE "[^\"' ]*hooks/[a-z0-9._-]+\.sh" ~/.claude/debug/latest | sort | uniq -c
62
+ ```
63
+
64
+ ## Fix
65
+
66
+ ```bash
67
+ ai-toolkit doctor --fix
68
+ ```
69
+
70
+ That sets the plugin to `false` in `enabledPlugins` and leaves the global
71
+ install authoritative. Claude Code then logs
72
+ `enabled=false; will NOT register, plugin is disabled`, and plugin skills and
73
+ agents drop to 0.
74
+
75
+ The global install is the richer surface for Claude Code: it delivers rules as
76
+ real files under `~/.claude/rules/`, which are always in context, while the
77
+ plugin exposes them as an `ai-toolkit-rules` skill the model has to load. The
78
+ plugin also has no `session-context.sh` hook.
79
+
80
+ Keep the plugin enabled only when Claude Code has no global install, for example
81
+ a machine that runs the Claude app alone. In that case the plugin is the single
82
+ source and check 11 stays quiet.
83
+
84
+ ## Verification
85
+
86
+ ```bash
87
+ claude --debug -p "ok"
88
+ grep -E "Total plugin (skills|agents) loaded|enabled=" ~/.claude/debug/latest
89
+ ```
90
+
91
+ Expect `Total plugin skills loaded: 0`, `Total plugin agents loaded: 0`, and the
92
+ `plugin is disabled` line.
93
+
94
+ ## Related
95
+
96
+ - `kb/reference/global-install-model.md`
97
+ - `kb/procedures/maintenance-sop.md`
package/llms-full.txt CHANGED
@@ -73,6 +73,7 @@
73
73
  - [Unique Features & Differentiators](kb/reference/unique-features.md)
74
74
  - [Windows Support](kb/reference/windows-support.md)
75
75
  - [Troubleshooting](kb/troubleshooting/README.md)
76
+ - [Claude App Plugin Loads Twice in Claude Code](kb/troubleshooting/plugin-double-load.md)
76
77
 
77
78
  ## Skills
78
79
 
@@ -243,6 +244,7 @@ service: ai-toolkit
243
244
  category: best-practices
244
245
  tags: [best-practices, guidelines]
245
246
  last_updated: "2026-03-25"
247
+ description: "Section index for ai-toolkit best-practices. No documents are filed here yet; new ones follow the best-practices template."
246
248
  ---
247
249
 
248
250
  # Best Practices
@@ -5081,6 +5083,7 @@ service: ai-toolkit
5081
5083
  category: howto
5082
5084
  tags: [howto, guides]
5083
5085
  last_updated: "2026-03-25"
5086
+ description: "Section index for ai-toolkit howto. No documents are filed here yet; new ones follow the howto template."
5084
5087
  ---
5085
5088
 
5086
5089
  # How-To Guides
@@ -16128,12 +16131,117 @@ title: "Troubleshooting"
16128
16131
  service: ai-toolkit
16129
16132
  category: troubleshooting
16130
16133
  tags: [troubleshooting, debugging]
16131
- last_updated: "2026-03-25"
16134
+ last_updated: "2026-08-21"
16135
+ description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
16132
16136
  ---
16133
16137
 
16134
16138
  # Troubleshooting
16135
16139
 
16136
- Problem resolution guides. Guides will be added here as they are created.
16140
+ Problem resolution guides.
16141
+
16142
+ - [Claude App Plugin Loads Twice in Claude Code](plugin-double-load.md) -- hooks fire twice after the plugin ZIP is uploaded next to a global install.
16143
+
16144
+ ---
16145
+
16146
+ ## kb/troubleshooting/plugin-double-load.md
16147
+
16148
+ ---
16149
+ title: "Claude App Plugin Loads Twice in Claude Code"
16150
+ category: troubleshooting
16151
+ service: ai-toolkit
16152
+ tags: [plugin, claude-app, hooks, duplication, doctor]
16153
+ created: "2026-08-21"
16154
+ last_updated: "2026-08-21"
16155
+ description: "Every toolkit hook fires twice and skills and agents load twice after the Claude app plugin ZIP is uploaded on a machine that already has the global install. Cause: the app registers the plugin under ~/.claude/plugins, which Claude Code also reads."
16156
+ ---
16157
+
16158
+ # Claude App Plugin Loads Twice in Claude Code
16159
+
16160
+ ## Symptom
16161
+
16162
+ After uploading `ai-toolkit-claude-app.zip` through `Customize > Plugins`, Claude
16163
+ Code sessions get slower and every hook side effect appears twice: duplicate rows
16164
+ in `governance.log`, duplicate session state writes, two Stop gates per turn.
16165
+ `ai-toolkit doctor` reports a healthy install because it only inspected
16166
+ `~/.claude` and `app/plugins`.
16167
+
16168
+ ## Cause
16169
+
16170
+ The Claude app writes uploaded plugins into
16171
+ `~/.claude/plugins/marketplaces/local-desktop-app-uploads/` and sets
16172
+ `enabledPlugins` in `~/.claude/settings.json`. Both paths belong to Claude Code
16173
+ as well, so Claude Code loads the plugin on top of the global install. The
16174
+ bundle carries the same catalog as `~/.claude`, and plugin hooks merge with user
16175
+ hooks without deduplication.
16176
+
16177
+ Confirmed in a Claude Code debug log (`claude --debug -p ...`, then read
16178
+ `~/.claude/debug/latest`):
16179
+
16180
+ ```
16181
+ Loaded 111 unique skills (... user: 111 ...)
16182
+ Total plugin skills loaded: 111 (0 duplicate/user-owned entries skipped)
16183
+ Total plugin agents loaded: 44
16184
+ Read manifest hooks for plugin ai-toolkit (enabled=true): ./claude-app/hooks/hooks.json
16185
+ ```
16186
+
16187
+ The `skills` manifest field adds to the default `skills/` directory instead of
16188
+ replacing it, so the plugin contributes its 109 catalog skills plus the 2
16189
+ app-only rule skills.
16190
+
16191
+ ## Diagnosis
16192
+
16193
+ ```bash
16194
+ ai-toolkit doctor
16195
+ ```
16196
+
16197
+ Check 11 reports the collision:
16198
+
16199
+ ```
16200
+ ## 11. Plugin Double-Load
16201
+ WARN: ai-toolkit@local-desktop-app-uploads is active next to the global install: 28 toolkit hooks fire twice per event and skills/agents load twice (run: ai-toolkit doctor --fix)
16202
+ ```
16203
+
16204
+ To see the duplication directly, count hook invocations per source in a fresh
16205
+ session log:
16206
+
16207
+ ```bash
16208
+ grep -oE "[^\"' ]*hooks/[a-z0-9._-]+\.sh" ~/.claude/debug/latest | sort | uniq -c
16209
+ ```
16210
+
16211
+ ## Fix
16212
+
16213
+ ```bash
16214
+ ai-toolkit doctor --fix
16215
+ ```
16216
+
16217
+ That sets the plugin to `false` in `enabledPlugins` and leaves the global
16218
+ install authoritative. Claude Code then logs
16219
+ `enabled=false; will NOT register, plugin is disabled`, and plugin skills and
16220
+ agents drop to 0.
16221
+
16222
+ The global install is the richer surface for Claude Code: it delivers rules as
16223
+ real files under `~/.claude/rules/`, which are always in context, while the
16224
+ plugin exposes them as an `ai-toolkit-rules` skill the model has to load. The
16225
+ plugin also has no `session-context.sh` hook.
16226
+
16227
+ Keep the plugin enabled only when Claude Code has no global install, for example
16228
+ a machine that runs the Claude app alone. In that case the plugin is the single
16229
+ source and check 11 stays quiet.
16230
+
16231
+ ## Verification
16232
+
16233
+ ```bash
16234
+ claude --debug -p "ok"
16235
+ grep -E "Total plugin (skills|agents) loaded|enabled=" ~/.claude/debug/latest
16236
+ ```
16237
+
16238
+ Expect `Total plugin skills loaded: 0`, `Total plugin agents loaded: 0`, and the
16239
+ `plugin is disabled` line.
16240
+
16241
+ ## Related
16242
+
16243
+ - `kb/reference/global-install-model.md`
16244
+ - `kb/procedures/maintenance-sop.md`
16137
16245
 
16138
16246
  ---
16139
16247
 
package/llms.txt CHANGED
@@ -73,3 +73,4 @@
73
73
  - [Unique Features & Differentiators](kb/reference/unique-features.md)
74
74
  - [Windows Support](kb/reference/windows-support.md)
75
75
  - [Troubleshooting](kb/troubleshooting/README.md)
76
+ - [Claude App Plugin Loads Twice in Claude Code](kb/troubleshooting/plugin-double-load.md)
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.25.1",
2
+ "version": "4.27.0",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "4.25.1",
3
+ "version": "4.27.0",
4
4
  "description": "AI coding toolkit: 109 skills, 44 agents, 12 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
5
5
  "keywords": [
6
6
  "claude",
@@ -20,8 +20,35 @@ from __future__ import annotations
20
20
  import os
21
21
  import platform
22
22
  import shutil
23
+ import sys
23
24
  from pathlib import Path
24
25
 
26
+ # ---------------------------------------------------------------------------
27
+ # Python version floor
28
+ # ---------------------------------------------------------------------------
29
+ # Kept in sync with PYTHON_MIN in bin/ai-toolkit.js and the python3 entry in
30
+ # scripts/check_deps.py. Checked here because _common is imported first by
31
+ # every entry point, so a stale interpreter fails with this message instead of
32
+ # a traceback from whichever module happens to use 3.11+ syntax.
33
+ PYTHON_MIN = (3, 11)
34
+
35
+ if sys.version_info < PYTHON_MIN:
36
+ _want = ".".join(str(n) for n in PYTHON_MIN)
37
+ _found = ".".join(str(n) for n in sys.version_info[:3])
38
+ sys.stderr.write(
39
+ "Error: ai-toolkit requires Python >= {}, found {} ({})\n".format(
40
+ _want, _found, sys.executable
41
+ )
42
+ )
43
+ if sys.platform == "darwin":
44
+ sys.stderr.write(
45
+ "macOS ships Python 3.9 as /usr/bin/python3. Install a newer one:\n"
46
+ " brew install python@3.13\n"
47
+ )
48
+ else:
49
+ sys.stderr.write("Install Python 3.11+ via your package manager.\n")
50
+ sys.exit(1)
51
+
25
52
  # ---------------------------------------------------------------------------
26
53
  # Re-exports from frontmatter module
27
54
  # ---------------------------------------------------------------------------
@@ -33,7 +33,7 @@ REQUIRED = [
33
33
  {
34
34
  "name": "python3",
35
35
  "check": "python3",
36
- "min_version": "3.8",
36
+ "min_version": "3.11",
37
37
  "packages": {
38
38
  "brew": "python3",
39
39
  "apt": "python3",
@@ -45,7 +45,7 @@ REQUIRED = [
45
45
  "choco": "python",
46
46
  "scoop": "python",
47
47
  },
48
- "reason": "All toolkit scripts run on Python 3 (stdlib only, no pip needed)",
48
+ "reason": "All toolkit scripts run on Python 3.11+ (stdlib only, no pip needed). macOS /usr/bin/python3 is 3.9 and will not work.",
49
49
  },
50
50
  {
51
51
  "name": "git",
package/scripts/doctor.py CHANGED
@@ -15,6 +15,8 @@ Checks:
15
15
  7. Benchmark freshness
16
16
  8. Stale rules
17
17
  9. URL hook sources
18
+ 10. Language rules drift (project-local)
19
+ 11. Plugin double-load (Claude app plugin vs global install)
18
20
 
19
21
  Exit codes:
20
22
  0 all checks pass
@@ -44,6 +46,7 @@ HOOKS_DIR = _HOOKS_DIR
44
46
  RULES_DIR = _RULES_DIR
45
47
  EXTERNAL_HOOKS_DIR = _EXTERNAL_HOOKS_DIR
46
48
  BENCHMARK_DASHBOARD = toolkit_dir / "benchmarks" / "ecosystem-dashboard.json"
49
+ PLUGIN_REGISTRY = CLAUDE_DIR / "plugins" / "installed_plugins.json"
47
50
 
48
51
  VALID_EVENTS = frozenset({
49
52
  "SessionStart", "Notification", "PreToolUse", "PostToolUse", "Stop",
@@ -668,6 +671,84 @@ def check_language_drift(dr: DiagResult) -> None:
668
671
  dr.warn(f"{lang} detected but {skill} not injected — run: ai-toolkit install --local --lang {lang}")
669
672
 
670
673
 
674
+ # ---------------------------------------------------------------------------
675
+ # Check 11: Plugin Double-Load
676
+ # ---------------------------------------------------------------------------
677
+
678
+ def _registered_toolkit_plugins(registry: dict) -> list[str]:
679
+ """Return ai-toolkit plugin keys from the Claude Code plugin registry."""
680
+ plugins = registry.get("plugins", {})
681
+ if not isinstance(plugins, dict):
682
+ return []
683
+ return [key for key in plugins if str(key).split("@", 1)[0] == "ai-toolkit"]
684
+
685
+
686
+ def check_plugin_double_load(dr: DiagResult, fix_mode: bool) -> None:
687
+ """Warn when the Claude app plugin and the global install both feed Claude Code.
688
+
689
+ Uploading the ``claude-app export`` ZIP from the Claude app registers it under
690
+ ``~/.claude/plugins``, which Claude Code reads as well. The plugin carries the
691
+ same skills, agents, and hooks as the global install, and Claude Code merges
692
+ plugin hooks with user hooks without deduplication, so every toolkit hook runs
693
+ twice per event.
694
+ """
695
+ print()
696
+ print("## 11. Plugin Double-Load")
697
+
698
+ if not PLUGIN_REGISTRY.is_file():
699
+ dr.skip("no Claude Code plugin registry")
700
+ return
701
+ try:
702
+ registry = json.loads(PLUGIN_REGISTRY.read_text(encoding="utf-8"))
703
+ except (OSError, json.JSONDecodeError):
704
+ dr.warn(f"{PLUGIN_REGISTRY} is not valid JSON")
705
+ return
706
+
707
+ keys = _registered_toolkit_plugins(registry)
708
+ if not keys:
709
+ dr.ok("no ai-toolkit plugin registered in Claude Code")
710
+ return
711
+
712
+ settings_json = CLAUDE_DIR / "settings.json"
713
+ try:
714
+ settings = json.loads(settings_json.read_text(encoding="utf-8"))
715
+ except (OSError, json.JSONDecodeError):
716
+ settings = {}
717
+ enabled = settings.get("enabledPlugins", {})
718
+ if not isinstance(enabled, dict):
719
+ enabled = {}
720
+
721
+ active = [key for key in keys if enabled.get(key, True)]
722
+ if not active:
723
+ dr.ok("ai-toolkit plugin registered but disabled for Claude Code")
724
+ return
725
+
726
+ hook_count, _ = _installed_toolkit_hook_count(settings)
727
+ if hook_count == 0:
728
+ dr.ok("ai-toolkit plugin active without global hooks (single source)")
729
+ return
730
+
731
+ for key in active:
732
+ dr.warn(
733
+ f"{key} is active next to the global install: {hook_count} toolkit hooks "
734
+ "fire twice per event and skills/agents load twice "
735
+ "(run: ai-toolkit doctor --fix)"
736
+ )
737
+
738
+ if not fix_mode:
739
+ return
740
+
741
+ enabled.update({key: False for key in active})
742
+ settings["enabledPlugins"] = enabled
743
+ try:
744
+ settings_json.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
745
+ except OSError as exc:
746
+ dr.fail(f"could not disable plugin in settings.json: {exc}")
747
+ return
748
+ for key in active:
749
+ dr.fixed(f"disabled {key} for Claude Code (global install stays authoritative)")
750
+
751
+
671
752
  # ---------------------------------------------------------------------------
672
753
  # Main
673
754
  # ---------------------------------------------------------------------------
@@ -691,6 +772,7 @@ def main() -> None:
691
772
  check_stale_rules(dr, fix_mode)
692
773
  check_url_hooks(dr, fix_mode)
693
774
  check_language_drift(dr)
775
+ check_plugin_double_load(dr, fix_mode)
694
776
 
695
777
  # Summary
696
778
  print("========================")