@softspark/ai-toolkit 4.26.0 → 4.28.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,83 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.28.0 — MCP servers reach Chat and Cowork (2026-08-21)
11
+
12
+ ### Added
13
+
14
+ - **`claude-app` MCP adapter.** `ai-toolkit mcp install --editor claude-app
15
+ --scope global <template>` writes `claude_desktop_config.json` — macOS
16
+ `~/Library/Application Support/Claude/`, Windows `%APPDATA%/Claude/`, Linux
17
+ `~/.config/Claude/`, with `CLAUDE_USER_DATA_DIR` overriding the root the same
18
+ way the app itself honors it. Until now the Claude app was reachable only
19
+ through an uploaded plugin ZIP, so a user with seven MCP servers configured for
20
+ Claude Code still had an empty `mcpServers` block in Chat and Cowork. The app
21
+ parses this file on startup and reports invalid entries in its own warning
22
+ dialog, so it was always a supported surface — ai-toolkit just never wrote to
23
+ it. Note that `--editor claude` targets Claude Code (`.mcp.json`,
24
+ `~/.claude.json`); the two are different runtimes and different files.
25
+ - **HTTP and SSE templates are bridged through `mcp-remote`.** The app validates
26
+ each entry as `{command, args, env}`; remote endpoints live in a separate
27
+ `remoteMcpServers` surface managed from its Connectors UI and are not
28
+ file-configurable. Writing a portable `{"type":"http","url":…}` entry verbatim
29
+ would produce a config the app silently skips, so the adapter wraps remote
30
+ servers as `npx -y mcp-remote <url>`, forwarding `headers` as `--header`
31
+ arguments. `mcp-remote` negotiates the transport itself, so one bridge shape
32
+ covers both the `http` and `sse` spellings. This also applies to
33
+ `ai-toolkit inject-mcp`, which propagates to every editor exposing a global
34
+ path and therefore now reaches the Claude app too.
35
+
36
+ ### Changed
37
+
38
+ - `_resolve_global_config_root` gained a `_validate_configured_config_root`
39
+ helper, shared with the new adapter. `COPILOT_HOME` and `CODEX_HOME` keep
40
+ their existing semantics; the Claude app root is validated only when set
41
+ explicitly, because its platform default need not exist yet.
42
+
43
+ ### Fixed
44
+
45
+ - **Two docs described the `claude` adapter's config path wrongly.**
46
+ `kb/reference/mcp-editor-compatibility.md` and `kb/reference/mcp-templates.md`
47
+ both claimed it writes `.claude/settings.local.json` and
48
+ `~/.claude/settings.json`. It writes `.mcp.json` and `~/.claude.json`, and has
49
+ for as long as the adapter has existed. Both scopes were wrong in both files.
50
+
51
+ ### Ecosystem
52
+
53
+ - `claude-app` gains three `config_paths`, the `scripts/mcp_editors.py`
54
+ generator, and an `MCP` capability marker. Re-baseline with
55
+ `python3 scripts/ecosystem_doctor.py --update --tool claude-app`.
56
+
57
+ ### Tests
58
+
59
+ - 1645 → 1659. Thirteen cover the adapter (path resolution, parent-tree
60
+ creation, preservation of `preferences`/`coworkUserFilesPath`/user-owned
61
+ servers, byte-identical re-install, the bridge shape, rejection of entries
62
+ with neither or both transports, four `CLAUDE_USER_DATA_DIR` validation cases,
63
+ removal, and transactional rollback). One covers `inject-mcp` propagation,
64
+ where per-editor failures are downgraded to warnings and a regression would
65
+ otherwise be silent.
66
+
67
+ ---
68
+
69
+ ## v4.27.0 — Doctor sees the Claude app plugin (2026-08-21)
70
+
71
+ ### Added
72
+
73
+ - **Doctor check 11: plugin double-load.** `ai-toolkit doctor` now reads the
74
+ Claude Code plugin registry (`~/.claude/plugins/installed_plugins.json`) and
75
+ `enabledPlugins` in `~/.claude/settings.json`. When an uploaded Claude app
76
+ plugin is active next to the global install, both feed the same session:
77
+ Claude Code merges plugin hooks with user hooks without deduplication, so every
78
+ toolkit hook fires twice per event and skills and agents load twice. The check
79
+ warns with the hook count, and `--fix` disables the plugin for Claude Code
80
+ while leaving the global install authoritative. Previously this passed as a
81
+ healthy install with no signal at all.
82
+ - `kb/troubleshooting/plugin-double-load.md` documents the symptom, the debug-log
83
+ evidence, and the fix.
84
+
85
+ ---
86
+
10
87
  ## v4.26.0 — Python floor is declared and enforced (2026-08-21)
11
88
 
12
89
  ### Added
package/README.md CHANGED
@@ -6,21 +6,22 @@
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-1640%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-1659%20passing-success)](tests/)
10
10
 
11
- ## What's New in v4.26.0
11
+ ## What's New in v4.28.0
12
12
 
13
- **v4.26.0** declares and enforces the Python floor the scripts already needed:
13
+ **v4.28.0** gives Claude Chat and Cowork their MCP servers:
14
14
 
15
- - `ai-toolkit` now checks `python3` before running anything and requires 3.11+.
16
- - macOS `/usr/bin/python3` is 3.9, which used to crash `update --local` with a
17
- raw `dataclass() got an unexpected keyword argument 'slots'` traceback. You
18
- now get the version, the requirement, and `brew install python@3.13`.
19
- - `python3 scripts/*.py` run directly fails the same way, via `_common.py`.
20
- - CI runs the syntax check plus a full import sweep on both 3.11 and 3.13, so a
21
- version-gated runtime feature cannot slip through `py_compile` again.
22
-
23
- See [CHANGELOG.md](CHANGELOG.md) for full history.
15
+ - The Claude app parses `claude_desktop_config.json` on startup and reports bad
16
+ entries in its own warning dialog, but ai-toolkit never wrote to it. A user
17
+ with seven servers configured for Claude Code still had an empty `mcpServers`
18
+ block in Cowork.
19
+ - `ai-toolkit mcp install --editor claude-app --scope global <template>` now
20
+ writes it, on macOS, Windows, and Linux, honoring `CLAUDE_USER_DATA_DIR` the
21
+ way the app does. `--editor claude` remains Claude Code -- different runtime,
22
+ different file.
23
+ - The app accepts stdio entries only, so HTTP/SSE templates are bridged as
24
+ `npx -y mcp-remote <url>` rather than written verbatim and silently skipped.
24
25
 
25
26
  ## Table of Contents
26
27
 
@@ -139,7 +140,7 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
139
140
  | Platform | Config Files | Hooks | Scope |
140
141
  |----------|-------------|:-----:|-------|
141
142
  | Claude Code | `~/.claude/agents`, `~/.claude/skills`, `~/.claude/rules/*.md`, `~/.claude/settings.json` | ✅ | global |
142
- | Claude Chat / Cowork | uploaded plugin ZIP + UI global/folder instructions | Cowork only | account/app |
143
+ | Claude Chat / Cowork | uploaded plugin ZIP + UI global/folder instructions + `claude_desktop_config.json` (MCP) | Cowork only | account/app |
143
144
  | Cursor | `.cursor/rules/*.mdc` + `.cursor/mcp.json` + `.cursor/skills/*` | ✅ | project (`~/.cursor/mcp.json` for MCP only) |
144
145
  | Windsurf (Devin Desktop) | `~/.config/devin/AGENTS.md` + `.devin/rules/*.md` + `.devin/hooks.v1.json` + `.windsurf/skills/*` | ✅ | global + project |
145
146
  | Gemini CLI | `~/.gemini/GEMINI.md` + `.gemini/settings.json` + `.gemini/{commands,skills,agents}/` | ✅ | project + user |
@@ -152,7 +153,7 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
152
153
  | Codex CLI | Project: `AGENTS.md` + `.agents/skills/*` + `.codex/{agents,hooks}/` + `.codex/{hooks.json,config.toml}`; user: `$CODEX_HOME/{AGENTS.md,agents,hooks.json,config.toml}` + `$HOME/.agents/skills/*` | ✅ | project + user |
153
154
  | opencode | `AGENTS.md` + `.opencode/{agents,commands,plugins,skills}/*` + `opencode.{json,jsonc}` | ✅ | project + global (`~/.config/opencode/`) |
154
155
 
155
- > Claude Code is always installed (primary platform). Other editors are selected with `--editors`; the Claude app uses the separate `claude-app export` flow because its customization store is UI/plugin-managed. The **Hooks** column marks platforms with lifecycle enforcement. Platforms marked — receive guidance without blocking hooks.
156
+ > Claude Code is always installed (primary platform). Other editors are selected with `--editors`; the Claude app uses the separate `claude-app export` flow because its customization store is UI/plugin-managed, except for MCP servers, which `ai-toolkit mcp install --editor claude-app --scope global` writes straight to `claude_desktop_config.json`. The **Hooks** column marks platforms with lifecycle enforcement. Platforms marked — receive guidance without blocking hooks.
156
157
 
157
158
  ---
158
159
 
@@ -188,7 +189,7 @@ ai-toolkit/
188
189
  │ └── ARCHITECTURE.md # Full system design
189
190
  ├── kb/ # Reference docs, procedures, plans
190
191
  ├── scripts/ # Validation, install, evaluation scripts
191
- ├── tests/ # Bats and Python test suite (1640 tests)
192
+ ├── tests/ # Bats and Python test suite (1659 tests)
192
193
  └── CHANGELOG.md
