Skip to content

feat(scripts): add SPECIFY_NO_PERSIST env var to suppress feature.json writes (#4128) - #4129

Open
chelsealong wants to merge 1 commit into
github:mainfrom
chelsealong:fix/4128-specify-no-persist
Open

feat(scripts): add SPECIFY_NO_PERSIST env var to suppress feature.json writes (#4128)#4129
chelsealong wants to merge 1 commit into
github:mainfrom
chelsealong:fix/4128-specify-no-persist

Conversation

@chelsealong

Copy link
Copy Markdown
Contributor

Problem

Fixes #4128.

When multiple Spec Kit script invocations run concurrently against the same
checkout (e.g. multiple agents/subagents each working on a different
feature), each one typically sets its own SPECIFY_FEATURE_DIRECTORY. Most
core scripts (setup-plan, setup-tasks) call get_feature_paths() without
--no-persist, so every invocation persists its SPECIFY_FEATURE_DIRECTORY
to the shared .specify/feature.json — the last writer wins, silently
overwriting another process's pinned feature directory.

--no-persist (added for #3025) already exists as a per-call opt-out, but
it's a parameter each call site has to pass — scripts that don't know they
need it (like setup-plan/setup-tasks) never do.

Fix

Add SPECIFY_NO_PERSIST as the environment-level equivalent of
--no-persist / -NoPersist / no_persist=True. When set to 1 or true,
it suppresses the .specify/feature.json write in get_feature_paths()
regardless of whether the calling script passes the per-call flag. An
orchestrator can set it once for a whole process tree so no script
invocation in that tree can write feature.json, even ones that don't know
to opt out themselves.

Implemented identically in all three script variants per the project's
parity rule (AGENTS.md):

  • scripts/bash/common.shget_feature_paths()
  • scripts/powershell/common.ps1Get-FeaturePathsEnv
  • scripts/python/common.pyget_feature_paths()

Also documented the new variable in docs/reference/core.md's environment
variables table, next to SPECIFY_FEATURE_DIRECTORY.

Fully backward compatible: existing behavior when SPECIFY_NO_PERSIST is
unset is unchanged in every scenario.

Test plan

Added tests/test_specify_no_persist.py, covering all three script
variants via setup-plan (which calls get_feature_paths() without
--no-persist, so it's a script that previously had no way to opt out):

  • Baseline: SPECIFY_FEATURE_DIRECTORY set, no SPECIFY_NO_PERSIST
    feature.json is written (existing behavior, unchanged).
  • SPECIFY_NO_PERSIST=1/truefeature.json write is suppressed, for
    bash, PowerShell, and Python.
  • Two-agent race: agent A persists specs/001-a, agent B runs with
    SPECIFY_FEATURE_DIRECTORY=specs/002-b and SPECIFY_NO_PERSIST=1
    feature.json still points at agent A's specs/001-a (the pin isn't
    clobbered).

Confirmed the new tests fail without the fix (git checkout HEAD~1 -- scripts/bash/common.sh scripts/powershell/common.ps1 scripts/python/common.py,
then re-ran):

tests/test_specify_no_persist.py::test_bash_persists_by_default PASSED
tests/test_specify_no_persist.py::test_bash_specify_no_persist_suppresses_write FAILED
tests/test_specify_no_persist.py::test_bash_specify_no_persist_does_not_clobber_existing_pin FAILED
tests/test_specify_no_persist.py::test_ps_specify_no_persist_suppresses_write FAILED
tests/test_specify_no_persist.py::test_py_specify_no_persist_suppresses_write FAILED
4 failed, 1 passed in 1.27s

And pass with the fix restored:

tests/test_specify_no_persist.py::test_bash_persists_by_default PASSED
tests/test_specify_no_persist.py::test_bash_specify_no_persist_suppresses_write PASSED
tests/test_specify_no_persist.py::test_bash_specify_no_persist_does_not_clobber_existing_pin PASSED
tests/test_specify_no_persist.py::test_ps_specify_no_persist_suppresses_write PASSED
tests/test_specify_no_persist.py::test_py_specify_no_persist_suppresses_write PASSED
5 passed in 1.28s

Also ran:

  • shellcheck --severity=error on all tracked .sh files — clean.
  • markdownlint-cli2 on docs/reference/core.md — same 2 pre-existing
    MD028 findings as on unmodified main (unrelated blockquotes further
    down the file, confirmed by diffing lint output before/after).
  • Full pytest tests/ -q (6944 tests): 6921 passed, 9 skipped, 4 pre-existing
    failures unrelated to this change (test_presets.py,
    test_resolve_template_python_parity.py — template-composition/locale
    edge cases), confirmed present on main before this change too.

No manual agent-driven testing was done for this change since it's a
core-script/docs-only change with no slash-command template modifications.

AI disclosure

This PR was written primarily by an autonomous AI coding agent (Claude Code,
Claude Sonnet 5), including the code change, tests, and this description.

…n writes (github#4128)

Multi-agent setups running several Spec Kit script invocations concurrently
against the same checkout each set their own SPECIFY_FEATURE_DIRECTORY.
Every invocation that omits --no-persist (e.g. setup-plan, setup-tasks)
still writes that value to the shared .specify/feature.json, so agents can
clobber each other's pinned feature directory. SPECIFY_NO_PERSIST=1|true is
the environment-level equivalent of --no-persist, letting an orchestrator
suppress that write across every call in the process tree without patching
each call site.

Assisted-by: Claude Code (model: claude-sonnet-5, autonomous)
@chelsealong
chelsealong requested a review from mnriem as a code owner August 14, 2026 22:38
@wittkung

Copy link
Copy Markdown

Issue author here. Reviewed the diff — the implementation across all three language variants (bash/ps/python) matches the proposal in #4128 exactly, and the clobber-prevention test (test_bash_specify_no_persist_does_not_clobber_existing_pin) covers the core race scenario we described.

We backported the bash change locally and ran 8 end-to-end scenarios against get_feature_paths() directly — all passing:

  • Default persist ✓
  • --no-persist suppresses ✓
  • SPECIFY_NO_PERSIST=1 suppresses ✓
  • SPECIFY_NO_PERSIST=true suppresses ✓
  • Agent-A pin not clobbered by Agent-B with NO_PERSIST ✓
  • Path resolution unaffected by NO_PERSIST ✓
  • SPECIFY_NO_PERSIST=0 does not suppress (boundary) ✓

Happy to contribute a follow-up docs/guides/multi-agent.md guide once this lands — covering the full isolation protocol with integration examples for Antigravity, Claude Code, Cursor, and CI matrix jobs.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Multi-agent isolation protocol — process-level feature context without shared-state races

2 participants