Google token invalid_grant: the Claude Code skill I built to check token expiry
On July 6 I tried to get my investment spreadsheet updating again, and the Google sign-in token I reached for failed with one word: invalid_grant. Nothing had warned me it was dead. That same day I had Claude Code build a small skill that lists every API token I use, checks which ones are about to expire, and messages me before they do. Two weeks later a second Google token died the same way. That was when I learned about Google's 7-day limit for apps left in "Testing".
I'm not a developer. I describe what I want, and Claude Code writes and runs the code. So I don't notice a dying token by reading code. I notice when something stops.
Contents
The first failure: a refresh token and no warning
The spreadsheet's automatic updates had stopped after June 5. My daily script was only updating files on my PC and never wrote to the sheet. When I tried to connect it, the Google OAuth token failed. OAuth gives you a short-lived access token and a longer-lived refresh token that gets new access tokens. As long as the refresh token works, you never see a login screen again. If Google rejects the refresh token with invalid_grant, the script can't get a new access token.
I didn't know which of my scripts relied on which token, or when any of them would expire. They were spread across Google, Kakao, my brokerage, Notion, Telegram bots, Figma and others, stored in different places, each with its own rules.
That day I connected the spreadsheet through a Google service account instead, which uses a key file instead of a user login and doesn't expire the same way. Then I asked for a way to check which tokens were about to expire.
What the checker is
It's a Claude Code skill called token-guardian, plus one Python script and one JSON file.
The JSON file is a list of my tokens, with no secrets in it. By October 19 it had 15 entries (one entry covers two Telegram bots). Each entry says what the token is, where it lives, when it expires if I know, how to renew it, and which of four groups it belongs to:
| Group | Meaning | Example from my list |
|---|---|---|
| A | Renews itself, the script can refresh it | Brokerage access tokens (about 6.8 hours left when I checked today) |
| B | Needs me to sign in again | Kakao refresh token, Search Console token |
| C | Needs me to issue a new one by hand | Figma personal key |
| Retired | Kept only as history | A work Tableau key I lost access to |
The script reports each entry as red (expired or dead), yellow (expiring soon), black (retired) or green, based on that entry's check. Some entries are informational only, so green doesn't always mean the token was tested. It can also refresh the group A tokens, print renewal steps for B and C, and send me a summary on KakaoTalk.
Two kinds of reminders:
- On the 1st of every month at 9:00, a Windows scheduled task runs a full scan and sends me a KakaoTalk summary, even when everything is green. I wanted proof it ran, not just silence.
- Calendar reminders. A calendar-sync step creates reminders scheduled for seven days before known expiry dates. When I set it up, that meant October 6 for a Figma key expiring October 13, and August 7 for a Kakao token I expected to expire around August 14.
I keep my token list in a Notion page too. The script never edits that page on its own. It only shows me what it would change, and I approve the edit in a session.
The second failure: Google's 7-day Testing limit
On July 19 my Search Console reports failed with the same invalid_grant. This token was only 8 days old. I had issued it on July 11.
The cause was a setting I didn't know mattered. My Google Cloud OAuth app was still in "Testing" status. For an external app in Testing, refresh tokens expire after 7 days unless the app only asks for basic identity information. My Search Console app asked for more than that. I signed in again twice (once before and once after the fix), and published the app from Testing to Production. After that, the token stopped expiring weekly.
Then I added the Search Console token to the checker. Instead of trusting a date, the checker actually tries a refresh with it. If Google refuses, the token shows red. This morning it showed "refresh succeeded."
If you made a Google Cloud project for a personal script and your token dies every week, check the OAuth consent screen's publishing status first.
A recorded expiry date needs checking
I ran a scan on October 19 while writing this. It showed 2 reds, 12 greens and 1 retired:
- Figma personal key: expired 6 days ago. That one is real. I need to issue a new key.
- Kakao refresh token: "expired 66 days ago."
I doubt the Kakao one. The same morning, two KakaoTalk messages from my own system went out with a success response. Those logs don't prove the refresh token itself was used, so I still have to check.
The reason is in how I recorded it. Kakao's API only tells you the exact refresh token expiry when there's less than a month left. Before that, I estimated August 14 and typed it in as a fixed date. My list never changed after that, so the checker kept counting from the old guess.
The Search Console entry doesn't have that problem because it tests the token directly. I want the Kakao entry to work the same way.
What I'd copy if you have more than a few tokens
- Write the list down, without the secrets. Name, where it's stored, expiry if known, how to renew.
- Test the token when you can. If a token can be tested with a cheap call, use that to check whether it still works. A recorded expiry date can become outdated.
- Have the monthly scan send a message even when nothing is red. Mine reports even when all is green. If that message doesn't arrive, I check whether the task ran and whether the message was delivered.
- Check Google's publishing status. A personal OAuth app left in Testing can give you 7-day refresh tokens.
My earlier token mistake was smaller. In May, a refresh error left my daily balance check without data for 3 days (the post about that job). That earlier mistake involved token refresh too. In July I built a list to track all of them.
FAQ
What does invalid_grant mean on a Google refresh token?
Google refused the refresh token. In my July 19 case, the cause was my OAuth app's Testing status, where refresh tokens last 7 days. My notes don't say why the first one failed.
How do I stop a Google OAuth refresh token from expiring every 7 days?
In my case, publishing the OAuth app from Testing to Production fixed it. After that I signed in one more time to get a new token.
What is a Claude Code skill?
A set of instructions and scripts that Claude Code loads when a task matches. Mine is triggered by phrases like "check my tokens" or by invalid_grant errors, and it runs the scan script.
Why not just set calendar reminders for every token?
I do, for tokens with a known date. But some tokens don't publish their expiry until late, and some die early. Testing the token itself can detect these failures even when a calendar reminder isn't due yet.
Comments
Post a Comment