Yeah, you're right.
The hook does fix one thing — without it the whole method body reruns, so you charge the card again on every single retry. But that's all it fixes. It gets you one send per commit, not one send ever, and I wrote that section like it was the whole answer.
The rest is exactly as you describe. Once COMMIT returns the row is there whether or not the hook works. Hooks in this library log and swallow errors on purpose — the transaction already committed, so throwing at the caller would be lying about what happened — which gets you precisely the ledger-says-paid-with-no-charge case. And retrying by hand is its own trap, because a call that timed out might have gone through anyway and you can't tell from the outside.
So yeah: outbox row in the same transaction, drain it after commit, idempotency key on the send. The hook just makes the drain happen sooner instead of waiting for the next poll. Stripe takes an Idempotency-Key header which makes that part easy.
The docs did mention the outbox, but buried it in a section about hook scoping, and it never made it into the post at all. My fault. I've added it to both.