Repository navigation
[Bug]: get_feature_paths persists .specify/feature.json during read-only path resolution (check-prerequisites.sh --paths-only) #3025
Description
Activity
github-actions commented
on Jun 17, 2026 on Jun 17, 2026 – with GitHub ActionsContributorMore actionsBug assessment — feature-json-paths-only-write: Valid · severity medium
Bug Assessment: get_feature_paths persists feature.json in --paths-only mode
- Slug:
feature-json-paths-only-write - Created: 2026-06-17T22:15:13Z
- Source: issue [Bug]: get_feature_paths persists .specify/feature.json during read-only path resolution (check-prerequisites.sh --paths-only) #3025
- Verdict: valid
- Severity: medium
Report (summarized)
Reported by @athemelis on Specify CLI v0.11.0 (macOS). When
SPECIFY_FEATURE_DIRECTORYis set to a value that differs from what is stored in.specify/feature.json, invokingcheck-prerequisites.sh --paths-onlysilently overwrites.specify/feature.jsonwith the env var value. The--paths-onlyflag 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-onlywithSPECIFY_FEATURE_DIRECTORYset to a value different from the one pinned in.specify/feature.jsonoverwrites the file and dirties the working tree. Expected behavior is that--paths-onlyperforms no writes to tracked files.Reproduction
- Initialize a spec-kit project and run
specifyto create.specify/feature.jsonwith a pinnedfeature_directory(e.g.,specs/001-my-feature). - Run
SPECIFY_FEATURE_DIRECTORY=specs/999-temp check-prerequisites.sh --paths-only. - Run
git status—.specify/feature.jsonshows as modified. - Run
cat .specify/feature.json— the pinned valuespecs/001-my-featurehas been replaced byspecs/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— callsget_feature_paths(and thus_persist_feature_json) unconditionally before reaching the--paths-onlyearly-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 callsSave-FeatureJsonunconditionally when$env:SPECIFY_FEATURE_DIRECTORYis set.scripts/powershell/check-prerequisites.ps1:60–63— callsGet-FeaturePathsEnvbefore the$PathsOnlyguard, so the write occurs even in-PathsOnlymode.
Root Cause Hypothesis
get_feature_paths()(and its PowerShell equivalentGet-FeaturePathsEnv) was designed to both resolve and persist the feature directory in a single step — the comment atcommon.sh:134states "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-onlypath incheck-prerequisites.sh. The function has no write/read-only mode concept, and the--paths-onlyguard incheck-prerequisites.shfires only afterget_feature_pathshas 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=1that_persist_feature_json()andSave-FeatureJson()check at the top of each function, returning immediately without writing when the var is set. Incheck-prerequisites.sh, export this var before callingget_feature_pathswhenPATHS_ONLY=true; do the same incheck-prerequisites.ps1for-PathsOnly. This keepsget_feature_pathsgeneric, adds no new parameters, requires no callers other thancheck-prerequisitesto change, and is consistent with the pattern already present in the codebase (SPECIFY_FEATURE,SPECIFY_FEATURE_DIRECTORY).Alternatives:
- Add an optional
--no-persistpositional argument toget_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 separateresolve_feature_paths()(read-only) andresolve_and_persist_feature_paths()(write-allowed) functions, withcheck-prerequisites.shusing 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— addSPECIFY_NO_PERSIST_FEATURE_JSONguard at the top of_persist_feature_json()scripts/powershell/common.ps1— add$env:SPECIFY_NO_PERSIST_FEATURE_JSONguard at the top ofSave-FeatureJson()scripts/bash/check-prerequisites.sh— set/exportSPECIFY_NO_PERSIST_FEATURE_JSON=1before theget_feature_pathscall whenPATHS_ONLY=truescripts/powershell/check-prerequisites.ps1— set$env:SPECIFY_NO_PERSIST_FEATURE_JSON = '1'beforeGet-FeaturePathsEnvwhen$PathsOnlyis settests/test_check_prerequisites_paths_only.py— add test assertingfeature.jsoncontent is unchanged after--paths-onlywhenSPECIFY_FEATURE_DIRECTORYdiffers 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_jsonandcommon.ps1/Save-FeatureJsonsimultaneously or the.ps1path will remain broken. - Other callers of
get_feature_paths:setup-plan.sh(line 31) andsetup-tasks.sh(line 26) also callget_feature_paths— they should continue to persist as before; the opt-out is only engaged fromcheck-prerequisites.sh. - Env var leakage: if
check-prerequisites.shis sourced (rather than executed) by another script, the exportedSPECIFY_NO_PERSIST_FEATURE_JSONwould persist in the caller's environment. Unsetting it after the call incheck-prerequisites.shmitigates 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 · ◷
- Slug:
- added a commit that references this issue
on Jun 29, 2026 - added a commit that references this issue
on Jun 30, 2026 - added a commit that references this issue
on Jun 30, 2026
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
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
specifyunder its own Python, not the system CLT 3.9.6 or Homebrew 3.13/3.14)Error Logs
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.