[BUG] /goal Stop hook is skipped while a background task is live and never re-evaluated when it ends, leaving the session idle indefinitely

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 2, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

/goal <condition> installs a session-scoped Stop hook that is supposed to block stopping until the
condition holds. In practice it is only invoked on stops where the session has zero live background
tasks
. If any background task is registered as running when the turn ends, the goal hook does not run,
the stop is not blocked, and the session goes idle.

The damaging half is what happens next: when those background tasks later terminate, the goal is never
re-evaluated
. Their completion notifications are recorded, but produce no stop and no turn, so nothing
re-checks the goal. The session stays idle until a human types something.

In the session below this cost 4 hours 6 minutes of dead time on an autonomous run that was supposed
to keep working unattended. The failure is silent — nothing in the UI indicates the goal has stopped
being enforced.

Evidence across all 28 stops in that session, cross-referenced against how many background tasks were
live at each:

| Goal hook | Stops | Live background tasks |
| --- | --- | --- |
| ran | 4 | 0 on every one |
| skipped | 24 | ≥1 on all but one |

The four stops where the goal hook ran are the only four with nothing running. A time-based cooldown is
ruled out: stops at 22:43, 23:04, 23:32 and 23:55 were all more than 40 minutes after the previous goal
evaluation at 22:19 and still skipped it — every one of them had live background work.

The one apparent exception is the stop that stranded the session, and only because of where the evidence
lives: two background shells were live, but they had been launched by subagents, whose tool calls are
written to subagents/agent-*.jsonl rather than the main transcript. The UI showed them correctly
("· 2 shells still running"); only a main-transcript reconstruction misses them.

What Should Happen?

Either:

  1. The goal hook is evaluated on every stop, including stops with live background work; or
  2. If skipping is deliberate — the session expects to be re-invoked when the background task notifies —

then the goal is re-evaluated when that task reaches a terminal state, so the skip is a deferral
rather than a permanent loss.

Today it is neither: the evaluation is skipped and never retried. A single background task that ends
without waking the loop permanently disarms the goal for the rest of the session.

At minimum, if a goal is armed and a stop is allowed anyway, that should be surfaced. A silent disarm is
what turned this into hours of dead time instead of a five-second correction.

Error Messages/Logs

# stop_hook_summary recorded at 01:40:20 — the turn that went idle.
# Only the unrelated local notification hook ran; the goal hook is absent.

{
  "subtype": "stop_hook_summary",
  "hookCount": 1,
  "hookInfos": [ { "command": "~/.claude/hooks/stop-notify.sh", "durationMs": 146 } ],
  "hookErrors": [],
  "preventedContinuation": false,
  "stopReason": "",
  "version": "2.1.220"
}

# The goal registered earlier in the same session (condition redacted):
"A session-scoped Stop hook is now active with condition: \"<redacted>\"."

# The two background shells that were live at that stop, both launched by subagents.
# Each was killed ~60 minutes after launch; neither kill produced a stop or a turn.

<task-notification>
<task-id>bg1s6uljs</task-id>
<status>killed</status>
<summary>Background command "Wait for hygiene agent completion" was stopped</summary>
</task-notification>

<task-notification>
<task-id>bfgjwpzkc</task-id>
<status>killed</status>
<summary>Background command "Wait for the workspace test to finish" was stopped</summary>
</task-notification>

# No stop_hook_summary of any kind exists between 01:40:20 and 05:48:24.

Steps to Reproduce

  1. Start a session and run /goal <a condition that is not yet true>. Confirm the acknowledgement

("A session-scoped Stop hook is now active with condition: …").

  1. Have the model launch a background task that outlives the turn — e.g. Bash with

run_in_background: true running a poll loop with no timeout:

``bash
until [ -s /path/that/never/appears ]; do sleep 30; done
``

In the observed case these were launched by subagents, but a directly-launched background command
should reproduce it just as well.

  1. Let the model end its turn while that background task is still registered as running.
  2. Observe: the goal hook does not run. In the transcript, the stop_hook_summary for that turn has

hookCount: 1 (or 0 if no other Stop hook is configured), does not list the goal condition, and has
preventedContinuation: false. The turn ends.

  1. Wait for the background task to finish or be killed.
  2. Observe: its <task-notification> is recorded, but no stop fires and no turn starts. The session

