·Vibe Coding·3 min read·Mid-level developers

Vibe Coding Isn't a Skill Issue — It's a Verification Issue

The popular dunk on "vibe coders" is that they can't read code. The real problem is they can't verify it. Two different failures, two very different fixes.

A developer ships a feature in an afternoon using Cursor and Claude, doesn't read most of the code, and it works. Three weeks later it silently corrupts a row of data once a week. Engineering Twitter blames the developer's skill. They're not entirely wrong. They're aiming at the wrong target.

The bug isn't in their reading. It's in their verifying.

Reading vs verifying

Reading code means understanding what it does. Verifying it means confirming what it will do under conditions you care about. Including the ones you didn't think to test. AI tools made reading optional. They did not make verification optional. The skill that's missing isn't "can you read JavaScript." It's "can you design a check that would catch this if it's wrong."

A senior who reads every line but never runs the code in a real edge case is just as exposed. The fix is the same for both. More verification, less trust.

A verification-first workflow

# Vibe-coded: 3 minutes of prompts, looks fine.
def calculate_refund(order):
    days_since_purchase = (now() - order.purchased_at).days
    if days_since_purchase <= 30:
        return order.total
    if days_since_purchase <= 60:
        return order.total * 0.5
    return 0

# Verification (the part most vibe coders skip):
@hypothesis.given(
    purchased_days_ago=hypothesis.integers(min_value=-5, max_value=400),
    total=hypothesis.floats(min_value=0, max_value=10_000, allow_nan=False),
)
def test_refund_invariants(purchased_days_ago, total):
    order = make_order(total=total, purchased_at=now() - days(purchased_days_ago))
    refund = calculate_refund(order)
    assert 0 <= refund <= total           # never refund more than paid
    assert refund == 0 or order.purchased_at <= now()  # no time-travel refunds
    assert isinstance(refund, (int, float))  # never None or NaN

Why this works

The property tests describe what the code must never do. Refund more than paid. Refund a future purchase. Return NaN. The AI didn't think to handle a negative days_since_purchase. The property test catches it on the first run. You didn't need to read the function to know it had a bug. You needed an invariant. Different skill. Same outcome.

When this approach is the right one

Any code you didn't write line by line. AI-generated, copy-pasted, written by a contractor, written by you at 1am. Verification scales independently of how the code was authored. Especially valuable on financial logic, permission checks, anything that touches money or trust.

When traditional review still wins

Code where the intent is the interesting part. Clever algorithms, novel data structures, performance-sensitive code where you need to know if the approach is right, not just if the outputs are valid. Property tests check outputs. Reading checks approach. Both have a job.

The trust shift

flowchart LR
    subgraph Old ["Old workflow"]
        Write[Human writes code] --> Read[Human reads code]
        Read --> Trust[Trust based on reading]
    end
    subgraph New ["Vibe workflow done right"]
        Prompt[Human writes prompt] --> AI[AI writes code]
        AI --> Inv[Human writes invariants]
        Inv --> Test[Property tests run]
        Test --> Trust2[Trust based on verification]
    end

Conclusion

Next time you accept AI-generated code, write the invariants before you write the prompt. Three lines describing what the function must never do is faster than reading 80 lines and missing the one bug that actually matters. Tried it. Recommend it.