193
194
  ```
194
195
 
@@ -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.26.0",
6
+ "version": "4.28.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-21T08:34:52Z",
2
+ "last_run": "2026-08-21T18:54:11Z",
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": "516d8db28cb3ec63",
27
+ "docs_hash": "fda8f5ca2c114b30",
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": "7e06a1985d5bb822",
69
+ "docs_hash": "73b44d89c7b771d9",
70
70
  "headings": [
71
71
  "Add global and folder instructions",
72
72
  "Availability",
@@ -101,13 +101,14 @@
101
101
  "markers": {
102
102
  "Folder instructions": true,
103
103
  "Global instructions": true,
104
+ "MCP": true,
104
105
  "plugins": true,
105
106
  "skills": true,
106
107
  "sub-agents": true
107
108
  }
108
109
  },
109
110
  "claude-code": {
110
- "docs_hash": "2b3d6b11c4be766f",
111
+ "docs_hash": "1666675fbb24aaad",
111
112
  "headings": [
112
113
  "Core concepts",
113
114
  "Documentation Index",
@@ -176,7 +177,7 @@
176
177
  "version": "2.1.238 (Claude Code)"
177
178
  },
178
179
  "cline": {
179
- "docs_hash": "f93d3a8437c1165f",
180
+ "docs_hash": "a0753bb220f610ba",
180
181
  "headings": [
181
182
  "API Reference",
182
183
  "Best Practices",
@@ -223,7 +224,7 @@
223
224
  }
224
225
  },
225
226
  "codex-cli": {
226
- "docs_hash": "fc1c7e43bec9f748",
227
+ "docs_hash": "61d85bae5c0cf885",
227
228
  "headings": [
228
229
  "API",
229
230
  "API Reference",
@@ -365,7 +366,7 @@
365
366
  "version": "codex-cli 0.148.0"
366
367
  },
367
368
  "cursor": {
368
- "docs_hash": "2abfadb030c12d07",
369
+ "docs_hash": "189e666436c73107",
369
370
  "headings": [
370
371
  "Agent",
371
372
  "CLI",
@@ -397,7 +398,7 @@
397
398
  }
398
399
  },
399
400
  "gemini-cli": {
400
- "docs_hash": "a2c9759e0296670d",
401
+ "docs_hash": "547969f433375d0c",
401
402
  "headings": [
402
403
  "Breadcrumbs",
403
404
  "Directory actions",
@@ -438,7 +439,7 @@
438
439
  "version": "0.55.1"
439
440
  },
440
441
  "github-copilot": {
441
- "docs_hash": "ab9b7f5b4b692e05",
442
+ "docs_hash": "a313606be316fada",
442
443
  "headings": [
443
444
  "About Copilot auto model selection",
444
445
  "About Copilot automations",
@@ -475,7 +476,7 @@
475
476
  }
476
477
  },
477
478
  "google-antigravity": {
478
- "docs_hash": "9fe532d247c3e361",
479
+ "docs_hash": "8cbf9aaffb6b57bf",
479
480
  "headings": [],
480
481
  "markers": {
481
482
  "AGENTS.md": false,
@@ -502,7 +503,7 @@
502
503
  }
503
504
  },
504
505
  "opencode": {
505
- "docs_hash": "4287f7d767dda4a8",
506
+ "docs_hash": "3e9f45631e71a28c",
506
507
  "headings": [
507
508
  "Add features",
508
509
  "Ask questions",
@@ -564,7 +565,7 @@
564
565
  }
565
566
  },
566
567
  "windsurf": {
567
- "docs_hash": "60e25f8cb4c381be",
568
+ "docs_hash": "8bf9d2e8b6d36350",
568
569
  "headings": [
569
570
  "Accounts",
570
571
  "Advanced",
@@ -26,8 +26,11 @@ active `CODEX_HOME` (default `~/.codex`) and shared user skills use
26
26
  Codex user surface.
27
27
 
28
28
  Claude Chat/Desktop/Cowork is a separate runtime. It does not read the
29
- filesystem surfaces under `~/.claude`; it receives ai-toolkit through an
30
- uploaded plugin plus app-managed global/folder instructions.
29
+ filesystem surfaces under `~/.claude`; it receives ai-toolkit skills, agents,
30
+ hooks, and rules through an uploaded plugin plus app-managed global/folder
31
+ instructions. Its one file-based surface is `claude_desktop_config.json`, which
32
+ the app parses for local MCP servers -- `ai-toolkit mcp install --editor
33
+ claude-app --scope global` targets it directly, without a plugin.
31
34
 
32
35
  ## Command Responsibilities
33
36
 
@@ -3,9 +3,9 @@ title: "AI Toolkit - MCP Editor Compatibility"
3
3
  category: reference
4
4
  service: ai-toolkit
5
5
  tags: [mcp, editors, compatibility, codex, cursor, antigravity]
6
- version: "1.3.1"
6
+ version: "1.4.0"
7
7
  created: "2026-04-12"
8
- last_updated: "2026-08-19"
8
+ last_updated: "2026-08-21"
9
9
  description: "Official MCP support matrix and native config targets for editors supported by ai-toolkit."
10
10
  ---
11
11
 
@@ -19,7 +19,8 @@ ai-toolkit keeps `.mcp.json` as the project-level canonical template format and
19
19
 
20
20
  | Editor | Scope | Native Config Path | Adapter Behavior |
21
21
  |--------|-------|--------------------|------------------|
22
- | Claude Code | project + global | `.claude/settings.local.json`, `~/.claude/settings.json` | Merges `mcpServers` while preserving other settings keys |
22
+ | Claude Code | project + global | `.mcp.json`, `~/.claude.json` | Merges `mcpServers` while preserving other top-level keys |
23
+ | Claude Chat / Cowork | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux); `CLAUDE_USER_DATA_DIR` overrides the root | Emits the app's stdio-only `{command, args, env}` schema and bridges HTTP/SSE servers through `mcp-remote`; preserves `preferences`, `coworkUserFilesPath`, and user-owned servers |
23
24
  | Cursor | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors `mcpServers` directly |
24
25
  | GitHub Copilot | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds Copilot-required `type` and `tools` fields |
25
26
  | Gemini CLI | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Merges `mcpServers` into settings JSON |
@@ -46,9 +47,32 @@ ai-toolkit mcp install --editor cursor --scope project github --target .
46
47
  ai-toolkit mcp install --editor antigravity --scope project context7 --target .
47
48
  ai-toolkit mcp install --editor codex --scope project context7 --target .
48
49
  ai-toolkit mcp install --editor codex context7
50
+ ai-toolkit mcp install --editor claude-app --scope global context7
49
51
  ai-toolkit mcp remove github --editor cursor --scope project --target .
50
52
  ```
51
53
 
54
+ ## Claude Chat / Cowork
55
+
56
+ The Claude app is the one target whose MCP surface is file-based while everything
57
+ else about it is not. Skills, agents, hooks, and rules reach Chat/Cowork only
58
+ through the uploaded plugin ZIP (`ai-toolkit claude-app export`), because the app
59
+ scans no filesystem location for them. Local MCP servers are different: the app
60
+ parses `claude_desktop_config.json` on startup and reports invalid entries in its
61
+ own warning dialog.
62
+
63
+ Two constraints shape the adapter:
64
+
65
+ - **stdio only.** Each entry is validated as `{command, args, env}`. Remote
66
+ endpoints live in a separate `remoteMcpServers` surface that the app manages
67
+ through its Connectors UI and does not read from this file. Portable HTTP/SSE
68
+ templates are therefore wrapped in `mcp-remote`, which negotiates the transport
69
+ itself, so one bridge shape covers both spellings.
70
+ - **The file is not ours.** It also holds `preferences` and `coworkUserFilesPath`,
71
+ which the app rewrites on its own. The adapter merges only `mcpServers` and
72
+ leaves every other key untouched.
73
+
74
+ The app must be restarted for a config change to take effect.
75
+
52
76
  ## Install Flow Integration
53
77
 
54
78
  When `.mcp.json` exists in a project, `ai-toolkit install --local` mirrors its servers into:
@@ -3,9 +3,9 @@ title: "MCP Server Templates"
3
3
  category: reference
4
4
  service: ai-toolkit
5
5
  tags: [mcp, templates, servers, configuration, editors, inject-mcp, external-templates]
6
- version: "1.4.1"
6
+ version: "1.5.0"
7
7
  created: "2026-04-07"
8
- last_updated: "2026-08-19"
8
+ last_updated: "2026-08-21"
9
9
  description: "Reference for 26 built-in MCP server templates, external template injection via inject-mcp, and native editor MCP installation support."
10
10
  ---
11
11
 
@@ -39,6 +39,7 @@ The `add` command merges the `mcpServers` block from the template into `.mcp.jso
39
39
  The `install` command renders the same canonical template into an editor-native config format:
40
40
  - JSON clients with `mcpServers` blocks: Claude Code, Cursor, Gemini CLI, Google Antigravity, Roo Code, Windsurf, Cline, Augment
41
41
  - JSON clients with additional transport metadata: GitHub Copilot
42
+ - JSON clients restricted to stdio: Claude Chat/Cowork (`claude-app`) -- remote servers are bridged through `mcp-remote`
42
43
  - TOML clients: Codex CLI (`[mcp_servers.<name>]`)
43
44
 
44
45
  When `install` runs with `--scope project`, ai-toolkit also updates the project's `.mcp.json` so it remains the source of truth for later syncs.
@@ -47,7 +48,8 @@ When `install` runs with `--scope project`, ai-toolkit also updates the project'
47
48
 
48
49
  | Editor | Scope | Native Config Path | Notes |
49
50
  |--------|-------|--------------------|-------|
50
- | `claude` | project + global | `.claude/settings.local.json`, `~/.claude/settings.json` | Preserves existing hooks and env keys |
51
+ | `claude` | project + global | `.mcp.json`, `~/.claude.json` | Preserves unrelated top-level keys |
52
+ | `claude-app` | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux) | Claude Chat/Cowork; stdio-only schema, bridges HTTP/SSE through `mcp-remote`, preserves `preferences` and `coworkUserFilesPath`; `CLAUDE_USER_DATA_DIR` overrides the root |
51
53
  | `cursor` | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors canonical `mcpServers` |
52
54
  | `copilot` | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds `type` and `tools: ["*"]` automatically |
53
55
  | `gemini` | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Uses Gemini CLI `mcpServers` format |
@@ -49,11 +49,12 @@ The canonical data lives in **`scripts/ecosystem_tools.json`** and is consumed b
49
49
  | ID | `claude-app` |
50
50
  | Docs | https://support.claude.com/en/articles/13345190-get-started-with-claude-cowork |
51
51
  | Plugin docs | https://support.claude.com/en/articles/13837440-use-plugins-in-claude |
52
- | Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`, and `Customize > Plugins` |
52
+ | Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`, `Customize > Plugins`, plus the one file-based surface: `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%/Claude/`, Linux `~/.config/Claude/`; `CLAUDE_USER_DATA_DIR` overrides the root) |
53
53
  | Plugin layout | `.claude-plugin/plugin.json`, `skills/*/SKILL.md`, `agents/*.md`, `hooks/hooks.json`; ai-toolkit uses manifest paths under `claude-app/` for its generated app-only rules and hooks |
54
- | Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`) |
54
+ | Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`); `scripts/mcp_editors.py` for the `claude-app` MCP adapter |
55
55
  | Runtime split | Skills work in Chat (web/Desktop) and Cowork. Hooks and sub-agents run only in Cowork. Claude app does **not** scan Claude Code's `~/.claude/rules/`, `CLAUDE.md`, or `~/.claude/settings.json`. |
56
- | Install/update | Export the ZIP, upload it from `Customize > Plugins`, then paste the generated global-instructions file into `Settings > Cowork > Global instructions`. Re-export/re-upload after toolkit updates. |
56
+ | MCP | `ai-toolkit mcp install --editor claude-app --scope global <template>` writes `claude_desktop_config.json` directly -- no plugin involved. Entries are validated by the app as `{command, args, env}`; remote endpoints belong to the separate UI-managed `remoteMcpServers` surface, so HTTP/SSE templates are bridged through `mcp-remote`. Restart the app to load a change. |
57
+ | Install/update | Export the ZIP, upload it from `Customize > Plugins`, then paste the generated global-instructions file into `Settings > Cowork > Global instructions`. Re-export/re-upload after toolkit updates. MCP config is the exception -- it updates from the CLI like any other editor. |
57
58
 
58
59
  ---
59
60
 
@@ -3,10 +3,12 @@ title: "Troubleshooting"
3
3
  service: ai-toolkit
4
4
  category: troubleshooting
5
5
  tags: [troubleshooting, debugging]
6
- last_updated: "2026-03-25"
7
- description: "Section index for ai-toolkit troubleshooting. No documents are filed here yet; new ones follow the troubleshooting template."
6
+ last_updated: "2026-08-21"
7
+ description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
8
8
  ---
9
9
 
10
10
  # Troubleshooting
11
11
 
12
- 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
 
@@ -11729,8 +11730,11 @@ active `CODEX_HOME` (default `~/.codex`) and shared user skills use
11729
11730
  Codex user surface.
11730
11731
 
11731
11732
  Claude Chat/Desktop/Cowork is a separate runtime. It does not read the
11732
- filesystem surfaces under `~/.claude`; it receives ai-toolkit through an
11733
- uploaded plugin plus app-managed global/folder instructions.
11733
+ filesystem surfaces under `~/.claude`; it receives ai-toolkit skills, agents,
11734
+ hooks, and rules through an uploaded plugin plus app-managed global/folder
11735
+ instructions. Its one file-based surface is `claude_desktop_config.json`, which
11736
+ the app parses for local MCP servers -- `ai-toolkit mcp install --editor
11737
+ claude-app --scope global` targets it directly, without a plugin.
11734
11738
 
11735
11739
  ## Command Responsibilities
11736
11740
 
@@ -13531,9 +13535,9 @@ title: "AI Toolkit - MCP Editor Compatibility"
13531
13535
  category: reference
13532
13536
  service: ai-toolkit
13533
13537
  tags: [mcp, editors, compatibility, codex, cursor, antigravity]
13534
- version: "1.3.1"
13538
+ version: "1.4.0"
13535
13539
  created: "2026-04-12"
13536
- last_updated: "2026-08-19"
13540
+ last_updated: "2026-08-21"
13537
13541
  description: "Official MCP support matrix and native config targets for editors supported by ai-toolkit."
13538
13542
  ---
13539
13543
 
@@ -13547,7 +13551,8 @@ ai-toolkit keeps `.mcp.json` as the project-level canonical template format and
13547
13551
 
13548
13552
  | Editor | Scope | Native Config Path | Adapter Behavior |
13549
13553
  |--------|-------|--------------------|------------------|
13550
- | Claude Code | project + global | `.claude/settings.local.json`, `~/.claude/settings.json` | Merges `mcpServers` while preserving other settings keys |
13554
+ | Claude Code | project + global | `.mcp.json`, `~/.claude.json` | Merges `mcpServers` while preserving other top-level keys |
13555
+ | Claude Chat / Cowork | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux); `CLAUDE_USER_DATA_DIR` overrides the root | Emits the app's stdio-only `{command, args, env}` schema and bridges HTTP/SSE servers through `mcp-remote`; preserves `preferences`, `coworkUserFilesPath`, and user-owned servers |
13551
13556
  | Cursor | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors `mcpServers` directly |
13552
13557
  | GitHub Copilot | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds Copilot-required `type` and `tools` fields |
13553
13558
  | Gemini CLI | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Merges `mcpServers` into settings JSON |
@@ -13574,9 +13579,32 @@ ai-toolkit mcp install --editor cursor --scope project github --target .
13574
13579
  ai-toolkit mcp install --editor antigravity --scope project context7 --target .
13575
13580
  ai-toolkit mcp install --editor codex --scope project context7 --target .
13576
13581
  ai-toolkit mcp install --editor codex context7
13582
+ ai-toolkit mcp install --editor claude-app --scope global context7
13577
13583
  ai-toolkit mcp remove github --editor cursor --scope project --target .
13578
13584
  ```
13579
13585
 
13586
+ ## Claude Chat / Cowork
13587
+
13588
+ The Claude app is the one target whose MCP surface is file-based while everything
13589
+ else about it is not. Skills, agents, hooks, and rules reach Chat/Cowork only
13590
+ through the uploaded plugin ZIP (`ai-toolkit claude-app export`), because the app
13591
+ scans no filesystem location for them. Local MCP servers are different: the app
13592
+ parses `claude_desktop_config.json` on startup and reports invalid entries in its
13593
+ own warning dialog.
13594
+
13595
+ Two constraints shape the adapter:
13596
+
13597
+ - **stdio only.** Each entry is validated as `{command, args, env}`. Remote
13598
+ endpoints live in a separate `remoteMcpServers` surface that the app manages
13599
+ through its Connectors UI and does not read from this file. Portable HTTP/SSE
13600
+ templates are therefore wrapped in `mcp-remote`, which negotiates the transport
13601
+ itself, so one bridge shape covers both spellings.
13602
+ - **The file is not ours.** It also holds `preferences` and `coworkUserFilesPath`,
13603
+ which the app rewrites on its own. The adapter merges only `mcpServers` and
13604
+ leaves every other key untouched.
13605
+
13606
+ The app must be restarted for a config change to take effect.
13607
+
13580
13608
  ## Install Flow Integration
13581
13609
 
13582
13610
  When `.mcp.json` exists in a project, `ai-toolkit install --local` mirrors its servers into:
@@ -13620,9 +13648,9 @@ title: "MCP Server Templates"
13620
13648
  category: reference
13621
13649
  service: ai-toolkit
13622
13650
  tags: [mcp, templates, servers, configuration, editors, inject-mcp, external-templates]
13623
- version: "1.4.1"
13651
+ version: "1.5.0"
13624
13652
  created: "2026-04-07"
13625
- last_updated: "2026-08-19"
13653
+ last_updated: "2026-08-21"
13626
13654
  description: "Reference for 26 built-in MCP server templates, external template injection via inject-mcp, and native editor MCP installation support."
13627
13655
  ---
13628
13656
 
@@ -13656,6 +13684,7 @@ The `add` command merges the `mcpServers` block from the template into `.mcp.jso
13656
13684
  The `install` command renders the same canonical template into an editor-native config format:
13657
13685
  - JSON clients with `mcpServers` blocks: Claude Code, Cursor, Gemini CLI, Google Antigravity, Roo Code, Windsurf, Cline, Augment
13658
13686
  - JSON clients with additional transport metadata: GitHub Copilot
13687
+ - JSON clients restricted to stdio: Claude Chat/Cowork (`claude-app`) -- remote servers are bridged through `mcp-remote`
13659
13688
  - TOML clients: Codex CLI (`[mcp_servers.<name>]`)
13660
13689
 
13661
13690
  When `install` runs with `--scope project`, ai-toolkit also updates the project's `.mcp.json` so it remains the source of truth for later syncs.
@@ -13664,7 +13693,8 @@ When `install` runs with `--scope project`, ai-toolkit also updates the project'
13664
13693
 
13665
13694
  | Editor | Scope | Native Config Path | Notes |
13666
13695
  |--------|-------|--------------------|-------|
13667
- | `claude` | project + global | `.claude/settings.local.json`, `~/.claude/settings.json` | Preserves existing hooks and env keys |
13696
+ | `claude` | project + global | `.mcp.json`, `~/.claude.json` | Preserves unrelated top-level keys |
13697
+ | `claude-app` | global | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux) | Claude Chat/Cowork; stdio-only schema, bridges HTTP/SSE through `mcp-remote`, preserves `preferences` and `coworkUserFilesPath`; `CLAUDE_USER_DATA_DIR` overrides the root |
13668
13698
  | `cursor` | project + global | `.cursor/mcp.json`, `~/.cursor/mcp.json` | Mirrors canonical `mcpServers` |
13669
13699
  | `copilot` | project + global | `.github/mcp.json`, `$COPILOT_HOME/mcp-config.json` (default `~/.copilot/mcp-config.json`) | Adds `type` and `tools: ["*"]` automatically |
13670
13700
  | `gemini` | project + global | `.gemini/settings.json`, `~/.gemini/settings.json` | Uses Gemini CLI `mcpServers` format |
@@ -15490,11 +15520,12 @@ The canonical data lives in **`scripts/ecosystem_tools.json`** and is consumed b
15490
15520
  | ID | `claude-app` |
15491
15521
  | Docs | https://support.claude.com/en/articles/13345190-get-started-with-claude-cowork |
15492
15522
  | Plugin docs | https://support.claude.com/en/articles/13837440-use-plugins-in-claude |
15493
- | Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`, and `Customize > Plugins` |
15523
+ | Config surfaces | `Settings > Cowork > Global instructions`, Cowork folder instructions, `Customize > Skills`, `Customize > Plugins`, plus the one file-based surface: `claude_desktop_config.json` (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%/Claude/`, Linux `~/.config/Claude/`; `CLAUDE_USER_DATA_DIR` overrides the root) |
15494
15524
  | Plugin layout | `.claude-plugin/plugin.json`, `skills/*/SKILL.md`, `agents/*.md`, `hooks/hooks.json`; ai-toolkit uses manifest paths under `claude-app/` for its generated app-only rules and hooks |
15495
- | Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`) |
15525
+ | Our generator | `scripts/claude_app.py` (`ai-toolkit claude-app export`); `scripts/mcp_editors.py` for the `claude-app` MCP adapter |
15496
15526
  | Runtime split | Skills work in Chat (web/Desktop) and Cowork. Hooks and sub-agents run only in Cowork. Claude app does **not** scan Claude Code's `~/.claude/rules/`, `CLAUDE.md`, or `~/.claude/settings.json`. |
15497
- | Install/update | Export the ZIP, upload it from `Customize > Plugins`, then paste the generated global-instructions file into `Settings > Cowork > Global instructions`. Re-export/re-upload after toolkit updates. |
15527
+ | MCP | `ai-toolkit mcp install --editor claude-app --scope global <template>` writes `claude_desktop_config.json` directly -- no plugin involved. Entries are validated by the app as `{command, args, env}`; remote endpoints belong to the separate UI-managed `remoteMcpServers` surface, so HTTP/SSE templates are bridged through `mcp-remote`. Restart the app to load a change. |
15528
+ | Install/update | Export the ZIP, upload it from `Customize > Plugins`, then paste the generated global-instructions file into `Settings > Cowork > Global instructions`. Re-export/re-upload after toolkit updates. MCP config is the exception -- it updates from the CLI like any other editor. |
15498
15529
 
15499
15530
  ---
15500
15531
 
@@ -16130,13 +16161,117 @@ title: "Troubleshooting"
16130
16161
  service: ai-toolkit
16131
16162
  category: troubleshooting
16132
16163
  tags: [troubleshooting, debugging]
16133
- last_updated: "2026-03-25"
16134
- description: "Section index for ai-toolkit troubleshooting. No documents are filed here yet; new ones follow the troubleshooting template."
16164
+ last_updated: "2026-08-21"
16165
+ description: "Section index for ai-toolkit troubleshooting, covering the Claude app plugin double-load collision."
16135
16166
  ---
16136
16167
 
16137
16168
  # Troubleshooting
16138
16169
 
16139
- Problem resolution guides. Guides will be added here as they are created.
16170
+ Problem resolution guides.
16171
+
16172
+ - [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.
16173
+
16174
+ ---
16175
+
16176
+ ## kb/troubleshooting/plugin-double-load.md
16177
+
16178
+ ---
16179
+ title: "Claude App Plugin Loads Twice in Claude Code"
16180
+ category: troubleshooting
16181
+ service: ai-toolkit
16182
+ tags: [plugin, claude-app, hooks, duplication, doctor]
16183
+ created: "2026-08-21"
16184
+ last_updated: "2026-08-21"
16185
+ 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."
16186
+ ---
16187
+
16188
+ # Claude App Plugin Loads Twice in Claude Code
16189
+
16190
+ ## Symptom
16191
+
16192
+ After uploading `ai-toolkit-claude-app.zip` through `Customize > Plugins`, Claude
16193
+ Code sessions get slower and every hook side effect appears twice: duplicate rows
16194
+ in `governance.log`, duplicate session state writes, two Stop gates per turn.
16195
+ `ai-toolkit doctor` reports a healthy install because it only inspected
16196
+ `~/.claude` and `app/plugins`.
16197
+
16198
+ ## Cause
16199
+
16200
+ The Claude app writes uploaded plugins into
16201
+ `~/.claude/plugins/marketplaces/local-desktop-app-uploads/` and sets
16202
+ `enabledPlugins` in `~/.claude/settings.json`. Both paths belong to Claude Code
16203
+ as well, so Claude Code loads the plugin on top of the global install. The
16204
+ bundle carries the same catalog as `~/.claude`, and plugin hooks merge with user
16205
+ hooks without deduplication.
16206
+
16207
+ Confirmed in a Claude Code debug log (`claude --debug -p ...`, then read
16208
+ `~/.claude/debug/latest`):
16209
+
16210
+ ```
16211
+ Loaded 111 unique skills (... user: 111 ...)
16212
+ Total plugin skills loaded: 111 (0 duplicate/user-owned entries skipped)
16213
+ Total plugin agents loaded: 44
16214
+ Read manifest hooks for plugin ai-toolkit (enabled=true): ./claude-app/hooks/hooks.json
16215
+ ```
16216
+
16217
+ The `skills` manifest field adds to the default `skills/` directory instead of
16218
+ replacing it, so the plugin contributes its 109 catalog skills plus the 2
16219
+ app-only rule skills.
16220
+
16221
+ ## Diagnosis
16222
+
16223
+ ```bash
16224
+ ai-toolkit doctor
16225
+ ```
16226
+
16227
+ Check 11 reports the collision:
16228
+
16229
+ ```
16230
+ ## 11. Plugin Double-Load
16231
+ 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)
16232
+ ```
16233
+
16234
+ To see the duplication directly, count hook invocations per source in a fresh
16235
+ session log:
16236
+
16237
+ ```bash
16238
+ grep -oE "[^\"' ]*hooks/[a-z0-9._-]+\.sh" ~/.claude/debug/latest | sort | uniq -c
16239
+ ```
16240
+
16241
+ ## Fix
16242
+
16243
+ ```bash
16244
+ ai-toolkit doctor --fix
16245
+ ```
16246
+
16247
+ That sets the plugin to `false` in `enabledPlugins` and leaves the global
16248
+ install authoritative. Claude Code then logs
16249
+ `enabled=false; will NOT register, plugin is disabled`, and plugin skills and
16250
+ agents drop to 0.
16251
+
16252
+ The global install is the richer surface for Claude Code: it delivers rules as
16253
+ real files under `~/.claude/rules/`, which are always in context, while the
16254
+ plugin exposes them as an `ai-toolkit-rules` skill the model has to load. The
16255
+ plugin also has no `session-context.sh` hook.
16256
+
16257
+ Keep the plugin enabled only when Claude Code has no global install, for example
16258
+ a machine that runs the Claude app alone. In that case the plugin is the single
16259
+ source and check 11 stays quiet.
16260
+
16261
+ ## Verification
16262
+
16263
+ ```bash
16264
+ claude --debug -p "ok"
16265
+ grep -E "Total plugin (skills|agents) loaded|enabled=" ~/.claude/debug/latest
16266
+ ```
16267
+
16268
+ Expect `Total plugin skills loaded: 0`, `Total plugin agents loaded: 0`, and the
16269
+ `plugin is disabled` line.
16270
+
16271
+ ## Related
16272
+
16273
+ - `kb/reference/global-install-model.md`
16274
+ - `kb/procedures/maintenance-sop.md`
16140
16275
 
16141
16276
  ---
16142
16277
 
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.26.0",
2
+ "version": "4.28.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.26.0",
3
+ "version": "4.28.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",
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("========================")
@@ -110,18 +110,23 @@
110
110
  "UI: Customize > Plugins",
111
111
  ".claude-plugin/plugin.json",
112
112
  "claude-app/hooks/hooks.json",
113
- "claude-app/skills/*/SKILL.md"
113
+ "claude-app/skills/*/SKILL.md",
114
+ "~/Library/Application Support/Claude/claude_desktop_config.json",
115
+ "%APPDATA%/Claude/claude_desktop_config.json",
116
+ "~/.config/Claude/claude_desktop_config.json"
114
117
  ],
115
118
  "our_generators": [
116
- "scripts/claude_app.py"
119
+ "scripts/claude_app.py",
120
+ "scripts/mcp_editors.py"
117
121
  ],
118
- "status_note": "Claude Chat/Desktop/Cowork does not scan ~/.claude/rules, CLAUDE.md, or Claude Code's user settings. ai-toolkit exports an uploadable plugin ZIP plus Cowork global instructions. Skills work in Chat and Cowork; plugin hooks and sub-agents run only in Cowork.",
122
+ "status_note": "Claude Chat/Desktop/Cowork does not scan ~/.claude/rules, CLAUDE.md, or Claude Code's user settings, so skills, agents, hooks, and rules ship only as an uploadable plugin ZIP plus Cowork global instructions. Local MCP servers are the exception: the app reads claude_desktop_config.json directly, so `ai-toolkit mcp install --editor claude-app --scope global` configures them without a plugin. That file accepts stdio entries only; HTTP/SSE templates are bridged through mcp-remote. Skills work in Chat and Cowork; plugin hooks and sub-agents run only in Cowork.",
119
123
  "capability_markers": [
120
124
  "Global instructions",
121
125
  "Folder instructions",
122
126
  "plugins",
123
127
  "skills",
124
- "sub-agents"
128
+ "sub-agents",
129
+ "MCP"
125
130
  ],
126
131
  "version_probe": null
127
132
  },
@@ -13,6 +13,7 @@ import json
13
13
  import math
14
14
  import os
15
15
  import re
16
+ import sys
16
17
  import tempfile
17
18
  from dataclasses import dataclass
18
19
  from pathlib import Path
@@ -23,6 +24,18 @@ except ModuleNotFoundError: # pragma: no cover - Python 3.11+ should have tomll
23
24
  tomllib = None
24
25
 
25
26
 
27
+ def _claude_app_config_relpath() -> str:
28
+ """Home-relative Claude app MCP config path for the running platform."""
29
+ if sys.platform == "darwin":
30
+ return "Library/Application Support/Claude/claude_desktop_config.json"
31
+ if os.name == "nt":
32
+ return "AppData/Roaming/Claude/claude_desktop_config.json"
33
+ return ".config/Claude/claude_desktop_config.json"
34
+
35
+
36
+ CLAUDE_APP_CONFIG_RELPATH = _claude_app_config_relpath()
37
+
38
+
26
39
  EDITOR_SPECS: dict[str, dict[str, str | None]] = {
27
40
  "claude": {
28
41
  "label": "Claude Code",
@@ -94,6 +107,13 @@ EDITOR_SPECS: dict[str, dict[str, str | None]] = {
94
107
  "format": "toml",
95
108
  "doc_scope": "project + global",
96
109
  },
110
+ "claude-app": {
111
+ "label": "Claude Chat / Cowork",
112
+ "project_path": None,
113
+ "global_path": CLAUDE_APP_CONFIG_RELPATH,
114
+ "format": "json",
115
+ "doc_scope": "global",
116
+ },
97
117
  }
98
118
 
99
119
 
@@ -202,6 +222,11 @@ def _resolve_global_config_root(
202
222
  if not configured:
203
223
  return (Path.home() / default_dir).absolute()
204
224
 
225
+ return _validate_configured_config_root(env_name, configured)
226
+
227
+
228
+ def _validate_configured_config_root(env_name: str, configured: str) -> Path:
229
+ """Validate an operator-supplied config root without resolving symlinks."""
205
230
  config_root = Path(configured).expanduser()
206
231
  if not config_root.is_absolute():
207
232
  raise ValueError(f"Configured {env_name} must be absolute: {configured}")
@@ -219,6 +244,30 @@ def _resolve_global_config_root(
219
244
  return config_root
220
245
 
221
246
 
247
+ def _resolve_claude_app_config_path(*, home: Path | None) -> Path:
248
+ """Resolve ``claude_desktop_config.json`` for the Claude app.
249
+
250
+ The Claude app honors ``CLAUDE_USER_DATA_DIR`` for its user-data root, so we
251
+ honor it too. Unlike ``COPILOT_HOME``/``CODEX_HOME`` the default directory is
252
+ platform-specific and need not exist yet -- ``_atomic_write_bytes`` creates it
253
+ -- so only an explicitly configured root is validated.
254
+ """
255
+ if home is not None:
256
+ return (home / CLAUDE_APP_CONFIG_RELPATH).expanduser().absolute()
257
+
258
+ configured = os.environ.get("CLAUDE_USER_DATA_DIR", "").strip()
259
+ if configured:
260
+ root = _validate_configured_config_root("CLAUDE_USER_DATA_DIR", configured)
261
+ return root / "claude_desktop_config.json"
262
+
263
+ if os.name == "nt": # pragma: no cover - exercised on Windows only
264
+ appdata = os.environ.get("APPDATA", "").strip()
265
+ if appdata:
266
+ return (Path(appdata) / "Claude" / "claude_desktop_config.json").absolute()
267
+
268
+ return (Path.home() / CLAUDE_APP_CONFIG_RELPATH).absolute()
269
+
270
+
222
271
  def resolve_editor_path(
223
272
  editor: str,
224
273
  scope: str,
@@ -256,6 +305,8 @@ def resolve_editor_path(
256
305
  default_dir=".codex",
257
306
  )
258
307
  return codex_home / "config.toml"
308
+ if editor == "claude-app":
309
+ return _resolve_claude_app_config_path(home=home)
259
310
  return (home or Path.home()) / str(rel)
260
311
  raise ValueError(f"Unsupported scope: {scope}")
261
312
 
@@ -509,6 +560,8 @@ def _rollback_config_update(update: ConfigUpdate) -> None:
509
560
  def _normalize_server(editor: str, server: dict) -> dict:
510
561
  if editor == "antigravity":
511
562
  return _normalize_antigravity_server(server)
563
+ if editor == "claude-app":
564
+ return _normalize_claude_app_server(server)
512
565
  data = copy.deepcopy(server)
513
566
  if editor == "copilot":
514
567
  if "url" in data:
@@ -519,6 +572,65 @@ def _normalize_server(editor: str, server: dict) -> dict:
519
572
  return data
520
573
 
521
574
 
575
+ MCP_REMOTE_PACKAGE = "mcp-remote"
576
+
577
+
578
+ def _normalize_claude_app_server(server: dict) -> dict:
579
+ """Emit the Claude app's stdio-only MCP schema, bridging remote endpoints.
580
+
581
+ ``claude_desktop_config.json`` validates each entry as ``{command, args,
582
+ env}``; remote servers live in a separate app-managed ``remoteMcpServers``
583
+ surface that is not file-configurable. Portable HTTP/SSE templates are
584
+ therefore wrapped in ``mcp-remote``, which negotiates the transport itself,
585
+ so one bridge shape covers both the ``http`` and ``sse`` spellings.
586
+ """
587
+ if not isinstance(server, dict):
588
+ raise ValueError("Claude app MCP server configuration must be an object")
589
+
590
+ data = copy.deepcopy(server)
591
+ for key in ("_source", "transport", "type", "tools"):
592
+ data.pop(key, None)
593
+
594
+ url = data.pop("url", None)
595
+ server_url = data.pop("serverUrl", None)
596
+ if url is not None and server_url is not None:
597
+ raise ValueError(
598
+ "Claude app MCP server requires exactly one of 'url' or 'serverUrl'"
599
+ )
600
+ url = url if url is not None else server_url
601
+
602
+ if "command" in data and url is not None:
603
+ raise ValueError(
604
+ "Claude app MCP server requires either 'command' or a URL, not both"
605
+ )
606
+
607
+ if url is not None:
608
+ if not isinstance(url, str) or not url.strip():
609
+ raise ValueError("Claude app MCP server URL must be a non-empty string")
610
+ headers = data.pop("headers", None)
611
+ args = ["-y", MCP_REMOTE_PACKAGE, url.strip()]
612
+ if headers is not None:
613
+ if not isinstance(headers, dict):
614
+ raise ValueError("Claude app MCP 'headers' must be an object")
615
+ for name, value in headers.items():
616
+ if not isinstance(name, str) or not isinstance(value, str):
617
+ raise ValueError(
618
+ "Claude app MCP 'headers' must map strings to strings"
619
+ )
620
+ args.extend(["--header", f"{name}: {value}"])
621
+ bridged: dict = {"command": "npx", "args": args}
622
+ if "env" in data:
623
+ bridged["env"] = data["env"]
624
+ return bridged
625
+
626
+ command = data.get("command")
627
+ if not isinstance(command, str) or not command.strip():
628
+ raise ValueError(
629
+ "Claude app MCP server requires a non-empty 'command' or a URL"
630
+ )
631
+ return {key: data[key] for key in ("command", "args", "env") if key in data}
632
+
633
+
522
634
  def _normalize_antigravity_server(server: dict) -> dict:
523
635
  """Validate and emit Antigravity's current ``serverUrl`` MCP schema."""
524
636
  if not isinstance(server, dict):