Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 197 additions & 43 deletions .dev-loop/INGEST_REPORT.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions log.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ Append-only. Format: `## [YYYY-MM-DD] <ingest|revise|lint|gap|contradiction|drif
## [2026-08-06] revise | Union-merged amendments from the same 15 PRs into 24 existing pages (largest: tests-that-cannot-fail — 7 PRs: per-assertion mutation granularity, restore-mechanism-by-commit-state, suite-total-drop detection; portable-shell-scripts — 8 PRs: POSIX inline set-- reordering, ${VAR:-} vs ${VAR-} colon semantics, quote-by-what-the-text-is; test-data-and-isolation — 6 PRs: env-derived write paths, absent-variable cases, exec-bit fixtures, leak attribution; change-impact/call-site-enumeration — 8 unique additions from 7 path-variants: rename-in-place, same-type reorder, *args forwarding, two-searches-agree fallacy, codemod, coverage completion check). All non-canonical cross-page ids remapped to canonical pages; every added line traced to its source PR hunk (fabricated worker output was detected by orphan-line verification and replaced with true PR content).
## [2026-08-06] dedup | Cross-PR duplicate clusters collapsed: client-side rate limiting (8 PR versions at 3 paths → 1), call-site enumeration (7 versions at 7 paths → additions folded into the already-merged canonical page), stderr/exit-0 diagnostics (4 versions at 4 paths → 1), macOS sysroot (2→1), env-var off switch (2→1), completion predicates (2→1), robots.txt (2→1), harness-mediated results (2→1), leaked test artifacts (2→1), orchestration category naming (orchestration vs agent-orchestration → agent-orchestration). Kept distinct after trigger comparison: differential-testing vs differential-run-agreement (setup vs interpretation), unset-versus-empty-parameters vs env-var-off-switches (semantics vs design, cross-linked), import-time-side-effects vs test-level-choice edge (tactics vs level choice, cross-linked). Merged-main near-dup scan (Jaccard over title+trigger, 141 pages): 0 pairs — no duplication among previously merged content.
## [2026-08-06] ingest | Reconciled post-consolidation flush PRs #42–#43 (created against pre-#44 main). Folded unique content: platforms/filesystems/permissions-and-exec-bits (+`sh "$SCRIPT"` interpreter-invocation rows — stubs need no exec bit, avoids EDR chmod+x heuristics), platforms/shells/command-text-inspected-before-execution (+gate-AUTHOR step 8: parse all three POSIX quoting forms, expand only ~/$HOME/${HOME}; red-then-green bats evidence), testing/quality/checks-that-cannot-pass (+vacuously-green pre-implementation test row), platforms/processes/tool-diagnostics-without-a-failing-exit-code (+repeat-suppression edge, +clang reproduction source), infrastructure/ci-cd/secrets-handling (+independent credential channels row: gh token vs git SSH vs API token, git push --dry-run / author:@me probes), testing/mocking/what-to-mock (+ESM read-only-bindings DI rows, +PATH-emptied tripwire negative-proof). Dropped as duplicates of the #44 state: #42's rate-limit row (already in timeouts-and-retries + client-side-rate-limiting), #42's warning-only-diagnostics page (same trigger as tool-diagnostics-without-a-failing-exit-code), #42's leak-attribution row (already in test-data-and-isolation).
## [2026-08-12] ingest | knowledge-flush of 4 queued insights → 2 new pages, 1 merge, 1 candidate corrected. New: platforms/tools/plugin-mcp-server-registration (a plugin-bundled MCP server absent from `/mcp` is a registration fault first — `/reload-plugins` before any config edit, then `claude mcp list`/`--debug`, then a manual `initialize` run; both `.mcp.json` shapes load, so a shape rewrite that "fixes" it was really the reload; an unset `${VAR}` with no default is delivered to the server as literal text with a warning only). New: security/authn/retiring-a-replaced-auth-gate (after an auth cutover, census the retired session key's readers and writers — a key with readers and zero writers is the missed route; its "empty config → allow" fallback passes locally and refuses everyone in production, so parameterize the regression test on the setting that decides the fallback; deny default per ASVS 4.1.5). Merged: infrastructure/agent-orchestration/pane-delivery-confirmation +1 Instead-of (a send wrapper's success word means the keys reached the pane, not that the prompt was submitted — confirm from the pane) +2 edge cases (collapsed paste placeholder → send Enter as its own key event; a queued-then-picked-up wait pair is a confirmation) + the 2026-08-12 three-worker field observation. Corrected before ingest: the queued claim that a plugin stdio server inherits arbitrary exported shell variables so `env` can be dropped was not substantiated — the MCP reference stdio client passes only a fixed allowlist when `env` is absent, and the session's evidence measured a manual shell run rather than the harness spawn; the page keeps the `env` entry with a `:-` default instead. Sources live-verified: code.claude.com plugins-reference + mcp, modelcontextprotocol/typescript-sdk#216, OWASP ASVS V4.1.5, CWE-561, CWE-1188.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ confidence: verified
sources:
- https://man7.org/linux/man-pages/man3/termios.3.html
- https://man7.org/linux/man-pages/man1/tmux.1.html
last_verified: 2026-08-05
last_verified: 2026-08-12
related: [platforms-shells-option-like-argument-values, infrastructure-agent-orchestration-session-completion-gates, platforms-processes-non-interactive-cli-invocation]
---

Expand Down Expand Up @@ -60,17 +60,21 @@ or escalate.
| The send is a multi-line prompt | Send the body and the submit key as separate calls and check the indicator between them; a single blob can be consumed partially |
| Several sends are in flight to one pane | Serialize them — one outstanding send per pane, confirmed before the next; interleaved input is reordered by the tty buffer, not by your script |
| No busy indicator exists in the target | Require the artifact check from the table; without either, the harness cannot distinguish queued from consumed |
| The pane shows the prompt collapsed into a paste placeholder (`❯ [Pasted text #3]`) with no busy marker | The body arrived as one bracketed-paste block and the submit key was consumed with it — send `Enter` as its own `send-keys` call and re-read; [platforms-processes-non-interactive-cli-invocation] owns the paste mechanism |
| A send helper reports a queued outcome and its own follow-up wait then reports pick-up | That pair is a confirmation: the wait observed the target take the input. A helper that reports delivery without a wait has observed only the write |

## Instead of

| If you are about to | Do this instead | Why |
|---------------------|-----------------|-----|
| Diff `capture-pane` before/after and call a difference "delivered" | Check the busy/queued indicator first and use the diff only when it is absent | The tty echoes typed characters while the program is busy, so the diff reports success for the queued case the check exists to catch |
| Sleep a fixed interval after `send-keys` and continue | Poll the indicator (or the artifact) until it clears, with a deadline | The right interval is the target's work time, which is what you are trying to measure |
| Read a send wrapper's success word or exit 0 as "the prompt is running" | Read it as "the keys reached the pane", then confirm submission from the pane: an empty input line plus the target's working indicator | The wrapper checks its own write, which succeeds whether the target submitted the input or parked it as an unsubmitted paste; the gap surfaces only as a phase timeout much later |
| Resend on the first unchanged capture | Distinguish "busy" from "not delivered" before resending | Resending into a busy pane queues a duplicate that runs when the pane drains |

## Sources

- https://man7.org/linux/man-pages/man3/termios.3.html — `ECHO` in `c_lflag`: "Echo input characters." The terminal driver echoes independently of when the program calls `read()`
- https://man7.org/linux/man-pages/man1/tmux.1.html — `send-keys` writes keys into a pane's input; `capture-pane` copies the pane's visible contents — neither reports whether the foreground process consumed the input
- Field observation 2026-08-12 (dev-loop orchestrate, 3 tmux worker sessions): `send-prompt.sh` returned 0/"delivered" for two workers whose panes both sat at `❯ [Pasted text #3]`/`#4` with the prompt unsubmitted, while the third returned "queued" and its follow-up `wait` reported pick-up — that one had actually submitted. Sending `Enter` as a separate key event to each stuck pane started both workers immediately
- Field reproduction 2026-08-05 (tmux 3.7b, macOS): a pane running `sleep 6` received `echo SECOND_PROMPT_MARKER`. Pane content changed (diff = YES) and the marker appeared once as echoed text, while the command's own output line count stayed 0; after the sleep drained, the command ran and the output line appeared
1 change: 1 addition & 0 deletions wiki/platforms/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Match your situation to a "load when" line; load only matching pages.
|------|-----------|
| [bsd-vs-gnu-cli](tools/bsd-vs-gnu-cli.md) | A command works on Linux but fails on macOS or vice versa (`date`, `sed -i`, `timeout`, `seq`, `grep -P`, `readlink`, `stat`); writing a script or CI step that must run on both userlands; deciding whether to install GNU coreutils on macOS or write POSIX-only |
| [harness-mediated-tool-results](tools/harness-mediated-tool-results.md) | A plugin or hook in your agent harness returned substitute content for a built-in tool (truncated read, redaction, a note telling you to call something else); deciding whether a short result is interception or a genuinely small/empty file; the hook's suggested workaround failed too; briefing spawned worker sessions about a known-degraded tool |
| [plugin-mcp-server-registration](tools/plugin-mcp-server-registration.md) | A plugin-bundled MCP server is missing from `/mcp` or its tools are uncallable while the plugin's skills work; deciding whether the fault is registration, config shape, or the server itself; about to change a plugin `.mcp.json` (bare map vs `mcpServers` wrapper, `command`/`args`/`env`) to make a server appear; relaying a shell variable into a server with `${VAR}` and the server reads the wrong value; a plugin updated mid-session and its server still runs the old path |
| [version-keyed-artifact-cache](tools/version-keyed-artifact-cache.md) | Shipping a code update to a distribution system that caches artifacts by a version string (a Claude Code marketplace plugin, or any tag-pinned cache) and the update runs but the old behavior persists; deciding why `/plugin update` reports "at latest" yet new code never runs; locating and clearing a stale `~/.claude/plugins/cache/<mkt>/<plugin>/<version>/` |

## environment
Expand Down
74 changes: 74 additions & 0 deletions wiki/platforms/tools/plugin-mcp-server-registration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
id: platforms-tools-plugin-mcp-server-registration
domain: platforms
category: tools
applies_to: [claude-code]
confidence: verified
sources:
- https://code.claude.com/docs/en/plugins-reference
- https://code.claude.com/docs/en/mcp
- https://github.com/modelcontextprotocol/typescript-sdk/issues/216
last_verified: 2026-08-12
related: [platforms-tools-version-keyed-artifact-cache, platforms-tools-harness-mediated-tool-results, infrastructure-config-environment-config]
---

# A Plugin-Bundled MCP Server That Does Not Appear in the Harness

## When this applies

You installed or edited a Claude Code plugin that bundles an MCP server and the
server is absent from `/mcp`, or its tools are not callable — while the plugin's
skills and commands work. Also when you are about to change that plugin's
`.mcp.json` shape, `command`/`args`, or `env` to make it appear.

## Do this

1. **Reload the plugin layer before editing any config.** Run `/reload-plugins`,
then re-open `/mcp`. A plugin's MCP servers are connected at session startup
for plugins that were already enabled; a plugin enabled, installed, or updated
mid-session keeps the session's previous plugin state until you reload.
2. **When it is still absent after a reload, read the harness's own report before
changing the file**: `claude mcp list` names the per-server failure detail and
any missing-variable warning, and `claude --debug` prints server
initialization errors.
3. **Run the installed copy by hand as the third step**, from the same working
directory the session uses, and feed it one `initialize` request on stdin. A
valid `serverInfo` response separates "the server is broken" from "the harness
never started it".
4. **Compare against a working sibling's file, not against your memory of the
schema**: `find ~/.claude/plugins/cache -maxdepth 4 -name .mcp.json`.

Order the hypotheses by what each one explains:

| Observation | Conclusion |
|-------------|------------|
| Server absent from `/mcp`, plugin's skills present, session predates the install/edit | The session holds the pre-change plugin state — reload |
| Server listed in `/mcp` with a failure status | The spawn or handshake failed — take the detail from `claude mcp list` / `claude --debug` |
| Server absent after a reload and a manual run answers `initialize` correctly | The config is not being read as you think — compare shape and placeholder expansion against a working sibling |
| Tools present but named unexpectedly | Plugin server tools are `mcp__plugin_<plugin>_<server>__<tool>`; search by that form before concluding they are missing |

## Edge cases

| Case | Then |
|------|------|
| The file is a bare map of server names, with no `"mcpServers"` wrapper | Both shapes load. The documented form wraps entries in `"mcpServers"`, and installed plugins ship both — treat the shape as an unlikely cause and keep looking |
| You relay a value the user already exports, as `"env": {"VAR": "${VAR}"}` | Give it a default (`${VAR:-<value>}`) or assert on the value inside the server at startup: with no default and the variable unset, the config still loads and the server receives the literal text `${VAR}`, warning only in `claude mcp list` |
| You are tempted to drop `env` and let the child inherit the variable from your shell | Keep the entry (with a default) unless you have confirmed the inheritance for your harness version — the MCP reference stdio client passes only a fixed allowlist (`HOME`, `PATH`, `SHELL`, `TERM`, `USER`, `LOGNAME`) to the child when `env` is absent |
| The plugin was updated mid-session | Hooks, MCP servers, and LSP servers keep the previous version's `${CLAUDE_PLUGIN_ROOT}` path until `/reload-plugins`; monitors need a session restart ([platforms-tools-version-keyed-artifact-cache] owns the stale-cache case) |
| Only the plugin's slash commands are missing after a reload | The reload connects MCP servers but has not always rebuilt the command index — restart the session for commands specifically |
| The server writes logs to stdout | The harness reads stdout as protocol frames and disconnects the server, counting it as a crash — send logs to stderr |

## Instead of

| If you are about to | Do this instead | Why |
|---------------------|-----------------|-----|
| Rewrite `.mcp.json` into the other shape because the server is missing | Run `/reload-plugins` first, then `claude mcp list` | Both shapes are accepted, so a shape edit that "fixes" it was really the reload; the true cause survives to the next install |
| Conclude the server is broken because `/mcp` does not list it | Run the installed copy by hand and feed it one `initialize` | A server that answers `initialize` was never started by the harness — that is a registration fault, not a server fault |
| Add `env` entries for variables your shell already exports, to be safe | Add them with `:-` defaults, or read the value inside the server and fail loudly when it is absent | An unexpanded `${VAR}` is delivered as literal text rather than rejected, so the server starts with a wrong value instead of not starting |

## Sources

- https://code.claude.com/docs/en/plugins-reference — "Changes you make to a skill's `SKILL.md` take effect immediately in the current session. Changes to the plugin's other components, such as `hooks/`, `.mcp.json`, `agents/`, and `output-styles/`, do not. Run `/reload-plugins` or restart Claude Code to pick those up"; "When a plugin updates mid-session, hook commands, monitors, MCP servers, and LSP servers keep using the previous version's path. Run `/reload-plugins` to switch hooks, MCP servers, and LSP servers to the new path; monitors require a session restart"; troubleshooting for a server whose tools do not appear ("Check the MCP server logs: `claude --debug` shows initialization errors", "Test the server manually outside of Claude Code"); stdout is read as protocol messages and non-protocol output disconnects the server
- https://code.claude.com/docs/en/mcp — "At session startup, Claude Code connects the servers for enabled plugins automatically"; "If you enable or disable a plugin during a session, run `/reload-plugins` to connect or disconnect its MCP servers"; expansion of `${VAR}` / `${VAR:-default}` in `command`, `args`, `env`, `url`, `headers`; "If a referenced environment variable isn't set and has no default value, the config still loads: Claude Code reports a missing-variable warning for that server in `claude mcp list` output and uses the unexpanded `${VAR}` text as-is"; plugin tool naming `mcp__plugin_<plugin-name>_<server-name>__<tool-name>`; `claude mcp get <name>` shows an `Issue:` line for a failed server
- https://github.com/modelcontextprotocol/typescript-sdk/issues/216 — the reference stdio client's `getDefaultEnvironment()` returns only `DEFAULT_INHERITED_ENV_VARS` (`HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, `USER` on POSIX) and a supplied `env` replaces rather than extends it; an arbitrary exported variable is not inherited by default
- Field reproduction 2026-08-12 (`lnpl-mcp` 0.3.0, macOS): install, `enabledPlugins` registration, and a manual run of the installed server (correct `serverInfo`) were all healthy while `/mcp` did not list the server; `/reload-plugins` exposed its two tools immediately. Control: in the same cache, `context7` and `playwright` ship a bare map and `claude-mem`, `figma`, and `atlassian` ship the `"mcpServers"` wrapper, and servers of both shapes were live in that session
2 changes: 1 addition & 1 deletion wiki/platforms/tools/version-keyed-artifact-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ sources:
- https://github.com/anthropics/claude-code/issues/17361
- https://github.com/anthropics/claude-code/issues/61954
last_verified: 2026-08-04
related: [platforms-toolchains-version-management]
related: [platforms-toolchains-version-management, platforms-tools-plugin-mcp-server-registration]
---

# Shipping New Code Through a Version-Keyed Artifact Cache
Expand Down
Loading
Loading