Package: agent-bundle claude adapter, at main 4e546516. Surfaced during final hardening of the movie-library port (its commit af0ab2a), where a copied claude artifact wrote persistent state into whatever directory the host happened to launch from.
What differs across the three adapters
Normalization gives every source-built (entry:) stdio server a plugin-root working directory in the canonical model — packages/agent-bundle/src/config/normalize.ts emits command: 'node', args: ['mcp/<name>.mjs', …], cwd: pathTokens.pluginRoot. The three adapters then diverge on what reaches the emitted config:
- portable (
src/adapters/portable.ts L159, L178): expands the token and emits cwd: "${PLUGIN_ROOT}". Anchor present.
- codex (
src/adapters/codex.ts L218–L226, L256): plugin-root cwd becomes cwd: "./", and plugin-root-prefixed command/args/env values are rewritten ./-relative against it — the anchor is load-bearing there (values embedding the plugin-root token without it are hard diagnostics). Anchor present.
- claude (
src/adapters/claude.ts L153–L161): absolutizes args[0] to ${CLAUDE_PLUGIN_ROOT}/mcp/…, then drops cwd for any server with source set:
if (server.source !== undefined && server.cwd === pathTokens.pluginRoot && args?.[0] !== undefined) {
args[0] = `${hookContract.commandRoot}/${args[0]}`;
}
// …
...(server.cwd === undefined || server.source !== undefined ? {} : { cwd: expandClaudeToken(server.cwd) }),
So the bundled script itself resolves (absolute arg), but the spawned process's working directory is whatever the Claude host launched from — the plugin-root anchor that both sibling targets provide is simply absent. (Command-based servers with an explicit cwd do get the field emitted, so the drop is specific to source-built entries.)
Consumer consequence
Any plugin whose runtime resolves state or assets relative to its install root gets no anchor on a bare claude install. In the movie-library port, persistent stores (per-tracker policy ledger, Trakt device-flow auth) fragmented into <launch-cwd>/.runtime per launch directory — live credentials effectively lost between sessions. The port had to fix it consumer-side twice over (commit af0ab2a):
// agent-bundle.config.ts — every server ships an env anchor, expanded per target
const withStateRootEnv = Object.fromEntries(
Object.entries(base.mcp!.servers).map(([serverName, server]) => [
serverName,
{ ...server, env: { MOVIE_LIBRARY_ROOT: pathTokens.pluginRoot } },
]),
);
The claude adapter runs env values through its token expander, so this lands as env: { MOVIE_LIBRARY_ROOT: "${CLAUDE_PLUGIN_ROOT}" } and works. As a second layer, the state-root resolver detects the artifact layout (bundle at <install>/mcp/ beside the generated manifest ⇒ root is <install>) so even an env-stripping host cannot land state in a random cwd.
What the Claude config format actually supports
Checked, and it cuts both ways:
- The
cwd field is documented in Claude's plugins reference for stdio servers, and this repo's own emitted-config schema (src/adapters/schemas/claude/mcp.schema.json) accepts it — the adapter already emits it for command-based servers.
- But Claude Code currently ignores the field at runtime — silently, on all platforms: anthropics/claude-code#17565, #42883, #54786. The adapter's
args[0] absolutization looks like a deliberate hedge against exactly that; it covers the script path but not runtime state. (It also means the cwd the adapter emits for command-based servers is a no-op upstream today.)
Suggested fix directions
- Adapter-provided env anchor (works today). For source-built stdio servers, have every adapter emit a well-known env var — e.g.
AGENT_BUNDLE_PLUGIN_ROOT — set to its native plugin-root representation (${PLUGIN_ROOT} / ${CLAUDE_PLUGIN_ROOT} / the codex ./-relative form), and document it as the sanctioned install-root anchor for runtime code. This is exactly the consumer workaround, hoisted into the framework so every plugin doesn't rediscover it after a state-fragmentation incident.
- Also emit
cwd: "${CLAUDE_PLUGIN_ROOT}" for source-built servers. Documented format, schema-valid, harmless, and it becomes the real fix the day Claude Code honors the field — but per the linked upstream bugs it cannot be the only fix today.
- Documentation-only. Bless the per-server
env: { X: pathTokens.pluginRoot } pattern in the adapter docs as the claude-target equivalent of portable/codex cwd anchoring. Weakest: the failure mode it papers over is silent state fragmentation, discovered only after data lands in the wrong place.
Related: #42, #43, #44, #45 — surfaced by the same consumer port (movie-library); this one came out of its final hardening pass rather than the initial scaffold.
Package:
agent-bundleclaude adapter, at main4e546516. Surfaced during final hardening of the movie-library port (its commitaf0ab2a), where a copied claude artifact wrote persistent state into whatever directory the host happened to launch from.What differs across the three adapters
Normalization gives every source-built (
entry:) stdio server a plugin-root working directory in the canonical model —packages/agent-bundle/src/config/normalize.tsemitscommand: 'node',args: ['mcp/<name>.mjs', …],cwd: pathTokens.pluginRoot. The three adapters then diverge on what reaches the emitted config:src/adapters/portable.tsL159, L178): expands the token and emitscwd: "${PLUGIN_ROOT}". Anchor present.src/adapters/codex.tsL218–L226, L256): plugin-root cwd becomescwd: "./", and plugin-root-prefixed command/args/env values are rewritten./-relative against it — the anchor is load-bearing there (values embedding the plugin-root token without it are hard diagnostics). Anchor present.src/adapters/claude.tsL153–L161): absolutizesargs[0]to${CLAUDE_PLUGIN_ROOT}/mcp/…, then dropscwdfor any server withsourceset:So the bundled script itself resolves (absolute arg), but the spawned process's working directory is whatever the Claude host launched from — the plugin-root anchor that both sibling targets provide is simply absent. (Command-based servers with an explicit
cwddo get the field emitted, so the drop is specific to source-built entries.)Consumer consequence
Any plugin whose runtime resolves state or assets relative to its install root gets no anchor on a bare claude install. In the movie-library port, persistent stores (per-tracker policy ledger, Trakt device-flow auth) fragmented into
<launch-cwd>/.runtimeper launch directory — live credentials effectively lost between sessions. The port had to fix it consumer-side twice over (commitaf0ab2a):The claude adapter runs env values through its token expander, so this lands as
env: { MOVIE_LIBRARY_ROOT: "${CLAUDE_PLUGIN_ROOT}" }and works. As a second layer, the state-root resolver detects the artifact layout (bundle at<install>/mcp/beside the generated manifest ⇒ root is<install>) so even an env-stripping host cannot land state in a random cwd.What the Claude config format actually supports
Checked, and it cuts both ways:
cwdfield is documented in Claude's plugins reference for stdio servers, and this repo's own emitted-config schema (src/adapters/schemas/claude/mcp.schema.json) accepts it — the adapter already emits it for command-based servers.args[0]absolutization looks like a deliberate hedge against exactly that; it covers the script path but not runtime state. (It also means thecwdthe adapter emits for command-based servers is a no-op upstream today.)Suggested fix directions
AGENT_BUNDLE_PLUGIN_ROOT— set to its native plugin-root representation (${PLUGIN_ROOT}/${CLAUDE_PLUGIN_ROOT}/ the codex./-relative form), and document it as the sanctioned install-root anchor for runtime code. This is exactly the consumer workaround, hoisted into the framework so every plugin doesn't rediscover it after a state-fragmentation incident.cwd: "${CLAUDE_PLUGIN_ROOT}"for source-built servers. Documented format, schema-valid, harmless, and it becomes the real fix the day Claude Code honors the field — but per the linked upstream bugs it cannot be the only fix today.env: { X: pathTokens.pluginRoot }pattern in the adapter docs as the claude-target equivalent of portable/codex cwd anchoring. Weakest: the failure mode it papers over is silent state fragmentation, discovered only after data lands in the wrong place.Related: #42, #43, #44, #45 — surfaced by the same consumer port (movie-library); this one came out of its final hardening pass rather than the initial scaffold.