Skip to content

fix(events): leave JSON hook configs untouched when nothing is owned - #5017

Open
yxshee wants to merge 1 commit into
github:mainfrom
yxshee:fix/events-json-noop-removal
Open

yxshee wants to merge 1 commit into
github:mainfrom
yxshee:fix/events-json-noop-removal

Conversation

@yxshee

@yxshee yxshee commented Oct 17, 2026

Copy link
Copy Markdown

Description

Many commands refresh runtime events: specify extension add/remove/enable/disable/update, specify init with extensions, and integration setup. When no events are configured, that refresh rewrites the agent's native JSON config even though it holds nothing Spec Kit owns. Affected files include .claude/settings.json and opencode.json; the Gemini, Qwen, Tabnine, Cursor, Copilot and Devin configs go through the same JSON helpers. The rewrite:

  • re-indents the user's file
  • drops empty values such as "hooks": {"Notification": []} or "plugin": []
  • deletes a config whose whole content is {}

#4563 / #4564 fixed exactly this for Codex's TOML (_remove_toml_entries()). The JSON helpers never got the same guard.

Reproduction in a plain Claude project, adding the bundled selftest extension (which declares no events):

$ specify init p --integration claude --script sh --ignore-agent-tools --non-interactive && cd p
$ printf '{\n\t"permissions": {"allow": ["Bash(npm test)"]},\n\t"hooks": {"Notification": []}\n}\n' > .claude/settings.json
$ specify extension add --dev extensions/selftest
# main: .claude/settings.json rewritten
< 	"permissions": {"allow": ["Bash(npm test)"]},
< 	"hooks": {"Notification": []}
---
>   "permissions": {
>     "allow": [
>       "Bash(npm test)"
>     ]
>   }
# this branch: byte-identical

$ echo '{}' > .claude/settings.json
$ specify extension disable selftest
# main: .claude/settings.json deleted
# this branch: still exists, still {}

Root cause:

  • With an empty event map, install_integration_events() calls _remove_native_event_hooks().
  • Its JSON removers rebuild and write the file unconditionally, and unlink it when the rebuilt object is empty: _remove_json_entries(), _remove_copilot_entries(), _remove_json_root_entries() and _remove_opencode_entries().

The fix:

  • A small _has_marked_entry() helper on top of the existing _has_marker(), which already recurses into nested matcher-group hooks.
  • The three JSON removers return early when no Specify-marked entry is present.
  • _remove_opencode_entries() returns early when opencode.json doesn't reference the Spec Kit plugin.
  • Files that do hold owned entries are stripped, rewritten or deleted exactly as before.

Vibe is deliberately left out because open PR #4646 rewrites Vibe's merge and removal.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable): the reproduction above, on main and on this branch

New TestJsonNoOpRemoval in tests/specify_cli/events/test_events.py, each case parametrized over claude, copilot, cursor-agent, devin and opencode:

  • test_no_events_leaves_user_config_untouched: the bytes are unchanged
  • test_no_events_keeps_empty_user_config: a {} file is kept
  • owned entries are still stripped (the positive case)

On main, the first two cases fail for every key: 10 failures, e.g. assert b'{\n "hooks...' == b'{\n\t"hooks...' and FileNotFoundError: .../.claude/settings.json. The positive cases pass. With the change the file gives 161 passed.

  • Full suite (macOS, Python 3.13): 12345 passed, 376 skipped. 11 failures in the tests/test_*_python_parity.py files occur identically on unmodified main on this machine under full-suite load; those files pass when run on their own (257 passed, 112 skipped).
  • uvx ruff@0.15.0 check src tests: clean

AI Disclosure

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

AI disclosure: Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode) opened this PR on behalf of @yxshee. A Claude Code subagent (same model, autonomous) found the bug and wrote the fix and tests. The lead agent independently re-ran the new tests against main and the branch, reproduced the behavior above with the real CLI, reviewed the diff, and wrote this description. No human reviewed the change line by line before it was opened.

🤖 Generated with Claude Code

`specify extension add`, `remove`, `enable`, `disable` and `update`,
`specify init` with extensions, and integration setup all refresh
runtime events. With no events configured, the refresh takes
install_integration_events()'s empty-map branch into
_remove_native_event_hooks(), and its JSON removal helpers rewrote the
agent's native config even when it held no Specify-owned entries.

In a plain Claude project (likewise Gemini, Qwen, Tabnine, Cursor,
Copilot, Devin and opencode), adding an extension that declares no
events re-indented the user's .claude/settings.json or opencode.json,
dropped empty values such as "hooks": {"Notification": []} or
"plugin": [], and deleted a config whose whole content was {}.

github#4563 fixed the same defect for Codex's TOML in _remove_toml_entries().
Give the JSON helpers the matching guard: _remove_json_entries(),
_remove_copilot_entries() and _remove_json_root_entries() return early
when no Specify-marked entry is present, and _remove_opencode_entries()
returns early when opencode.json does not reference the Spec Kit plugin.
Files that do hold owned entries are still stripped, rewritten or
deleted as before.

Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yxshee
yxshee requested a review from mnriem as a code owner October 17, 2026 19:31
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Oct 18, 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.

2 participants