Skip to content

[Bug]: get_feature_paths persists .specify/feature.json during read-only path resolution (check-prerequisites.sh --paths-only) #3025

Description

@athemelis

Bug Description

get_feature_paths() in common.sh calls _persist_feature_json when SPECIFY_FEATURE_DIRECTORY is set, even when invoked purely for path resolution — e.g. check-prerequisites.sh --paths-only, which is documented as "only output path variables (no validation)". This writes the tracked file .specify/feature.json and can overwrite a previously pinned feature directory when a caller passes a temporary override. A skip-write-when-unchanged guard exists, so it only writes when the value differs — but an override value still triggers an unexpected write.

Steps to Reproduce

  1. Pin a feature directory in .specify/feature.json.
  2. Run check-prerequisites.sh --paths-only with SPECIFY_FEATURE_DIRECTORY set to a different value.
  3. Run git status.

Expected Behavior

Read-only / --paths-only path resolution does not modify tracked files.

Actual Behavior

.specify/feature.json is rewritten — the working tree is dirtied and the pinned value is overwritten.

Specify CLI Version

0.11.0

AI Agent

GitHub Copilot

Operating System

macOS Tahoe 26.5.1

Python Version

3.11.15 (uv-managed standalone CPython, cpython-3.11-macos-aarch64-none — uv installs specify under its own Python, not the system CLT 3.9.6 or Homebrew 3.13/3.14)

Error Logs

none (silent write)

Additional Context

Files: .specify/scripts/bash/common.sh (get_feature_paths, _persist_feature_json), .specify/scripts/bash/check-prerequisites.sh. Suggested fix: an opt-out (e.g. SPECIFY_NO_PERSIST_FEATURE_JSON=1) honored in --paths-only mode. Sibling issues: agent-context scoped layout; empty CURRENT_BRANCH.

Activity

  1. github-actions commented on Jun 17, 2026

    @github-actions
    Contributor

    Bug assessment — feature-json-paths-only-write: Valid · severity medium


    Bug Assessment: get_feature_paths persists feature.json in --paths-only mode

    Report (summarized)

    Reported by @athemelis on Specify CLI v0.11.0 (macOS). When SPECIFY_FEATURE_DIRECTORY is set to a value that differs from what is stored in .specify/feature.json, invoking check-prerequisites.sh --paths-only silently overwrites .specify/feature.json with the env var value. The --paths-only flag is documented as "only output path variables (no validation)", which users reasonably expect to be a read-only operation. The working tree is dirtied and the previously pinned feature directory is overwritten with no output or warning.

    Symptom

    Running check-prerequisites.sh --paths-only with SPECIFY_FEATURE_DIRECTORY set to a value different from the one pinned in .specify/feature.json overwrites the file and dirties the working tree. Expected behavior is that --paths-only performs no writes to tracked files.

    Reproduction

    1. Initialize a spec-kit project and run specify to create .specify/feature.json with a pinned feature_directory (e.g., specs/001-my-feature).
    2. Run SPECIFY_FEATURE_DIRECTORY=specs/999-temp check-prerequisites.sh --paths-only.
    3. Run git status — .specify/feature.json shows as modified.
    4. Run cat .specify/feature.json — the pinned value specs/001-my-feature has been replaced by specs/999-temp.

    Suspected Code Paths

    • scripts/bash/common.sh:121–164 — get_feature_paths(): at line 135, unconditionally calls _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY" whenever the env var is set. There is no mechanism for callers to request a read-only resolution.
    • scripts/bash/common.sh:93–119 — _persist_feature_json(): has a "skip-write-when-unchanged" guard at lines 103–108 but only skips when the stored value already matches the incoming one. When the values differ, the write proceeds.
    • scripts/bash/check-prerequisites.sh:82–84 — calls get_feature_paths (and thus _persist_feature_json) unconditionally before reaching the --paths-only early-exit at line 87. By the time the early-exit fires, the file has already been written.
    • scripts/powershell/common.ps1:103–155 — Get-FeaturePathsEnv(): same issue; line 119 calls Save-FeatureJson unconditionally when $env:SPECIFY_FEATURE_DIRECTORY is set.
    • scripts/powershell/check-prerequisites.ps1:60–63 — calls Get-FeaturePathsEnv before the $PathsOnly guard, so the write occurs even in -PathsOnly mode.

    Root Cause Hypothesis

    get_feature_paths() (and its PowerShell equivalent Get-FeaturePathsEnv) was designed to both resolve and persist the feature directory in a single step — the comment at common.sh:134 states "Persist to feature.json so future sessions without the env var still work." This is correct for write-intent callers (create-new-feature.sh, setup-plan.sh, setup-tasks.sh) but is an unintended side effect for the read-only --paths-only path in check-prerequisites.sh. The function has no write/read-only mode concept, and the --paths-only guard in check-prerequisites.sh fires only after get_feature_paths has already mutated the file. Confidence: high — the code path is unambiguous and the reproduction steps are mechanically deterministic.

    Proposed Remediation

    Preferred: Introduce an opt-out env var SPECIFY_NO_PERSIST_FEATURE_JSON=1 that _persist_feature_json() and Save-FeatureJson() check at the top of each function, returning immediately without writing when the var is set. In check-prerequisites.sh, export this var before calling get_feature_paths when PATHS_ONLY=true; do the same in check-prerequisites.ps1 for -PathsOnly. This keeps get_feature_paths generic, adds no new parameters, requires no callers other than check-prerequisites to change, and is consistent with the pattern already present in the codebase (SPECIFY_FEATURE, SPECIFY_FEATURE_DIRECTORY).

    Alternatives:

    • Add an optional --no-persist positional argument to get_feature_paths() (e.g., get_feature_paths --no-persist) so callers declare intent. Trade-off: changes the function signature; any future caller that forgets the flag silently gets the write behavior.
    • Refactor get_feature_paths() into separate resolve_feature_paths() (read-only) and resolve_and_persist_feature_paths() (write-allowed) functions, with check-prerequisites.sh using the former. Trade-off: larger refactor and more surface to keep in sync across bash and PowerShell.

    Files likely to change:

    • scripts/bash/common.sh — add SPECIFY_NO_PERSIST_FEATURE_JSON guard at the top of _persist_feature_json()
    • scripts/powershell/common.ps1 — add $env:SPECIFY_NO_PERSIST_FEATURE_JSON guard at the top of Save-FeatureJson()
    • scripts/bash/check-prerequisites.sh — set/export SPECIFY_NO_PERSIST_FEATURE_JSON=1 before the get_feature_paths call when PATHS_ONLY=true
    • scripts/powershell/check-prerequisites.ps1 — set $env:SPECIFY_NO_PERSIST_FEATURE_JSON = '1' before Get-FeaturePathsEnv when $PathsOnly is set
    • tests/test_check_prerequisites_paths_only.py — add test asserting feature.json content is unchanged after --paths-only when SPECIFY_FEATURE_DIRECTORY differs from the pinned value

    Risks & Considerations

    • No backward-compatibility breakage: the opt-out env var approach only changes behavior when explicitly set; all existing call sites that rely on the implicit persist (plan, tasks, create-new-feature) are unaffected.
    • PowerShell parity required: the fix must be applied to both common.sh / _persist_feature_json and common.ps1 / Save-FeatureJson simultaneously or the .ps1 path will remain broken.
    • Other callers of get_feature_paths: setup-plan.sh (line 31) and setup-tasks.sh (line 26) also call get_feature_paths — they should continue to persist as before; the opt-out is only engaged from check-prerequisites.sh.
    • Env var leakage: if check-prerequisites.sh is sourced (rather than executed) by another script, the exported SPECIFY_NO_PERSIST_FEATURE_JSON would persist in the caller's environment. Unsetting it after the call in check-prerequisites.sh mitigates this.

    Open Questions

    • None — the report provides sufficient detail for a complete fix.

    Generated by 🐛 Assess Bug from Labeled Issue for issue #3025 · 162.8 AIC · ⌖ 34.2 AIC · ⊞ 31.2K · ◷

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions