A friend once suggested that I markup ledges to decide which ones should be grabbable, such as putting an Area2D
on each ledge and responding when the player enters it. But that seemed like way too much work. I ended up dynamically detecting ledges with raycast sweeps. This is where you move a raycast in increments to determine where a collision starts or stops.
I put my raycast origin at my player's chest facing towards the travel direction. When the player is in the air I check that raycast to see if the player is within range of a wall. If so I start the sweep, moving the raycast up 4 pixels at a time still facing the travel direction. The first iteration that gets no collision tells me that I have a ledge within those last 4 pixels.
Once I find a ledge I do a finer 1 pixel sweep to get the exact ledge position. I then transition to a "ledge" state where my player grabs the ledge and can then pull themselves up.