Cowork Desktop: ${CLAUDE_PLUGIN_DATA} is not persistent across conversations (MCP plugin tokens lost on every new conversation)

Resolved 💬 5 comments Opened Apr 21, 2026 by grbaden Closed Jul 4, 2026

Summary

${CLAUDE_PLUGIN_DATA} — the environment variable documented as the persistent, per-plugin storage location for MCP plugins — resolves to a per-conversation session-scoped path in Cowork desktop. Any file a plugin writes there is discarded when the user starts a new conversation, even though Cowork itself has not restarted and the plugin is re-loaded from the marketplace cleanly.

For any MCP plugin that performs an OAuth flow and caches the resulting token under ${CLAUDE_PLUGIN_DATA}, this means the user is forced to re-authenticate on every new conversation, which is contrary to the documented purpose of the variable and breaks the "stay signed in" expectation users bring from every other Claude product.

Reporter: Greg Baden, SVP & General Counsel / AI Initiative Lead, NetBrain Technologies
Contact: greg.baden@netbraintech.com
Anthropic org: NetBrain Technologies (Enterprise)
Date observed: 2026-04-20
Severity: Blocks 400-user rollout of our custom Salesforce MCP plugin
Product area: Cowork desktop, MCP plugin runtime, plugin-data storage

---

Expected behavior

Per Anthropic's MCP plugin documentation, ${CLAUDE_PLUGIN_DATA} should resolve to a directory that persists across conversations, sessions, and Cowork restarts, scoped to the plugin + user. A token written there on Monday should still be readable on Tuesday — the whole reason the variable exists.

Anthropic's own AI support agent confirmed this expectation verbatim when asked about the issue (2026-04-20):

"The ${CLAUDE_PLUGIN_DATA} directory should be persistent across conversations and plugin updates. It resolves to ~/.claude/plugins/data/{id}/ where {id} is your plugin identifier. This directory is specifically designed to survive updates and store things like node_modules, Python virtual environments, generated code, and caches that should persist across plugin versions. […] The data directory is only deleted when you uninstall the plugin from all scopes where it's installed. If it's not persisting between conversations, this suggests there may be a bug with how the directory is being handled."

On Cowork desktop we are observing precisely the "not persisting between conversations" behavior the agent flags as bug-shaped. Note also the key phrasing "resolves to ~/.claude/plugins/data/{id}/" — a host-filesystem path, not a sandboxed-VM path. Our observation is that Cowork is resolving it to a VM-internal location under /sessions/<session-id>/mnt/.claude/plugins/data/<plugin>-inline/ instead.

Actual behavior

${CLAUDE_PLUGIN_DATA} resolves to /sessions/<session-id>/mnt/.claude/plugins/data/<plugin-name>-inline/, where <session-id> is a random per-conversation name (e.g., vigilant-laughing-pascal, wonderful-peaceful-newton). The directory is part of a per-session VM user's home, is not readable by later sessions, and effectively no longer exists from a plugin's point of view once the conversation ends.

---

