Skip to content

specify extension add leaves shipped POSIX scripts non-executable #3722

Description

@ogil109

What happens

Installing an extension that ships a shell script and then running that script the way
the extension's docs / CI instructions say to run it fails:

$ specify extension add <id> --from https://…/ext.zip
✓ Extension installed successfully!

$ .specify/extensions/<id>/scripts/bash/gate.sh check
bash: .../gate.sh: Permission denied

The installed script is -rw-r--r-- — the execute bit is gone.

Why

Extension archives are unpacked with zipfile.extractall (extensions/__init__.py),
and --dev installs copy the tree; neither restores a stripped Unix mode. So a bundled
*.sh lands non-executable.

The project already has ensure_executable_scripts() (specify_cli/__init__.py), whose
docstring explicitly covers .specify/extensions, and it is called from init,
migrate, and integration-install. extension add is the one install path that never
calls it — so the bit is only restored later, incidentally, if the user happens to run
specify init afterward. For someone adding an extension to an existing project (the
common case), it stays broken.

Impact

Any extension that ships a .sh and documents a direct
.specify/extensions/<id>/scripts/... invocation — e.g. a CI gate — fails on a fresh
extension add. The workaround (bash <path>) works but shouldn't be necessary, and it
isn't obvious.

Repro

  1. Any extension whose zip contains an executable-intended scripts/foo.sh.
  2. specify extension add … --from <zip-url> into a project.
  3. ls -l .specify/extensions/<id>/scripts/foo.sh-rw-r--r--; running it directly →
    Permission denied.

Verified against specify built from main (c0ba811).

Fix

One line: call ensure_executable_scripts(project_root) after a successful install in
extension_add, matching init / migrate / integration-install. PR attached.

AI assistance: this report and the linked fix were prepared with Claude Code; the
behaviour was verified end-to-end against the real CLI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions