CLAUDE LABJP
VERSION — v2.1.227 landed on August 10 with no new features, just fixes around plan detection and CI behaviorAUTO — Two days remain until August 14, when auto mode becomes the default in Claude Code for Pro, Max, and TeamCI — Bash commands no longer fail across the board under claude-code-action with allowed_non_write_users on GitHub-hosted runnersBILLING — Sessions started with an expired token could misread your plan and nudge Max users toward usage credits; that is now fixedSUNSET — The legacy Workbench and the experimental prompt tool APIs retire on August 17, five days outPRICE — Sonnet 5 promo pricing at $2/$10 per Mtok runs through August 31, moving to $3/$15 on September 1VERSION — v2.1.227 landed on August 10 with no new features, just fixes around plan detection and CI behaviorAUTO — Two days remain until August 14, when auto mode becomes the default in Claude Code for Pro, Max, and TeamCI — Bash commands no longer fail across the board under claude-code-action with allowed_non_write_users on GitHub-hosted runnersBILLING — Sessions started with an expired token could misread your plan and nudge Max users toward usage credits; that is now fixedSUNSET — The legacy Workbench and the experimental prompt tool APIs retire on August 17, five days outPRICE — Sonnet 5 promo pricing at $2/$10 per Mtok runs through August 31, moving to $3/$15 on September 1
Articles/Claude Code
Claude Code/2026-06-27Advanced

When an OAuth Token Expires, Your Unattended Run Has Nowhere to Go — A Token-Lifecycle Design That Keeps Remote MCP Alive

Remote MCP connectors are authorized via OAuth, but access tokens are short-lived. Interactive sessions can re-authorize in a browser; an unattended scheduled run has nobody to click the dialog. Here is a token-lifecycle design that owns expiry and refreshes ahead of time.

Claude Code216MCP50OAuth4Authentication2Indie Development8

Premium Article

A job that was supposed to run at 2 a.m. has produced nothing by morning. Tracing the log, a single remote MCP tool call failed, and the cause was an expired access token. When you work interactively, an expired token is invisible: an authorization dialog appears, you click a button in the browser once, and everything continues. But an unattended scheduled run has nobody to click that button.

As an indie developer, I run automated posting across several sites for Dolice Labs, and more than once a freshly connected remote MCP connector worked fine, only to stall quietly some tens of hours later. The connector configuration itself was correct. What had broken was the one thing nobody was managing: the token's lifespan.

This article lays out, with working code, how to hold a remote MCP connector's OAuth tokens as state that you own — refreshing them ahead of time, both before and during a run — so your unattended jobs keep moving.

Why token expiry is invisible during interactive use

An OAuth access token is usually short-lived, expiring in anywhere from tens of minutes to a few hours. The refresh token, by contrast, is long-lived, and you can use it to obtain a new access token without a human present.

Interactive clients do this renewal silently behind the scenes. If the access token has expired, they quietly trade the refresh token for a new one; if that has also expired, they open a browser and ask the human, "Authorize?" In other words, the assumption that "a human can always click the authorize button" is exactly what turns expiry into an invisible problem.

That assumption collapses under unattended execution. Opening a browser does nothing because there is no one to click, so the moment both the access token and the refresh token expire, the job can no longer move forward. Worse, expiry comes back as a 401, which the MCP client layer tends to flatten into a generic tool error. All you see is "the tool failed," and it takes a while to realize the real reason was "authorization expired."

So for unattended operation, you cannot leave renewal to the client. You have to hold the lifecycle yourself. Let's build it up step by step.

Hold the token as state with an expiry

The first step is to persist the access token, refresh token, and expiry time together. Without an expiry time you have no idea when it will lapse, which makes refreshing ahead of time impossible.

import json
import os
import time
import tempfile
from dataclasses import dataclass, asdict
from pathlib import Path
 
@dataclass
class TokenSet:
    access_token: str
    refresh_token: str
    expires_at: float  # UNIX seconds: the absolute time the access token expires
 
    def expires_in(self) -> float:
        return self.expires_at - time.time()
 
class TokenStore:
    """Persists the token set as JSON. Writes are atomic."""
 
    def __init__(self, path: str):
        self.path = Path(path)
 
    def load(self) -> TokenSet:
        data = json.loads(self.path.read_text(encoding="utf-8"))
        return TokenSet(**data)
 
    def save(self, tokens: TokenSet) -> None:
        # Write to a temp file in the same directory, then rename.
        # If the process dies mid-write, we never overwrite with a partial file.
        d = self.path.parent
        d.mkdir(parents=True, exist_ok=True)
        fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as f:
                json.dump(asdict(tokens), f)
            os.replace(tmp, self.path)
            os.chmod(self.path, 0o600)  # only the owner may read the token file
        finally:
            if os.path.exists(tmp):
                os.unlink(tmp)

The quietly important part is that the save is atomic (temp file plus rename). If the process dies while saving tokens, a half-written file is left behind and the next load fails. Renewal happens on every unattended run, so a break here becomes a breeding ground for silent stalls. The chmod 0o600 narrows the file's permissions because tokens are secrets.

Token endpoints usually return expires_in (seconds remaining) rather than expires_at (an absolute time), so convert it to an absolute time with time.time() + expires_in the instant you receive it and store that. Keeping seconds-remaining forces you to separately remember when you fetched it, which is a source of drift.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Owning the access token's expiry yourself and refreshing ahead of time using a skew window
A token store resilient to refresh-token rotation invalidation and clock skew
A retreat design that fails loudly with a structured skip instead of stalling silently
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Claude Code2026-04-26
Untangling Claude Code's 'Authorization Failed' Error — OAuth, MCP, and Dynamic Client Registration
Why Claude Code suddenly throws 'authorization failed' or 'incompatible auth server: does not support dynamic client registration', and what to actually do about it. A practical walkthrough of OAuth, MCP server requirements, and the real fixes that work in production.
Claude Code2026-08-09
One Transient 401 Replaced My Long-Lived Token — Giving Credentials a Provenance Field
A shared credential file can lose its long-lived token to a single transient 401. I injected exactly one 401 into a 24-worker fleet, measured the blast radius, and compared a provenance guard against full isolation.
Claude Code2026-07-29
The MCP Server Connects Fine and Still Hands Back Zero Tools — Catching Silent Capability Drift with a Tool Manifest Diff
A missing credential does not break the MCP handshake. initialize succeeds, tools/list succeeds, and the array comes back empty. Here is the measured behaviour, and a preflight that locks the expected tool surface and fails closed before the agent ever starts.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →