Skip to content

[Bug]: workflow add <url> installs workflow.yml with mode 0600, while a catalog install uses the project file mode #5077

Open

Description

@hnipps

Bug Description

When specify workflow add is given a direct URL to a workflow YAML file, it downloads the file into a tempfile.NamedTemporaryFile, which has mode 0600 (src/specify_cli/workflows/command_add.py:400). It then copies that file's mode onto the installed workflow.yml (command_add.py:105 and :166). A catalog install of the same file uses the normal project file mode, 0666 masked by the umask (src/specify_cli/workflows/_commands.py:441). The same workflow therefore ends up readable only by its owner when installed from a URL.

Steps to Reproduce

Save the script below as repro.py in a fresh clone (after uv sync) and run uv run python -I repro.py. It serves the files from a local 127.0.0.1 HTTP server.

import functools, http.server, json, os, stat, subprocess, tempfile, threading
from pathlib import Path

os.umask(0o022)
wf = ('schema_version: "1.0"\nworkflow:\n  id: "{}"\n  name: "Demo"\n'
      '  version: "1.0.0"\nsteps:\n  - id: s1\n    type: shell\n    run: "true"\n')
proj = Path(tempfile.mkdtemp())
(proj / ".specify").mkdir()
site = proj / "site"
site.mkdir()
(site / "catwf.yml").write_text(wf.format("catwf"))
(site / "urlwf.yml").write_text(wf.format("urlwf"))
class Quiet(http.server.SimpleHTTPRequestHandler):
    def log_message(self, *args):
        pass
handler = functools.partial(Quiet, directory=str(site))
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{srv.server_address[1]}"
(site / "catalog.json").write_text(json.dumps({"workflows": {"catwf": {
    "id": "catwf", "name": "Demo", "version": "1.0.0", "url": base + "/catwf.yml"}}}))
env = {**os.environ, "SPECKIT_WORKFLOW_CATALOG_URL": base + "/catalog.json"}

subprocess.run(["specify", "workflow", "add", "catwf"], cwd=proj, env=env, check=True, capture_output=True)
subprocess.run(["specify", "workflow", "add", base + "/urlwf.yml"], cwd=proj, env=env, check=True, capture_output=True)
for wid in ("catwf", "urlwf"):
    mode = (proj / ".specify/workflows" / wid / "workflow.yml").stat().st_mode
    print(wid, oct(stat.S_IMODE(mode)))

Expected Behavior

Both installs get the project file mode, 0o644 under umask 022. tests/specify_cli/workflows/test_command_add.py:2484-2528 asserts 0644 for a fresh catalog install, and the _stage_workflow_file docstring (_commands.py:441) describes "the normal project-file mode" for downloaded files.

Actual Behavior

catwf 0o644
urlwf 0o600

Specify CLI Version

1.2.4.dev0 (main at 27d14e2)

AI Agent

Not applicable

Operating System

Linux 6.18 (Ubuntu container)

Python Version

Python 3.12.3

Additional Context

Low severity. Staging the URL download with use_project_file_mode=True, as the catalog path does, would make the two consistent.

Also reproduced on macOS (Darwin 25.6, arm64) with Python 3.14.4 and uv run python -I repro.py against 27d14e2; the output matches the above.

AI Disclosure

Found and reproduced with Claude Code (model: Claude Opus 4.8), run autonomously under human supervision. The human reporter re-ran every reproduction above before filing.

Activity

  1. added
    triage-can-waitVerdict: valid and in-scope but deprioritized; held behind the evidence gate
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage-can-waitVerdict: valid and in-scope but deprioritized; held behind the evidence gate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions