The approach that's worked best for me: keep hitbox activation completely out of animation events and drive it off a frame-indexed data asset per attack. Each attack defines active windows as inclusive ranges (active: [[8, 14]]), and a dedicated system evaluates a discrete game-tick counter on every FixedUpdate to decide whether boxes are live.
The critical part is defining "frame" as a game-tick-aligned counter you control, not animation system time and not wall clock time. Once you have that, hitbox data is just data. Editable in a spreadsheet, loadable from JSON, diffable in version control. The animation plays; it doesn't own the gameplay timing. Mixing those concerns is where the mess comes from. Hitbox logic in animation events means gameplay state is embedded in animation assets, which creates a weird ownership problem between animators and combat programmers that nobody ever quite resolves.
