Skip to content

fix(cline): stop unrelated prose from suppressing the hook command note - #4150

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/cline-hook-note-per-instruction
Open

fix(cline): stop unrelated prose from suppressing the hook command note#4150
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/cline-hook-note-per-instruction

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

ClineIntegration._inject_hook_command_note guards idempotency with a whole-document substring scan:

if "replace dots" in content:
    return content

The shared SkillsIntegration._inject_hook_command_note helper (base.py:1637-1642) instead compares only the line immediately above each match:

previous_lines = content[:m.start()].splitlines()
if previous_lines and previous_lines[-1] == indent + note:
    return m.group(0)

Cline never picked up that change.

Reproduction on current main

A. unrelated prose -> note injected? False      (base: True)
B. two hook sections, one already noted -> notes: 1   (want 2)
C. indented instruction after a blank line:
   '## Hooks\n\n  <note>\n\n  - For each executable hook, ...'   <-- spurious blank line

(a) Any command or extension markdown whose prose happens to contain the phrase "replace dots" — e.g. "When normalizing table names, replace dots with underscores." — loses the note entirely. The generated workflow then tells the agent to emit /speckit.git.commit, a dotted command Cline never registers (Cline installs speckit-git-commit.md). The note exists precisely to prevent that.

(b) A document with one already-noted hook section never gets a note on a second, un-noted one — exactly the case the base helper was changed to handle.

(c) Secondary: cline's regex captures indentation with ^(\s*) where the base uses ^([ \t]*). Because \s matches newlines, the captured "indent" can swallow a preceding blank line, which is then re-emitted between the note and the instruction. The per-instruction check cannot line up until this is fixed, so both belong in one change.

Fix

Adopt the base helper's per-instruction check and its [ \t]* capture. After the fix:

A. unrelated prose -> note injected? True
B. two sections -> notes: 2 (want 2)
C. indented+blank -> note sits directly above the instruction, indentation preserved
D. idempotent -> True

Verification

  • Fail-before / pass-after: 3 new-vs-baseline failures with the source reverted → 35 passed with the fix.
  • Idempotency is asserted explicitly (re-running on already-noted content is a no-op) — the property the old guard was there to provide is kept.
  • Full tests/integrations scope: 18 failed, 2627 passed — identical to the clean-main baseline's 18 (all Windows symlink-privilege), plus my 3 new tests.
  • uvx ruff@0.15.0 check src tests → clean

No breaking change. Content that already has the note above every instruction is returned byte-identical; only documents that were previously missing a note now gain one.


Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current main.

`ClineIntegration._inject_hook_command_note` guarded idempotency with a
whole-document substring scan -- `if "replace dots" in content: return
content` -- instead of the per-instruction check the shared
`SkillsIntegration._inject_hook_command_note` helper uses (base.py:1637-1642),
which compares only the line immediately above each match.

Two consequences, both verified on main:

  A. unrelated prose -> note injected? False      (base: True)
  B. two hook sections, one already noted -> notes: 1   (want 2)

(a) Any command or extension markdown whose prose happens to contain the
phrase "replace dots" loses the note entirely, so the generated workflow
tells the agent to emit `/speckit.git.commit` -- a dotted command Cline never
registers, since Cline installs `speckit-git-commit.md`.

(b) A document with one already-noted hook section never gets a note on a
second, un-noted one -- exactly what the base helper was changed to handle.

Also aligns the capture group with the base helper: `^([ \t]*)` rather than
`^(\s*)`. Because `\s` matches newlines the captured "indent" could swallow a
preceding blank line, which was then re-emitted between the note and the
instruction:

  '## Hooks\n\n  <note>\n\n  - For each executable hook, ...'

The per-instruction check cannot line up until this is fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali requested a review from mnriem as a code owner August 15, 2026 16:56
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.

1 participant