Skip to content

fix(integrations): keep shared scripts executable when the default integration changes - #5425

Open
kartsan03 wants to merge 1 commit into
github:mainfrom
kartsan03:fix/keep-scripts-executable-on-default-change
Open

kartsan03 wants to merge 1 commit into
github:mainfrom
kartsan03:fix/keep-scripts-executable-on-default-change

Conversation

@kartsan03

Copy link
Copy Markdown
Contributor

Description

After specify integration switch <key>, specify integration use <key>, or specify integration uninstall of the default integration (when another installed one takes over), every script in .specify/scripts/bash/ has mode 0644. The generated commands and skills tell the agent to run them directly (.specify/scripts/bash/setup-plan.sh --json in speckit-plan, and so on), so on macOS and Linux each of those steps now fails with Permission denied (exit 126).

All three commands go through _set_default_integration() in integrations/_helpers.py. It refreshes shared infrastructure with _install_shared_infra(..., refresh_managed=True), which writes the scripts with their packaged mode, 0644. init, integration install, integration upgrade and the first refresh in switch call ensure_executable_scripts() after they write the scripts. _set_default_integration() didn't. In switch the existing call (command_switch.py:293) runs before the final default refresh, and that refresh writes the scripts a second time.

This PR calls ensure_executable_scripts() right after that refresh, the same way the other commands do and the way #3723 handled extension add. On Windows it does nothing, like the other call sites.

Repro with the real CLI:

specify init --here --integration claude --script sh --ignore-agent-tools
specify integration switch codex
stat -c '%a %n' .specify/scripts/bash/*.sh
.specify/scripts/bash/check-prerequisites.sh --help
After main (2ee2b1e) This PR
integration switch codex scripts 0644, direct run exits 126 0755, exits 0
integration install codex --script sh, then integration use codex 0644, exits 126 0755, exits 0
integration install codex --script sh, then integration uninstall claude 0644, exits 126 0755, exits 0

Release 1.3.3 behaves the same for all three. #5377 added one more path: switch <current default> --refresh-shared-infra now refreshes too, so it also drops the bits.

With this change, the next use, switch or uninstall also fixes a project that is already affected. On main, specify integration upgrade already fixes them, since it calls ensure_executable_scripts().

On the switch path the console now prints "Updated execute permissions on 6 script(s) recursively" twice, once after each refresh. I kept the existing call in command_switch.py: if the switch fails before the default is set, the scripts from the first refresh still get their bits.

Testing

  • Tested locally with .venv/bin/specify --help
  • Ran relevant existing tests with .venv/bin/python -m pytest tests -q
  • Tested with a sample project (if applicable)

New CLI tests, one per command, in the files design/cli.md assigns them to. Each checks the scripts are executable before the command and still executable after it:

  • test_command_switch.py: TestIntegrationSwitch::test_switch_keeps_shared_scripts_executable, for a new target, an installed target, and the current default with --force and with --refresh-shared-infra
  • test_command_use.py: TestIntegrationUse::test_use_keeps_shared_scripts_executable
  • test_command_uninstall.py: TestIntegrationUninstall::test_uninstall_default_keeps_fallback_scripts_executable

With _helpers.py reverted to main, all 6 cases fail, for example AssertionError: switch left shared scripts non-executable: ['check-prerequisites.sh', 'common.sh', 'create-new-feature.sh', 'resolve-template.sh', 'setup-plan.sh', 'setup-tasks.sh']. With the fix, all 6 pass. They are skipped on Windows, like #3723's test.

Full suite on Linux, Python 3.13: 16273 passed, 566 skipped, 1 failed (uv sync --extra test, then pytest -n 2, 9 min). The failure, test_version_execution.py::TestWindowsUvToolEnvironment::test_non_windows_uv_tool_dry_run_still_previews_installer, fails the same way on main here: Rich wraps the dry-run output, and my checkout's venv path is long enough to be split mid-path. Ruff 0.15.0 (ruff check src tests) passes. I haven't run it on Windows or macOS.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: Codex CLI (model: gpt-6.1-sol, reasoning effort xhigh, autonomous, offline sandbox) found the dropped execute bits while comparing release 1.3.3 and main on the same commands, and wrote a first patch. Claude Code (model: claude-opus-5-5, autonomous) traced every affected command to _set_default_integration(), rewrote the fix and the tests, reran the repro with the real CLI on main and the branch, ran the full suite and lint, and drafted this description. The commit carries Assisted-by: trailers for both.

…tegration changes

`integration use`, every `integration switch` path, and `integration uninstall`
of the default integration refresh shared infrastructure through
`_set_default_integration()`. The refresh rewrites `.specify/scripts/bash/*.sh`
with the packaged mode (0644) and nothing restored the execute bits, so the
scripts that generated commands and skills run directly failed with
"Permission denied". Restore them after the refresh, as init, install and
upgrade already do.

Assisted-by: Codex (model: gpt-6.1-sol, autonomous)
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
@kartsan03
kartsan03 requested a review from mnriem as a code owner November 13, 2026 15:38
Copilot AI dynamic review requested due to automatic review settings November 13, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human confirmation is needed because the shared transition changes permissions across use, switch, uninstall, and rollback, with interactions involving lifecycle journaling, partial-refresh recovery, and POSIX/Windows behavior.

No open issues 🎉

What changed in this PR

This PR fixes a permissions regression where integration changes rewrote shared POSIX scripts without execute permissions, causing directly invoked generated commands to fail. It restores executable bits after default-integration refreshes and adds regression coverage.

Changes:

  • Restore execute permissions in _set_default_integration() after shared infrastructure refreshes.
  • Test integration switch, integration use, and default-integration fallback during integration uninstall.
  • Cover new, installed, and current-default switch paths, including refresh options.
File Description
tests/​specify_cli/​integrations/​test_command_use.py Verifies that integration use preserves executable permissions on shared scripts.
tests/​specify_cli/​integrations/​test_command_uninstall.py Verifies that fallback shared scripts remain executable after uninstalling the default integration.
tests/​specify_cli/​integrations/​test_command_switch.py Covers executable permissions across new, installed, and current-default integration switch paths.
src/​specify_cli/​integrations/​_helpers.py Restores executable permissions after default-integration shared infrastructure refreshes.

Review effort: Lite (dynamic)

@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Nov 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-must-have Verdict: high-value, important work for Spec Kit — do first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants