Repository navigation
fix(extensions): reject hook commands that are not strings - #5146
Conversation
`ExtensionManifest` only checked that a hook's `command` was truthy, so `command: [speckit.x.hello]` or a mapping passed validation. `extension add` then crashed in `HookExecutor.register_hooks` with `TypeError: unhashable type: 'list'`, after the extension directory and the agent skill had already been written. Apply the rule extension events already use (github#17): a hook command must be a non-blank string, so the manifest is rejected with a `ValidationError` before anything is installed. Refs github#5080 Assisted-by: Codex CLI (model: gpt-6.1-sol, autonomous) Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
There was a problem hiding this comment.
🟡 Changes recommended
An open review nit flags that (#17) points to an unrelated Gemini CLI issue rather than the relevant validate_events() check.
1 open issue
What changed in this PR
This PR rejects malformed hook commands during extension manifest validation, preventing the installation crash and leftover files reported in #5080.
Changes:
- Require hook commands to be non-blank strings, matching event validation.
- Add parameterized manifest tests for invalid commands in both supported hook shapes.
- Add CLI regression tests verifying clean failure without installed files or registry entries.
The PR provides before-and-after regression results; tests were not rerun during this review.
| File | Description |
|---|---|
tests/test_extensions.py |
Tests rejection of non-string and blank hook commands. |
tests/specify_cli/extensions/test_command_add.py |
Verifies invalid hooks fail before installation writes files. |
src/specify_cli/extensions/__init__.py |
Validates hook command type and non-blank content. |
Review effort: Max
|
Please address Copilot feedback |
`github#17` read as a GitHub issue reference, but that issue is about Gemini CLI. Refs github#5080 Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
|
Fixed in b6d14f3. The comment now points to Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). |
|
Thank you! |

Description
ExtensionManifestonly checks that a hook'scommandis truthy, socommand: [speckit.test-ext.hello]or a mapping passes validation.specify extension addthen fails inHookExecutor.register_hookswithTypeError: unhashable type: 'list'. By then the extension directory and the agent skill are already written, so the user gets a raw traceback and leftover files, with no registry entry.Extension events already reject this:
events/__init__.pyrequires a non-blank stringcommand(validate_events()). This PR applies the same check to hooks, so the manifest fails with aValidationErrorbefore anything is installed.EXTENSION-API-REFERENCE.mdalready documents the field ascommand: string.Repro with the real CLI and the manifest from the issue:
main(222a973)TypeError: unhashable type: 'list'Validation Error: Hook 'after_tasks' missing required 'command' string.specify/extensions/test-ext/,.claude/skills/speckit-test-ext-hello/SKILL.mdRefs #5080. This covers the reported case only. The issue also notes that the install rollback only cleans up for the generic agent; I left that alone because #4959 reworks the install transaction.
Testing
uv run specify --helpuv sync && uv run pytestNew tests:
tests/test_extensions.py::TestExtensionManifest::test_hook_command_must_be_a_string: list, mapping, integer, boolean and blank commands, each as a single mapping and in a list of entries, are rejected.tests/specify_cli/extensions/test_command_add.py::TestExtensionAddCLI::test_add_dev_rejects_invalid_hook_command_before_writing_files[list|mapping]:extension add --devexits 1 with no traceback, and leaves no extension directory, agent files or registry entry.With
extensions/__init__.pyreverted tomain, all 10 manifest cases fail (DID NOT RAISE), and both CLI cases fail onTypeError("unhashable type: 'list'")/'dict'.Full suite on Linux, Python 3.13: 13179 passed, 435 skipped (
uv sync --extra test, thenpytest -n 3, 6 min). Ruff 0.15.0 (ruff check src tests) passes. I haven't run it on Windows.AI Disclosure
AI disclosure: Codex CLI (model: gpt-6.1-sol, reasoning effort xhigh, autonomous, offline sandbox) reproduced the issue on
mainand wrote a first version of the fix and tests. Claude Code (model: claude-opus-5-5, autonomous) narrowed it to thecommandcheck, matched it to the existing events rule, reran the repro with the real CLI onmainand the branch, ran the full suite and lint, and drafted this description. The commit carriesAssisted-by:trailers for both.