Skip to content

Claude Desktop (macOS) discards ZIP-stored Unix modes on extension extraction and writes 0600 — type: binary servers fail with EACCES #294

Description

@strahlungsfluss

Preflight

Searched existing issues. Related but distinct:

None covers the host discarding modes at extraction time.

Environment

Claude Desktop 1.21459.3
macOS 15.7.7 (24G720), arm64
Affected extension Fantastical 1.1.2 (ant.dir.gh.flexibits.fantastical-mcp), server.type: "binary", published by Flexibits Inc. via the in-app connector directory

Summary

Claude Desktop's extension extraction path writes every unpacked file at mode 0600, discarding the Unix permission bits stored in the .mcpb ZIP's external_attr. Files arrive owner-read/write only, with no execute bit for anyone.

type: node and type: python extensions survive this, because the executable being spawned is the interpreter (node), which lives outside the extension tree; the bundle's own files only need to be read, and 0600 grants owner read.

type: binary extensions do not survive it. The declared mcp_config.command is a file inside the extension tree, so the host must exec() it — and exec() on a file without +x returns EACCES. The server never starts, on install and on every subsequent launch.

Evidence: controlled comparison

A leftover install staging directory (~/Library/Application Support/Claude/dxt-install-8UbDW8/package.mcpb) preserved an original bundle, allowing a direct before/after comparison on the same file from the same archive:

Bundle: "Control your Mac" 0.0.1, server.type: node
File:   server/index.js

ZIP external_attr        -> 0o644   (creator_system: 3 = Unix)
Extracted with `ditto`   -> -rw-r--r--   (644)
Extracted by Claude Desktop -> -rw-------   (600)

The archive stores well-formed Unix permissions with the correct creator-system byte. ditto honours them. Claude Desktop does not.

This is not a umask artifact: the user's shell umask is 0022, and .DS_Store files written into the same directory tree by Finder are 0644. 0600 is neither 0666 & ~0022 nor the stored mode — the host is applying 0600 itself.

The normalisation is uniform across every installed extension, from three unrelated publishers (Anthropic's own chrome-control, a third-party osascript server, and Flexibits' Fantastical): every file in all three is -rw-------.

The failure, for the binary case

ant.dir.gh.flexibits.fantastical-mcp as extracted by Claude Desktop:

-rw-------  server/FantasticalMCP.app/Contents/MacOS/FantasticalMCP     <-- entry_point, no +x
-rw-------  server/FantasticalMCP.app/Contents/Frameworks/libswiftCompatibilitySpan.dylib
-rw-------  manifest.json
-rw-------  icon.png

Manifest (verbatim from the installed bundle):

"server": {
  "type": "binary",
  "entry_point": "server/FantasticalMCP.app/Contents/MacOS/FantasticalMCP",
  "mcp_config": {
    "command": "${__dirname}/server/FantasticalMCP.app/Contents/MacOS/FantasticalMCP",
    "args": [],
    "env": {}
  }
}

mcp-server-Fantastical.log, repeating on every launch:

[Fantastical] [info] Initializing server...
[Fantastical] [info] Using MCP server command: /Users/<user>/Library/Application Support/Claude/
  Claude Extensions/ant.dir.gh.flexibits.fantastical-mcp/server/FantasticalMCP.app/Contents/
  MacOS/FantasticalMCP with path: { ... }
[Fantastical] [info] Server started and connected successfully
Failed to spawn process: Permission denied
[Fantastical] [info] Server transport closed
[Fantastical] [info] Server transport closed unexpectedly, this is likely due to the process
  exiting early. ...
[Fantastical] [error] Server disconnected.

Note that Server started and connected successfully is logged before the spawn is known to have succeeded, and the actual cause (Failed to spawn process: Permission denied) is emitted un-timestamped and outside the [Fantastical] prefix. The user-visible surface is only "Server disconnected" and "Could not attach to MCP server Fantastical", neither of which points at permissions. This is a secondary reporting bug and is the reason the condition is hard to self-diagnose.

The bundle itself is sound

Everything except the mode bits is correct — this is not a publisher-side defect:

$ codesign -dv --verbose=2 FantasticalMCP.app
Identifier=com.flexibits.fantastical2.mac.mcp
Authority=Developer ID Application: Flexibits Inc. (85C27NK92C)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Notarization Ticket=stapled
TeamIdentifier=85C27NK92C

$ spctl -a -vv FantasticalMCP.app
accepted
source=Notarized Developer ID

$ file .../MacOS/FantasticalMCP
Mach-O universal binary with 2 architectures: [x86_64] [arm64]

No quarantine xattr. Signature valid, notarised, ticket stapled. The manifest declares type: binary exactly as the spec requires.

Workaround

chmod 755 "$HOME/Library/Application Support/Claude/Claude Extensions/\
ant.dir.gh.flexibits.fantastical-mcp/server/FantasticalMCP.app/Contents/MacOS/FantasticalMCP"

Then fully quit (⌘Q) and relaunch Claude Desktop. codesign --verify --deep --strict still passes afterwards (chmod does not affect the code directory hashes), and the server then completes a normal handshake:

{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"tools":{"listChanged":true}},
 "protocolVersion":"2024-11-05","serverInfo":{"name":"Fantastical","version":"1.1.2"}}}

The workaround does not survive extension auto-update, which re-extracts at 0600.

Note that reinstalling — the obvious user response, and the one the error text invites — cannot ever fix this, because each reinstall reproduces the same modes. Affected users will conclude the connector is simply broken.

Impact on #209

PR #209's description states:

Claude Desktop's MCPB extraction preserves ZIP-stored permissions without post-processing.

The comparison above shows this is false for Claude Desktop 1.21459.3 on macOS: a ZIP entry storing 0644 with creator_system: 3 lands on disk as 0600.

If extraction discards external_attr unconditionally, then #209's pack-side fix — forcing |0o111 into external_attrcannot have any effect on Claude Desktop installs, regardless of how the bundle was packed or on which platform. The same applies to the proposed fix in #235. Both are correct and worth having for other consumers of .mcpb, but neither can resolve binary-entry-point EACCES on Claude Desktop while the host clobbers modes on the way in.

This may mean #57 is not actually fixed in the shipping product.

Suggested fix

In the host's extraction path, in order of preference:

  1. Honour the ZIP's external_attr when creator_system == 3, as ditto and unzip do. This is what feat: enhanced structural validation for validate command #209 assumes already happens.
  2. Failing that, special-case the entry point: after extraction, if server.type == "binary", chmod +x the resolved entry_point. Cheap, targeted, and independent of what the archive stored.
  3. Either way, stop normalising to 0600. If the intent is to restrict access to extension files, 0700/0755 achieves that for the owner without breaking exec().

Separately, in the logging path:

  1. Do not log Server started and connected successfully before the spawn has been confirmed.
  2. Surface EACCES on the entry point as an actionable message. A permissions failure on a path the host itself just wrote is detectable at install time — ideally mcpb-installed binary extensions should fail loudly at install rather than silently at every launch.

Reproduction

  1. On macOS, install any extension with server.type: "binary" from the in-app connector directory (Fantastical 1.1.2 is a public, signed, notarised example).
  2. ls -l the path named in server.entry_point under ~/Library/Application Support/Claude/Claude Extensions/<ext-id>/.
  3. Observe mode 0600 — no execute bit — irrespective of the mode stored in the source .mcpb.
  4. Observe Failed to spawn process: Permission denied in ~/Library/Logs/Claude/mcp-server-<Name>.log, and "Server disconnected" in the UI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions