/goal completion Stop hook re-matches phrases from earlier in the transcript forever, preventing clean session termination
Summary
When a session runs with an active /goal, the goal-completion Stop hook appears to match trigger phrases against the entire cumulative transcript rather than the current state. A phrase emitted once mid-session (e.g. a status line containing "BLOCKED" that was later resolved) keeps re-matching forever: the session can never terminate cleanly and loops on Stop hook feedback: … The transcript contains … even after the condition was resolved by the operator.
Environment
- Claude Code CLI (macOS, Darwin 25.5.0), observed 2026-07-02 on sessions spawned headless (
claude --dangerously-skip-permissions) with a/goalset at session start. - Reproduced across several independent worker sessions in our orchestration setup.
Repro (as observed)
- Start a session and set a
/goalwhose completion criterion is textual (e.g. "finished when X is merged and nothing is BLOCKED"). - During the session, the model legitimately emits a line containing the trigger word (e.g. a status report: "step 3 BLOCKED on review").
- The blocker is later resolved; the model states resolution and stops producing output.
- On stop, the goal checker re-matches the OLD "BLOCKED" line from the transcript and refuses completion — indefinitely. The session stays parked in the
/goalloop (◎ /goal active) with the Stop hook feedback repeating.
Evidence it is not our hooks
We audited every Stop hook in our environment before filing: our two Stop hooks (a heartbeat emitter and a telemetry forwarder) are both fire-and-forget, exit 0, no stdout, no transcript access; no other plugin/settings level (project, user, managed) declares a Stop hook. The observed Stop hook feedback: … transcript contains … text can only come from the native /goal completion checker.
Expected
Goal-completion checking should evaluate the current state (latest turn / a state marker), or at least de-duplicate/discount matches that were superseded later in the transcript — one historical occurrence of a phrase should not permanently block session completion.
Workaround we use
Phrase goal files so the completion/failure criteria are not strings the session will naturally emit mid-run.