Skip to content

best-effort token auto-refresh on expired token - #47

Open
hunandy14 wants to merge 3 commits into
zacdcook:masterfrom
hunandy14:feat/auto-token-refresh
Open

best-effort token auto-refresh on expired token#47
hunandy14 wants to merge 3 commits into
zacdcook:masterfrom
hunandy14:feat/auto-token-refresh

Conversation

@hunandy14

@hunandy14 hunandy14 commented Apr 14, 2026

Copy link
Copy Markdown

Summary

When a request arrives and the OAuth token is expiring (<60s remaining),
the proxy runs claude -p "ping" --max-turns 1 --no-session-persistence
to refresh the token before forwarding.

Best-effort only — if refresh fails, the request proceeds as if this
code did not exist. Triggered on-demand, not by cron or polling.

Why 60 seconds?

Claude CLI only rotates the token when <2 minutes remain
(confirmed by @DBostik in #32).
60 seconds sits inside this window with enough buffer for the ~8s CLI invocation.

Design decisions

  • On-demand, not polling — no timers, no cron, no background threads.
    Refresh only happens when a real request needs it.
  • Best-effort — wrapped in try/catch. Failure is logged and swallowed;
    the original getToken() flow is untouched.
  • DeduplicationrefreshInProgress ensures concurrent requests share
    a single CLI invocation instead of spawning many.
  • CLI-based refresh — delegates to claude -p "ping" rather than
    calling the OAuth endpoint directly. The CLI already handles token
    refresh reliably, so reusing it avoids breakage if Anthropic changes
    the OAuth flow in the future.
  • Minimal diff — ~30 lines added, 1 line changed (() =>async () =>).
    Original request handling logic is not modified.

Verified in production

Proxy ran for ~7 hours, served 78 requests. Token expired naturally,
auto-refreshed in 8 seconds, and continued serving — zero downtime:

Apr 14 03:31:09 claude-proxy node[561265]:   Token expires:     6.8h
Apr 14 03:36:34 claude-proxy node[561265]: [03:36:34] #1 POST /v1/messages (173b -> 456b)
Apr 14 03:36:38 claude-proxy node[561265]: [03:36:34] #1 > 200
   ...
Apr 14 10:03:05 claude-proxy node[561265]: [10:03:05] #78 POST /v1/messages (69766b -> 54141b)
Apr 14 10:03:07 claude-proxy node[561265]: [10:03:05] #78 > 200
Apr 14 10:30:09 claude-proxy node[561265]: [token] Token expired, running claude CLI to refresh...
Apr 14 10:30:17 claude-proxy node[561265]: [token] CLI refresh done
Apr 14 10:30:17 claude-proxy node[561265]: [10:30:17] #79 POST /v1/messages (69632b -> 50029b)
Apr 14 10:30:19 claude-proxy node[561265]: [10:30:17] #79 > 200
Apr 14 10:30:21 claude-proxy node[561265]: [10:30:21] #80 POST /v1/messages (83628b -> 65149b)
Apr 14 10:30:23 claude-proxy node[561265]: [10:30:21] #80 > 200
Apr 14 10:30:31 claude-proxy node[561265]: [10:30:31] #81 POST /v1/messages (84308b -> 66123b)
Apr 14 10:30:33 claude-proxy node[561265]: [10:30:31] #81 > 200
Apr 14 10:45:09 claude-proxy node[561265]: [10:45:09] #83 POST /v1/messages (69735b -> 49879b)
Apr 14 10:45:11 claude-proxy node[561265]: [10:45:09] #83 > 200

Limitations

  • Requires claude CLI available on PATH. Only tested with systemd deployment, not Docker.
  • The triggering request is delayed by ~8s while the CLI runs.

Relates to #6, #39

hunandy14 and others added 2 commits April 14, 2026 03:15
When a request arrives and the OAuth token is already expired, the proxy
now invokes `claude -p "ping"` to trigger a CLI-based token refresh before
forwarding. This is a non-blocking convenience helper — if the refresh
fails, the request proceeds with the expired token (API returns 401),
same behavior as without this change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hunandy14 hunandy14 changed the title feat: best-effort token auto-refresh on expired token best-effort token auto-refresh on expired token Apr 14, 2026
@DBostik

DBostik commented Apr 14, 2026

Copy link
Copy Markdown

Nicely done.

I've still been doing it manually inside the 2 minute window to keep testing it but I have run into the issue of Claude logging me out.

Now every 3-4 refreshes, so what seems to be once a day, when I use:

claude -p "ping" --max-turns 1 --no-session-persistence

I get:

Not logged in · Please run /login

This is another hurdle I can't get past. Anyone experience that or know how to stay logged in?

@hunandy14

Copy link
Copy Markdown
Author

Nicely done.

I've still been doing it manually inside the 2 minute window to keep testing it but I have run into the issue of Claude logging me out.

Now every 3-4 refreshes, so what seems to be once a day, when I use:

claude -p "ping" --max-turns 1 --no-session-persistence

I get:

Not logged in · Please run /login

This is another hurdle I can't get past. Anyone experience that or know how to stay logged in?

Thanks! I haven't run into the logout issue on my end — my proxy has been refreshing successfully so far without getting logged out. Will keep an eye on it though.

JJValentin pushed a commit to JJValentin/openclaw-billing-proxy that referenced this pull request Apr 14, 2026
Ports 10 fixes discovered through code-level comparison with opencode-claude-auth
(griffinmartin/opencode-claude-auth v1.4.10), verified with 38 unit tests and
live-tested on an OpenClaw Max subscription.

Critical fixes:
- Add mcp_ prefix to all tool renames (Anthropic rejects bare PascalCase)
- Compute real CCH hash (was hardcoded cch=00000)
- Model-aware beta flags: exclude interleaved-thinking for Haiku,
  add effort only for 4.6 models, remove fake betas

Reliability fixes:
- Strip effort parameter for Haiku models (prevents 400 errors)
- Direct OAuth token refresh via POST to claude.ai/v1/oauth/token
  with pre-expiry check, 401 retry, and race-condition dedup
- Improved system prompt boundary detection (6 identity markers,
  generic Windows drive letter patterns, bounded to system[] array)

Cleanup fixes:
- Disable CC tool stubs by default (fixes zacdcook#43 tool-not-found loops)
- Add orphaned tool_use/tool_result pair repair
- Remove non-existent beta flags (advanced-tool-use, fast-mode)
- Update Stainless SDK version to 0.90.0

Also adds test-functions.js with 38 unit tests covering all pure
functions (no credentials or network required to run).

Fixes zacdcook#41, zacdcook#43, zacdcook#46. Partially addresses zacdcook#47 (token refresh).
@hunandy14

Copy link
Copy Markdown
Author

Day 2 update — auto-refresh triggered again successfully, zero downtime:

0|openclaw | [18:03:29] #639 POST /v1/messages (90202b -> 78798b)
0|openclaw | [18:03:29] #639 > 200
0|openclaw | [token] Token expired, running claude CLI to refresh...
0|openclaw | [token] CLI refresh done
0|openclaw | [STRIP] Removed 14265 chars of config template
0|openclaw | [18:33:41] #640 POST /v1/messages (90709b -> 79301b)

639 requests served before expiry, refreshed, #640 continued normally.

@yetdog

yetdog commented Apr 15, 2026

Copy link
Copy Markdown

I updated proxy.js with these PR changes but still no change on my end.

@hunandy14

Copy link
Copy Markdown
Author

I updated proxy.js with these PR changes but still no change on my end.

Did you restart the proxy after updating? Also, this change likely won't work in Docker since there's no Claude installation inside the container.

@yetdog

yetdog commented Apr 16, 2026

Copy link
Copy Markdown

Sure did:

Apr 16 09:18:10 clawdbot node[695175]: [token] Token expired, running claude CLI to refresh...
Apr 16 09:18:10 clawdbot node[695175]: [STRIP] Removed 11655 chars of config template
Apr 16 09:18:10 clawdbot node[695175]: [14:18:10] #2507 POST /v1/messages (106328b -> 91901b)
Apr 16 09:18:10 clawdbot node[695175]: [14:18:10] #2507 > 401

Running it as a systemd service, not claude. Appreciate the response!

- Replace hardcoded /home/ubuntu/.local/bin with findClaudeBin() that
  checks: config.json "claudeBin" → PATH → ~/.local/bin/claude
- Log errors inside refreshOAuthToken() so failures are always visible
- Remove duplicate error logging from call site

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hunandy14

hunandy14 commented Apr 16, 2026

Copy link
Copy Markdown
Author

Fixed — the previous version had a hardcoded path (/home/ubuntu/.local/bin) which wouldn't work on other systems. The updated code now auto-detects the claude CLI location by checking PATH and ~/.local/bin/claude. You can also set "claudeBin": "/path/to/claude" in config.json if needed.

@yetdog Could you try pulling the latest changes and restarting?

@yetdog

yetdog commented Apr 16, 2026

Copy link
Copy Markdown

Roger that - thank you so much!

Token set to expire in about 7 hours; will report back.

@hunandy14

hunandy14 commented Apr 17, 2026

Copy link
Copy Markdown
Author

Updated code verified — findClaudeBin() auto-detected the CLI path and refresh worked on first trigger after the update:

0|openclaw | [18:19:20] #7 POST /v1/messages (184077b -> 189389b)
0|openclaw | [18:19:20] #7 > 200
0|openclaw | [token] Claude CLI found: /home/ubuntu/.local/bin/claude
0|openclaw | [token] Token expired, running claude CLI to refresh...
0|openclaw | [token] CLI refresh done
0|openclaw | [18:49:29] #8 POST /v1/messages (184077b -> 189389b)
0|openclaw | [18:49:29] #8 > 200

@yetdog

yetdog commented Apr 19, 2026

Copy link
Copy Markdown

No go, sorry to say.

Apr 19 07:17:51 clawdbot node[30887]: OpenClaw Billing Proxy v2.2.3
Apr 19 07:17:51 clawdbot node[30887]: ─────────────────────────────
Apr 19 07:17:51 clawdbot node[30887]: Port: 18801
Apr 19 07:17:51 clawdbot node[30887]: Bind address: 127.0.0.1
Apr 19 07:17:51 clawdbot node[30887]: Emulating: Claude Code v2.1.97
Apr 19 07:17:51 clawdbot node[30887]: Subscription: max
Apr 19 07:17:51 clawdbot node[30887]: Token expires: 7.9h
Apr 19 07:17:51 clawdbot node[30887]: String patterns: 30 sanitize + 28 reverse
Apr 19 07:17:51 clawdbot node[30887]: Tool renames: 31 (bidirectional)
Apr 19 07:17:51 clawdbot node[30887]: Property renames: 8 (bidirectional)
Apr 19 07:17:51 clawdbot node[30887]: CC tool stubs: 5
Apr 19 07:17:51 clawdbot node[30887]: System strip: enabled
Apr 19 07:17:51 clawdbot node[30887]: Description strip: enabled
Apr 19 07:17:51 clawdbot node[30887]: Billing hash: dynamic (SHA256 fingerprint)
Apr 19 07:17:51 clawdbot node[30887]: CC headers: Stainless SDK + identity
Apr 19 07:17:51 clawdbot node[30887]: Credentials: /home//.claude/.credentials.json
Apr 19 07:17:51 clawdbot node[30887]: Ready. Set openclaw.json baseUrl to http://127.0.0.1:18801
Apr 19 07:18:06 clawdbot node[30887]: [STRIP] Removed 11655 chars of config template
Apr 19 07:18:06 clawdbot node[30887]: [12:18:06] #1 POST /v1/messages (105456b -> 90620b)
Apr 19 07:18:08 clawdbot node[30887]: [12:18:06] #1 > 200
Apr 19 13:06:18 clawdbot node[30887]: [STRIP] Removed 11586 chars of config template
Apr 19 13:06:18 clawdbot node[30887]: [18:06:18] #2 POST /v1/messages (183534b -> 163792b)
Apr 19 13:06:20 clawdbot node[30887]: [18:06:18] #2 > 200
Apr 19 17:25:59 clawdbot node[30887]: [STRIP] Removed 11586 chars of config template
Apr 19 17:25:59 clawdbot node[30887]: [22:25:59] #3 POST /v1/messages (185224b -> 165494b)
Apr 19 17:25:59 clawdbot node[30887]: [22:25:59] #3 > 401

@0n1cOn3

0n1cOn3 commented Apr 20, 2026

Copy link
Copy Markdown

I have applied this PR to my local instance. It works flawlessly since two days (on Debian).

@hunandy14

hunandy14 commented Apr 20, 2026

Copy link
Copy Markdown
Author

@0n1cOn3
Awesome, thanks for confirming!

@yetdog
What OS are you on?

Your first log (Apr 16) had [token] Token expired, running claude CLI to refresh..., meaning the PR was actually running, just failing on the hardcoded path.
But the Apr 19 log has no [token] messages at all, which suggests the updated code might not have been picked up.

@yetdog

yetdog commented Apr 20, 2026 via email

Copy link
Copy Markdown

@yetdog

yetdog commented Apr 20, 2026

Copy link
Copy Markdown

Some additional info. I don't actually have to login. If I get 401, and I just run "claude /login" from my open claw VM, it refreshes the token. Haven't tried it w/o the /login. I wonder if the original suggested cron job would work now?

@0n1cOn3

0n1cOn3 commented Apr 20, 2026

Copy link
Copy Markdown

Some additional info. I don't actually have to login. If I get 401, and I just run "claude /login" from my open claw VM, it refreshes the token. Haven't tried it w/o the /login. I wonder if the original suggested cron job would work now?

Works also without /login. Already tested that :)

@0n1cOn3

0n1cOn3 commented Apr 20, 2026

Copy link
Copy Markdown

No go, sorry to say.

07:17:51 clawdbot node[30887]: Token expires: 7.9h Apr 19

Token has been refreshed. 8.0h is the standard. The log shows that your token has been refreshed 10mins after you've ran node diagnostic.js

Or what you wanna tell us? :D

@hunandy14

hunandy14 commented Apr 21, 2026

Copy link
Copy Markdown
Author

Some additional info. I don't actually have to login. If I get 401, and I just run "claude /login" from my open claw VM, it refreshes the token. Haven't tried it w/o the /login. I wonder if the original suggested cron job would work now?

This PR attempts to obtain the token via the Claude CLI:
claude -p ping --max-turns 1 --no-session-persistence

I'm unsure if Claude requires a persistent login state.
Is there some reason on your machine that's causing you to be automatically logged out?

Another solution is to try obtaining the token directly via the API instead of the CLI,
but this isn't currently implemented in the PR.

Also, could you print your claude's location?
which claude

@0n1cOn3

0n1cOn3 commented Apr 23, 2026

Copy link
Copy Markdown

I have applied this PR to my local instance. It works flawlessly since two days (on Debian).

Short update: still rocking solid on debian so far. Since then, no further intervention has become necessary. :D

@yetdog

yetdog commented Apr 24, 2026

Copy link
Copy Markdown

No go, sorry to say.
07:17:51 clawdbot node[30887]: Token expires: 7.9h Apr 19

Token has been refreshed. 8.0h is the standard. The log shows that your token has been refreshed 10mins after you've ran node diagnostic.js

Or what you wanna tell us? :D

Well, I had just recently manually run 'claude' for it to refresh. It's still not working automatically for me.

which claude:
/home/myuser/.local/bin/claude

@0n1cOn3

0n1cOn3 commented Jun 13, 2026

Copy link
Copy Markdown

Well, I had just recently manually run 'claude' for it to refresh. It's still not working automatically for me.

Hmm.. which OS do you use and have you applied this PR to the project and restarted the proxy after that? I have set it up once - and since then, works flawlessly.

Running under Debian 13 here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants