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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions