Two roads to complete
A task gets marked complete two ways, and your hook fires before either one applies. Claude calls TaskUpdate, or a teammate ends its turn with tasks still in progress and Claude Code completes them on the teammate’s behalf. TaskCompleted fires for both. There is no matcher. The payload carries permission_mode, task_id, task_subject, an optional task_description, an optional teammate_name, and a deprecated optional team_name. Nothing in it tells you which of the two roads fired the event.
Exit 2 blocks on either road, the JSON on one
Exit 2 prevents completion and feeds stderr back to the model. That holds for both triggers. Exit 0 and exit 1 mark the task complete unless JSON that applies stops the teammate.
The JSON is the shared continue: false with stopReason, and it stops a teammate entirely, with the reason shown to you, when the teammate’s turn completion triggered the event. When a TaskUpdate call triggered it, the hooks reference says Claude Code ignores the field.
You gate completion with continue: false because it is the field that stopped a teammate on TeammateIdle. It works when a teammate’s turn ends with the task open.
It does nothing when Claude calls TaskUpdate directly. The task is marked complete, nothing reports the ignored field, and the payload never told you which case you were in.
Exit 2 blocks on both triggers. Use it for the gate, and keep continue: false for the case where you mean to stop a teammate.