The task exists only if the hook lets it
You want to vet a task before it lands in the list. TaskCreated runs while the TaskCreate tool is creating a task, on every call, whenever Task tools are available. There is no matcher. The payload carries task_id, task_subject, an optional task_description, an optional teammate_name, and a deprecated optional team_name.
Two ways to cancel, and one that does nothing
Exit 2 deletes the task and returns stderr to Claude as the tool error. A top-level decision: "block" with reason cancels it and hands Claude the reason. Exit 0 and exit 1 create the task unless that JSON blocks it.
The shared continue: false field, which stops the loop on other events, is ignored here. The hooks reference says so in as many words.
You return {"continue": false, "stopReason": "no tasks during a release freeze"} to stop a task being created.
The task is created. The field is ignored on this event and nothing says so. Your reason goes nowhere, and Claude proceeds with a task you thought you had refused.
Return {"decision": "block", "reason": "no tasks during a release freeze"} instead, or exit 2 with the reason on stderr.