The refusal already happened
By the time your hook runs, the call is already blocked. Auto mode is the permission mode where a classifier, an automatic judge of allow or refuse, reviews each action instead of you. Module 11 covers the modes. When that classifier refuses a tool call, PermissionDenied fires, after the refusal. It does not fire for a denial you gave at a prompt, a denial a PreToolUse hook issued, or a call that matched a deny rule.
The matcher filters tool names and the if filter applies. The payload carries permission_mode, tool_name, tool_input, tool_use_id, and reason, the text explaining why auto mode refused.
One field, and when it is ignored
The only lever is hookSpecificOutput.retry: true. It tells the model it may try the call again. It does not reverse the denial. When the classifier produced no verdict, the field is ignored and the original rejection stands. retry: false, or no JSON, leaves the rejection message as it was.
Exit codes here decide nothing
Do not reach for exit 2 here. Exit 0 leaves the denial in place, and valid JSON may add the retry hint. Exit 1 is the same, because the hooks reference reads JSON on every code. Exit 2 and its stderr are ignored outright. The denial already occurred, and there is no code that un-occurs it.
You watch a denial, decide the classifier got it wrong, and return {"hookSpecificOutput": {"retry": true}} expecting the call to go through.
It does not. The denial stands. The field tells Claude it may try again, and it does nothing to make the second attempt succeed. Then the narrower case: some denials carry no classifier verdict, and on those retry is ignored outright. The same hook returning the same JSON has no effect, and nothing says so.
To let the call through, the mechanism is a permission rule in your settings, which module 11 covers. This event sits after the decision, and there is no field on it that changes the decision.