stays idle indefinitely until a human types something.

Configuring any second Stop hook (a one-line script) makes step 4 much easier to see, since the goal
hook's presence or absence in hookInfos becomes visible by contrast.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.1.220 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Timeline

| Time | Event |
| --- | --- |
| 01:20:55 | Subagent A launches background poll loop bg1s6uljs ("Wait for hygiene agent completion"), run_in_background: true, no timeout |
| 01:22:11 | Subagent A itself reports completed — 76 s later. Its watchdog keeps running |
| 01:27:15 | Subagent B launches background poll loop bfgjwpzkc ("Wait for the workspace test to finish"), same shape |
| 01:32:50 | Subagent B reports completed. Its watchdog keeps running |
| 01:40:20 | Turn ends. hookCount: 1, goal hook skipped, preventedContinuation: false |
| 02:20:57 | bg1s6uljsstatus: killed (60 min 2 s after launch) |
| 02:27:15 | bfgjwpzkcstatus: killed (exactly 60 min after launch) |
| — | No stop_hook_summary, no assistant turn, between 01:40:20 and 05:48:24 |
| 05:46:03 | Human types continue; work resumes normally |

Contributing factor — possibly worth its own issue

Background commands launched by a subagent outlive the subagent. Both shells here were still
registered as live long after their parent agents had reported completed — 58 and 54 minutes
respectively — and were cleared only by what looks like a 60-minute cap on background commands (both
killed within 2 seconds of their sixtieth minute).

Whether or not the /goal behaviour changes, a background command whose owning subagent has finished
seems like it should be reaped with its parent rather than continuing to count as live session work.
Filed here as context rather than as a second request, per the single-bug-per-report rule.

Suggested fixes

  1. Re-evaluate the goal condition whenever a background task reaches a terminal state

(completed / failed / killed) and no further tasks are live. This alone closes the hole.

  1. Alternatively, evaluate the goal hook on every stop regardless of background state.
  2. Reap subagent-owned background commands when their owning subagent completes.
  3. Surface the skip in the UI when a goal is armed and a stop is allowed anyway.

How this evidence was gathered

All of it comes from the session's own JSONL transcript plus the per-subagent transcripts under
~/.claude/projects/<project>/<session>/subagents/: stop_hook_summary rows for hook invocations,
<task-notification> blocks for task lifecycle, and tool_use records for the launches. Project paths,
ticket identifiers and internal names are redacted; nothing else has been altered.

One caveat, stated plainly

The mechanism claim — "skipped because background work was live" — is inference from a 27-of-28
correlation, not from reading the implementation. The correlation is shown above so it can be confirmed
or corrected against the actual gating logic.

Similar tickets

Directly relevant

https://github.com/anthropics/claude-code/issues/82546
[BUG] /goal set at a compact_boundary never starts its turn; session idles silently for hours with "active" still displayed
Open · 2026-07-30 · no labels · 1 comment · reported on 2.1.220
Same symptom as this. The comment names a second trigger — end_turn mid-work without compaction — and concludes "the goal loop not re-arming after an end_turn it should have overridden."

https://github.com/anthropics/claude-code/issues/59584
[DOCS] /goal docs do not explain evaluator waits for background shells or delegated subagents
Open · 2026-05-16 · labels: documentation, enhancement, area:docs · 3 comments
Says the wait is deliberate, citing changelog v2.1.143 "Fixed /goal evaluator firing while background shells or delegated subagents are still running." I could not verify that line — the public CHANGELOG.md retains only back to 2.1.178.

Same feature, different failure

https://github.com/anthropics/claude-code/issues/73674
/goal completion Stop hook re-matches phrases from earlier in the transcript forever, preventing clean session termination
Open · 2026-07-03 · labels: bug, area:hooks, platform:macos · 0 comments

https://github.com/anthropics/claude-code/issues/78121
Stop hook re-fires despite stop_hook_active: true, causing /goal loop to spin after completion
Open · 2026-07-16 · labels: bug, has repro, area:hooks

The rest of the /goal Stop hook family

Titles below come from the search listing rather than fetching each issue, so they may be lightly abbreviated. None match this bug — they're evaluator prompt-size, output-format and loop problems.

Open:

Closed:

Background-work adjacent, not /goal

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