/goal completion Stop hook re-matches phrases from earlier in the transcript forever, preventing clean session termination

Open 💬 0 comments Opened Jul 3, 2026 by geremyturcotte

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 /goal set at session start.
  • Reproduced across several independent worker sessions in our orchestration setup.

Repro (as observed)

  1. Start a session and set a /goal whose completion criterion is textual (e.g. "finished when X is merged and nothing is BLOCKED").
  2. During the session, the model legitimately emits a line containing the trigger word (e.g. a status report: "step 3 BLOCKED on review").
  3. The blocker is later resolved; the model states resolution and stops producing output.
  4. On stop, the goal checker re-matches the OLD "BLOCKED" line from the transcript and refuses completion — indefinitely. The session stays parked in the /goal loop (◎ /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.

View original on GitHub ↗