Marketplace plugin .sh files lose execute permission after download

Resolved 💬 2 comments Opened Mar 26, 2026 by 64andrewwalker Closed Mar 27, 2026

Summary

All .sh files in the official marketplace (claude-plugins-official) lose their execute permissions after download, causing Permission denied errors when hooks try to execute them.

Root Cause (two bugs)

Bug 1: ZIP build pipeline strips permissions

The ZIP hosted at downloads.claude.ai/claude-code-releases/plugins/claude-plugins-official/{sha}.zip stores all .sh files with 0o600 permissions, despite the source repo (anthropics/claude-plugins-public) having them at 0o755.

Evidence:

import zipfile
with zipfile.ZipFile('marketplace.zip') as zf:
    for info in zf.infolist():
        if info.filename.endswith('.sh'):
            print(f'{oct(info.external_attr >> 16)} {info.filename}')
# All show 0o600

vs source repo:

$ gh api 'repos/anthropics/claude-plugins-public/git/trees/main?recursive=1' | jq '.tree[] | select(.path | endswith(".sh")) | "\(.mode) \(.path)"'
# All show 100755

Bug 2: fetchOfficialMarketplaceFromGcs doesn't restore permissions

In the client-side extraction code, files are written with fs.writeFile(path, content) without a mode parameter. This means even if Bug 1 is fixed, all files will get 0o666 & ~umask = 0o644 (no execute bit).

The fix should either:

  1. Read the ZIP entry's Unix permissions and pass them to writeFile's mode option
  2. Or add a post-extraction chmod +x for .sh files

Impact

  • ralph-loop plugin's stop hook fails every session with Permission denied
  • All other marketplace plugins with .sh hooks are affected (14 .sh files total)
  • The error recurs after every marketplace update, making chmod +x fixes temporary

Workaround

Added a SessionStart hook to auto-fix permissions:

{
  "type": "command",
  "command": "find ~/.claude/plugins/marketplaces -name '*.sh' ! -perm +111 -exec chmod +x {} +",
  "timeout": 5
}

Environment

  • Claude Code 2.1.84
  • macOS Darwin 25.1.0 (arm64)
  • umask: 022

View original on GitHub ↗

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