Toby Allen

Adversarial review, real bugs, and dead ends: building an OpenFGA model with Claude Code

· Auth0, Customer Identity Products, Auth0 FGA, AI-Assisted Development

The five posts before this one came out of a model built almost entirely inside Claude Code, with the actual authoring done against OpenFGA's own tooling rather than by hand-writing DSL and hoping. This post is a different kind of post to the rest of the series - not the model, but how it got built, where an adversarial review across two frontier models earned its keep, and where it didn't.

Modelling with the actual tooling, not just a text editor

OpenFGA publishes a skill for coding agents that encodes its own modelling conventions - roles versus can_* permissions, concentric role ordering, how to propagate a parent role via X from service rather than duplicating it. Installing it and the fga CLI changed the shape of the first draft immediately, which is exactly the point of using an authoritative source instead of pattern-matching from memory. It also meant every claim this series makes about the model - "this check returns true", "this tuple is rejected" - is something I actually ran, not something that merely reads plausibly.

I also compared the finished model against openfga/sample-stores, a collection of roughly forty reference models. None of them model delegation, a person-versus-login split, or a business acting on someone else's behalf - they're single-organisation role hierarchies, which this model deliberately isn't - but two specific choices were worth adopting a stance on rather than ignoring. Their banking store's delegate relation is unconditioned and reaches transfer authority, gated by a spend-limit policy attached once per role at the bank level; this model's delegate is always time-bounded and always view-only, with a spend cap as its own separate relation. Neither is wrong. They're different defaults for different problems, and having a real point of comparison is what made that distinction worth stating plainly rather than assuming it was obvious.

What the adversarial review actually caught

Before extending the model further, I ran an adversarial review: two independent frontier models, briefed to find real flaws rather than validate the design, given the full model and every worked example and asked to trace the actual relation chains rather than take the accompanying prose on faith.

The single best finding wasn't a modelling opinion, it was a genuine OpenFGA semantics bug, and it's worth detailing because it's the kind of thing that's easy to get wrong and hard to notice you've gotten wrong. An early version tried to gate a company-director permission on identity verification by chaining three hops: business to login, login to person, person to a verification fact. It validated cleanly and returned false for a login that genuinely was verified. The reason: OpenFGA's X from Y traversal keeps the calling user constant through every hop - it only ever changes which object is being checked, never who's asking. A two-hop chain works because it collapses into a check against the calling login itself. A third hop needs a tuple where that login is literally the holder of a fact stored on a different object entirely, which never exists if the fact was written the other way around. The fix was to attach verification directly to the login rather than the person behind it - which turned out to be more accurate anyway, since a specific credential in a specific relying-party context is what actually gets proofed, not an abstract person record.

The same review also produced two confident, wrong claims, and reporting only the hit rate without the misses would be a dishonest summary of what adversarial review is actually good for. One model asserted that a check with a missing required condition parameter would silently default to zero and pass - I wrote a two-line scratch test and watched OpenFGA raise an explicit error instead, which a properly fail-closed application denies on, not grants. Both models also flagged the published test counts as wrong, computing a lower total - they'd both been given a context bundle that accidentally omitted one test file, an error in how I'd prepared their input rather than a real discrepancy, caught only by re-running the actual test suite rather than trusting either model's arithmetic.

A tooling dead end worth mentioning

Claude Code's built-in facility for spawning several sub-agents in parallel turned out to be restricted, in this environment, to a small allow-list that didn't include the model I was running the primary session on - every spawn attempt failed with the same permission error regardless of which model alias I asked for. The fix was mechanically simple once identified: call the underlying model APIs directly rather than going through the orchestration layer, which had the incidental benefit of letting me pick genuinely different model families for the two review passes rather than several instances of the same one. Two independent architectures disagreeing with each other is a better adversarial signal than the same model disagreeing with itself, so the workaround ended up being the better approach anyway.

Final Thoughts

The pattern that held throughout - verify a claim against the real system before it goes in a sentence anywhere - is the same discipline this whole series has tried to apply to the model itself, and it's the reason I'm comfortable publishing the earlier posts' specific claims about what OpenFGA does and doesn't do. The repository has the full model, all twelve worked verticals, and the tests behind every check mentioned across this series, if you'd rather read the DSL than take my word for any of it.