Skip to content

fix: allow Claude to chain skills for hook execution#2227

Open
mnriem wants to merge 4 commits intogithub:mainfrom
mnriem:fix/claude-hook-chaining-2178
Open

fix: allow Claude to chain skills for hook execution#2227
mnriem wants to merge 4 commits intogithub:mainfrom
mnriem:fix/claude-hook-chaining-2178

Conversation

@mnriem
Copy link
Copy Markdown
Collaborator

@mnriem mnriem commented Apr 15, 2026

Summary

Fixes #2178 — Claude Code fails to execute speckit commands because:

  1. disable-model-invocation: true prevents Claude from invoking extension skills (e.g., speckit-git-feature) when chaining from workflow skills
  2. Hook sections reference dot-notation command names (speckit.git.commit) but Claude skills use hyphens (speckit-git-commit)
  3. Unicode in PowerShell auto-commit.ps1 causes parser errors on Windows

Changes

Core fix: disable-model-invocationfalse

  • Removed Claude-specific frontmatter from the shared build_skill_frontmatter() in agents.py
  • Added post_process_skill_content() hook to SkillsIntegration (identity default)
  • ClaudeIntegration overrides it to inject user-invocable: true and disable-model-invocation: false
  • Wired through presets.py and extensions.py so all skill-generation paths go through the integration

Hook command name normalization

  • ClaudeIntegration.post_process_skill_content() injects a dot-to-hyphen note into hook sections of generated SKILL.md files, so Claude maps speckit.git.commit/speckit-git-commit

PowerShell encoding fix

  • Replaced Unicode with ASCII [OK] in both auto-commit.ps1 and auto-commit.sh

Tests

  • 9 new tests for disable-model-invocation (positive: value is false, negative: true never appears, non-Claude agents unaffected)
  • 5 new tests for hook note injection (positive: present in hook skills, negative: absent without hooks, idempotent, preserves indentation)
  • 4 new tests for auto-commit scripts (positive: [OK] in output, negative: no Unicode )

All 1433 tests pass.

- Set disable-model-invocation to false so Claude can invoke extension
  skills (e.g. speckit-git-feature) from within workflow skills
- Inject dot-to-hyphen normalization note into Claude SKILL.md hook
  sections so the model maps extension.yml command names to skill names
- Replace Unicode checkmark with ASCII [OK] in auto-commit scripts to
  fix PowerShell encoding errors on Windows
- Move Claude-specific frontmatter injection to ClaudeIntegration via
  post_process_skill_content() hook on SkillsIntegration, wired through
  presets and extensions managers
- Add positive and negative tests for all changes

Fixes github#2178
Copilot AI review requested due to automatic review settings April 15, 2026 13:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Claude Code failing to execute chained speckit skills/hooks by ensuring generated skills are user-invocable, model-invocation is enabled (for skill chaining), and hook documentation clarifies dot-to-hyphen command normalization; also removes Unicode output that breaks PowerShell parsing on Windows.

Changes:

  • Moves Claude-specific SKILL.md frontmatter mutation into ClaudeIntegration.post_process_skill_content() and routes preset/extension skill generation through integration post-processing.
  • Injects a dot→hyphen normalization note into hook instruction sections in Claude-generated skills.
  • Replaces Unicode with ASCII [OK] in auto-commit scripts and adds/updates tests to lock in behavior.
Show a summary per file
File Description
src/specify_cli/agents.py Removes Claude-only SKILL frontmatter from shared builder to avoid affecting other skill-generation paths.
src/specify_cli/integrations/base.py Adds SkillsIntegration.post_process_skill_content() hook (default identity) for integration-specific skill post-processing.
src/specify_cli/integrations/claude/__init__.py Implements Claude-specific post-processing: injects user-invocable: true, disable-model-invocation: false, and a hook command normalization note.
src/specify_cli/presets.py Applies integration post-processing to preset-created/restored SKILL.md content.
src/specify_cli/extensions.py Applies integration post-processing to extension-created SKILL.md content.
extensions/git/scripts/bash/auto-commit.sh Replaces Unicode checkmark success output with [OK] for portability.
extensions/git/scripts/powershell/auto-commit.ps1 Replaces Unicode checkmark success output with [OK] to avoid Windows PowerShell parser/encoding issues.
tests/integrations/test_integration_claude.py Updates expectations for disable-model-invocation: false and adds coverage for Claude post-processing + hook note injection behavior.
tests/test_extension_skills.py Updates extension skill YAML expectations to match new Claude post-processing (disable-model-invocation: false).
tests/test_presets.py Updates preset skill assertions to expect disable-model-invocation: false.
tests/extensions/git/test_git_extension.py Adds tests ensuring [OK] is used and Unicode is absent in script outputs.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 11/11 changed files
  • Comments generated: 3

