Repository navigation
[Bug]: integration-events.yml entry without an events key silently removes all event hooks #5092
Description
Bug Description
In .specify/integration-events.yml, an integration entry that is a mapping but has no events key is treated as an explicit "disable everything". resolve_events reads it with key_data.get("events", {}) (src/specify_cli/events/__init__.py:1000), so a missing key becomes an empty map. That empty map replaces the built-in and extension handlers, and the next install or upgrade removes the native hooks. No warning is printed, so a typo such as evnts: turns off every hook without any sign.
Steps to Reproduce
From a fresh clone after uv sync, save this as repro.py and run uv run python -I repro.py:
import subprocess, sys, tempfile
from pathlib import Path
specify = str(Path(sys.executable).parent / "specify")
proj = Path(tempfile.mkdtemp()) / "p"
def run(*args):
r = subprocess.run([specify, *args], cwd=proj if proj.exists() else None,
capture_output=True, text=True)
return r.stdout + r.stderr
run("init", str(proj), "--integration", "claude", "--non-interactive", "--ignore-agent-tools")
ext = proj / ".specify/extensions/foo"
ext.mkdir(parents=True)
(ext / "extension.yml").write_text(
'schema_version: "1.0"\nextension:\n id: foo\n'
"events:\n pre_tool_use:\n command: speckit.foo.x\n matcher: Bash\n")
settings = proj / ".claude/settings.json"
run("integration", "upgrade", "claude", "--force")
print("Without override, settings.json exists:", settings.exists())
# Typo: "evnts" instead of "events".
(proj / ".specify/integration-events.yml").write_text(
"integrations:\n claude:\n evnts:\n stop:\n command: speckit.x\n")
out = run("integration", "upgrade", "claude", "--force")
print("With typo override, settings.json exists:", settings.exists())
print("Warning lines:", [l for l in out.splitlines() if "warn" in l.lower() or "override" in l.lower()])The script initialises a Claude project, adds an extension directory that declares a pre_tool_use event, upgrades the integration, then adds an override whose only key is misspelt (evnts) and upgrades again.
Expected Behavior
The override should be ignored with a warning, and the existing handlers kept. The code comment at src/specify_cli/events/__init__.py:993-995 says "Only an explicitly present, mapping-valued events field replaces the prior layers." docs/integrations/cli.md:447-449 documents events: {} as the way to disable all handlers, and says an override that fails validation produces a warning and the defaults and extension handlers are kept. A non-mapping entry (claude: bad) is already handled this way (test test_non_mapping_integration_entry_abandons_override in tests/specify_cli/events/test_events.py).
Actual Behavior
The hook config is deleted and nothing is printed:
Without override, settings.json exists: True
With typo override, settings.json exists: False
Warning lines: []
Specify CLI Version
1.2.4.dev0 (main at 27d14e2)
AI Agent
Claude Code
Operating System
Linux 6.18 (Ubuntu container)
Python Version
Python 3.12.3
Additional Context
The same happens for claude: {}. Only the case where events is present as a mapping should replace the prior layers.
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