How to Auto-Revoke a Claude Agent's Access When a User Is Offboarded With Kinde Webhooks A developer built a demo app using Kinde webhooks and a pre-action authorization check to revoke a Claude agent's access when its user is offboarded mid-task. Testing showed that suspending a user in Kinde does not invalidate an already-issued OAuth access token, so the agent kept working on the task after the suspension. The app closes the gap by having a webhook update the app's own user record and checking that record before every agent tool call. Imagine this scenario, someone on your team gets offboarded while their AI agent is still mid-task. Nobody remembers the agent is even running. It's just doing what it was told, on behalf of someone who, as of a minute ago, doesn't work at your company or on your team anymore. Does it stop? Well, I built a small app to test that scenario, using Kinde https://kinde.com/?utm source=devto&utm medium=content&utm campaign=shola&campaignid=chatgptapp&network=&adgroup=&keyword=&matchtype=&creative=24&device=&adposition= to handle sign-in and to hold the record of who's still active. I signed a real user in, handed their agent a task, and while the agent was still working through it, suspended that same user, in Kinde's dashboard, mid-run, to see what the agent would do next. Unsurprisingly, the agent kept working on the task. You see, suspending or deleting a person changes how Kinde itself sees that user, but it doesn't touch the access token their agent is already holding, because nothing about a suspension reaches back into a token that was already signed and handed out before it happened. The token still verifies exactly as it did before the suspension, so the agent has no way to know anything changed. Everything I am going to talk about in this article is about closing that gap, and about what I actually found while doing it: webhook deliveries measured live, a production bug that could have left an offboarded user's record looking active forever, and a hard number for how long an offboarded person's agent keeps acting before anything catches it. Let's start with what an access token actually is, because the whole gap follows from it. An OAuth access token isn't a receipt you hand back to check against a ledger. It's a signed claim, a small JSON payload with a cryptographic signature attached, and whatever's checking it just verifies that signature against a public key rather than calling home to ask if the token's still good. That's the entire appeal of the design: an API can confirm a token is genuine without a database round trip on every request. Which means suspending a user in Kinde only changes a row in Kinde's own database. It doesn't reach the token at all, because there's nothing there for it to reach: the token was already handed out, already signed, already valid until whatever expiry it was minted with. Revoking it properly would mean tracking every issued token in a lookup table somewhere, which throws away the entire point of signing one in the first place, or it would mean just waiting for the thing to expire on its own. I suspended a signed-in test user mid-session, and the app's own check kept reporting that user's access token as valid, seconds after Kinde had already suspended them. So that gap isn't a bug in Kinde, and it isn't a bug in OAuth either. It's just what a stateless credential is, by design, and the real question is what you build on top of it. Two pieces close the gap. A webhook tells the app when Kinde's view of a user changes, and a check runs before every single agent action, reading the app's own record of that user instead of trusting whatever was true when the session started. The agent itself is a small Claude Messages API loop, working through a closed set of three tools against a demo set of internal resources: list resources , read resource , write resource . None of what follows is specific to what the agent does. It's specific to the one place every tool call has to pass through before it's allowed to run at all. First, we start with the registry that defines those three actions, because it's closed by construction rather than by convention. An action that isn't in this table doesn't half-exist somewhere in the code, waiting to be called by accident. It just doesn't exist: js export const ACTION REGISTRY: Record