Comment on lines +171 to +173
return re.sub(
r"(?m)^(\s*)(- For each executable hook, output the following)",
lambda m: m.group(1) + _HOOK_COMMAND_NOTE.rstrip("\n") + "\n" + m.group(0),
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_inject_hook_command_note() always inserts a \n newline. Elsewhere in this module the injection helpers preserve the existing EOL style (\n vs \r\n), so this can introduce mixed line endings if a SKILL.md uses CRLF. Consider preserving the existing line-ending style when inserting the hook note for consistency.

Suggested change
return re.sub(
r"(?m)^(\s*)(- For each executable hook, output the following)",
lambda m: m.group(1) + _HOOK_COMMAND_NOTE.rstrip("\n") + "\n" + m.group(0),
def repl(m: re.Match[str]) -> str:
indent = m.group(1)
instruction = m.group(2)
eol = m.group(3)
return (
indent
+ _HOOK_COMMAND_NOTE.rstrip("\n")
+ eol
+ indent
+ instruction
+ eol
)
return re.sub(
r"(?m)^(\s*)(- For each executable hook, output the following)(\r\n|\n|$)",
repl,

Copilot uses AI. Check for mistakes.
Comment on lines +802 to +812
@staticmethod
def _post_process_skill(agent_key: str, content: str) -> str:
"""Delegate to the integration's post_process_skill_content if available."""
if not isinstance(agent_key, str) or not agent_key:
return content
from specify_cli.integrations import get_integration

integration = get_integration(agent_key)
if integration is not None and hasattr(integration, "post_process_skill_content"):
return integration.post_process_skill_content(content)
return content
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_post_process_skill() is duplicated here and in specify_cli/extensions.py. This duplication increases the risk of future drift (e.g., changing integration lookup or error handling in one place but not the other). Consider extracting a shared helper (module-level function) and reusing it in both call sites.

Copilot uses AI. Check for mistakes.
Comment on lines +869 to +879
@staticmethod
def _post_process_skill(agent_key: str, content: str) -> str:
"""Delegate to the integration's post_process_skill_content if available."""
if not isinstance(agent_key, str) or not agent_key:
return content
from specify_cli.integrations import get_integration

integration = get_integration(agent_key)
if integration is not None and hasattr(integration, "post_process_skill_content"):
return integration.post_process_skill_content(content)
return content
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_post_process_skill() is duplicated here and in specify_cli/presets.py. To reduce maintenance overhead and avoid inconsistencies, consider consolidating this into a single shared helper used by both extension and preset skill generators.

Copilot uses AI. Check for mistakes.
@mnriem
Copy link
Copy Markdown
Collaborator Author

mnriem commented Apr 15, 2026

@copilot apply changes based on the comments in this thread

@mnriem mnriem self-assigned this Apr 15, 2026
mnriem added 2 commits April 15, 2026 09:29
- Preserve line-ending style (CRLF/LF) in _inject_hook_command_note
  instead of always inserting \n, matching the convention used by other
  injection helpers in the same module.

- Extract duplicated _post_process_skill() from extensions.py and
  presets.py into a shared post_process_skill() function in agents.py.
  Both modules now import and call the shared helper.
The regex in _inject_hook_command_note only matched lines ending
immediately after 'output the following', but the actual template
lines continue with 'based on its `optional` flag:'. Use [^\r\n]*
to capture the rest of the line before the EOL.
Copilot AI review requested due to automatic review settings April 15, 2026 14:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Claude Code hook execution for Speckit by ensuring Claude-generated skills are invocable/chained, normalizing hook command naming expectations, and removing a Unicode character that breaks PowerShell parsing on Windows.

Changes:

  • Move Claude-specific SKILL.md frontmatter behavior out of the shared build_skill_frontmatter() and into Claude’s integration post-processing (sets disable-model-invocation: false, user-invocable: true).
  • Post-process SKILL.md content during preset/extension skill generation so Claude flags (and hook note) apply consistently across generation paths.
  • Replace Unicode with ASCII [OK] in auto-commit scripts and add tests to prevent regressions.
Show a summary per file
File Description
src/specify_cli/agents.py Adds post_process_skill() helper to route skill content through the selected integration’s post-processor.
src/specify_cli/integrations/base.py Introduces post_process_skill_content() hook (default no-op) for skills integrations.
src/specify_cli/integrations/claude/__init__.py Implements Claude-specific post-processing: injects user-invocable: true, disable-model-invocation: false, and a dot→hyphen hook command note.
src/specify_cli/presets.py Applies integration post-processing to generated/restored preset SKILL.md files.
src/specify_cli/extensions.py Applies integration post-processing to generated extension SKILL.md files.
extensions/git/scripts/bash/auto-commit.sh Replaces Unicode checkmark with [OK] in success output.
extensions/git/scripts/powershell/auto-commit.ps1 Replaces Unicode checkmark with [OK] in success output (avoids Windows PowerShell parsing issues).
tests/integrations/test_integration_claude.py Expands tests for Claude frontmatter flags and hook-note injection behavior.
tests/test_extension_skills.py Updates expectation for Claude-generated extension SKILL.md to have disable-model-invocation: false.
tests/test_presets.py Updates expectations for preset SKILL.md to have disable-model-invocation: false.
tests/extensions/git/test_git_extension.py Adds tests asserting [OK] output and absence of Unicode checkmark for both bash and PowerShell scripts.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new

Instead of a free function in agents.py that re-resolves the
integration by key, callers in extensions.py and presets.py now
resolve the integration once via get_integration() and call
integration.post_process_skill_content() directly. The base
identity method lives on SkillsIntegration.
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.

[Bug]: Claude dont execute speckit commands

2 participants