Reproduction steps

  1. Install any MCP plugin that caches state under ${CLAUDE_PLUGIN_DATA}. (Our plugin is salesforce-mcp v0.2.2, published to NetBrain's private marketplace — plugin manifest sets "TOKEN_CACHE_PATH": "${CLAUDE_PLUGIN_DATA}/salesforce-token.json".)
  2. In a Cowork conversation, complete the OAuth Device Flow. Plugin writes a token file to ${CLAUDE_PLUGIN_DATA}/salesforce-token.json and logs the absolute path.
  3. Confirm the plugin reports status: authenticated with has_refresh_token: true.
  4. Fully quit Cowork (Cmd+Q on macOS).
  5. Relaunch Cowork. Start a new conversation.
  6. In the new conversation, query the plugin's auth status (salesforce_check_auth).

Observed: Plugin reports "not connected — no token is cached for this session." User must re-run the entire OAuth flow, including MFA.

---

Evidence

In the authenticating conversation, salesforce_check_auth returned:

status: authenticated
instance_url: https://nbtech.my.salesforce.com
token_path: /sessions/vigilant-laughing-pascal/mnt/.claude/plugins/data/salesforce-mcp-inline/salesforce-token.json
has_refresh_token: true

In the next conversation (after Cowork quit+relaunch), the plugin's own startup log shows it resolving CLAUDE_PLUGIN_DATA to a different path:

token_path: /sessions/wonderful-peaceful-newton/mnt/.claude/plugins/data/salesforce-mcp-inline/salesforce-token.json

Filesystem state in the new session:

$ ls -la /sessions/wonderful-peaceful-newton/mnt/.claude/plugins/data/salesforce-mcp-inline/
salesforce-mcp.log      <- log file present (newly created for this session)
                        <- no salesforce-token.json — directory is otherwise empty

The previous session's directory still exists on the host but is unreadable from the new session:

$ ls -la /sessions/vigilant-laughing-pascal/
drwxr-x--- 6 nobody nogroup ...       <- owned by a different session user,
                                         locked out by Unix permissions

The wonderful-peaceful-newton session cannot read vigilant-laughing-pascal's directory, so even if the file survived on the host, the plugin in the new session has no way to reach it.

Plugin manifest (.mcp.json): — uses the documented pattern exactly:

{
  "mcpServers": {
    "netbrain-salesforce": {
      "command": "node",
      "args": ["${CLAUDE_PLUGIN_ROOT}/servers/salesforce-mcp-server.js"],
      "env": {
        "TOKEN_CACHE_PATH": "${CLAUDE_PLUGIN_DATA}/salesforce-token.json"
      }
    }
  }
}

What does persist across conversations (for comparison)

We tested by writing sentinel files to six candidate locations in one conversation and checking each from a new conversation. Within the same plugin sandbox:

| Location | Persists across new conversations? |
|---|---|
| /sessions/<id>/mnt/.claude/plugins/data/<plugin>/ (i.e. ${CLAUDE_PLUGIN_DATA}) | No |
| /sessions/<id>/mnt/.claude/ root | (tested — result in attached sentinel notes) |
| /sessions/<id>/mnt/.auto-memory/ | Yes (auto-memory files from prior conversations are present) |
| /sessions/<id>/mnt/<user-selected-workspace-folder>/ | Yes (host-mounted) |
| /sessions/<id>/ (VM home) | No — session-scoped |
| /sessions/<id>/tmp/ | No |

The first row is the surprising one. Everywhere else under mnt/ that we expect to be persistent (auto-memory, workspace folders) is persistent. .claude/plugins/data/ is the only exception — and it is the one location a plugin is explicitly directed to use.

---

Impact

  • Blocks our planned rollout of the NetBrain Salesforce MCP plugin to ~400 users. Forcing every Salesforce user through an MFA challenge at the start of every Cowork conversation is not acceptable in a security-conscious enterprise.
  • Affects every OAuth-backed MCP plugin on Cowork desktop, not just ours — any plugin following the documented ${CLAUDE_PLUGIN_DATA} pattern has the same problem (Google Workspace, Slack, Zoom, Microsoft, Notion, etc.).
  • The problem is silent: plugins appear to work during initial QA (within one conversation) and only surface after users start a second conversation, so this is very easy to miss in pre-publish validation.

Related issue

We are also tracking a separate Cowork bug observed the same day: the admin "Allow network egress" domain allowlist does not actually enforce entries when set to "Package managers only" with additional specific domains added. Wildcard entries (e.g., *.salesforce.com) do not resolve hostnames (e.g., login.salesforce.com) inside the plugin runtime — DNS fails with EAI_AGAIN. Switching the dropdown to "All domains" is the only workaround. Happy to bundle that into a separate ticket or include here — let us know which is preferred.

---

Requested fix (one of)

  1. Preferred: make ${CLAUDE_PLUGIN_DATA} resolve to a stable, user-scoped, plugin-scoped path that survives new conversations and Cowork restarts — matching the documented behavior.
  2. Alternatively, expose a new, clearly-named environment variable for genuinely-persistent plugin storage (e.g., CLAUDE_PLUGIN_DATA_PERSISTENT or CLAUDE_USER_PLUGIN_DATA) and update documentation so plugin authors know to use it for credentials.
  3. If the session-scoped behavior is intentional for some reason, update the plugin documentation to call this out explicitly, and provide a supported path for OAuth token persistence.

Suggested workaround for plugin authors in the meantime

If we have to ship against the current behavior, the plugin could be modified to probe an ordered list of candidate paths on startup (e.g., $HOME/.salesforce-mcp-token.json, ${CLAUDE_PLUGIN_DATA}, or a known-persistent mount like /sessions/*/mnt/.auto-memory/) and migrate an existing token file into ${CLAUDE_PLUGIN_DATA} on first access. This is hacky and depends on Cowork's internal filesystem layout. A first-class fix from Anthropic is preferable.

---

Contact

Please reply to greg.baden@netbraintech.com. Able to provide:

  • Full plugin source (TypeScript, open to your engineers)
  • Plugin debug logs with full err.cause chains (added in v0.2.2 specifically to capture issues like this)
  • Screenshots of the successful auth in session A followed by the "not connected" state in session B
  • Access to a NetBrain Enterprise Cowork environment to reproduce live

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