Skip to content

Instantly share code, notes, and snippets.

@2u841r
Last active June 2, 2026 03:55
Show Gist options
  • Select an option

  • Save 2u841r/f02adeb2ad82c11c56ea9e911d212593 to your computer and use it in GitHub Desktop.

Select an option

Save 2u841r/f02adeb2ad82c11c56ea9e911d212593 to your computer and use it in GitHub Desktop.
Claude Usage – Show Local Reset Time
// ==UserScript==
// @name Claude Usage – Show Local Reset Time
// @namespace https://gist.github.com/2u841r/f02adeb2ad82c11c56ea9e911d212593
// @version 1.3
// @description On claude.ai/settings/usage, appends the actual local clock time after the "Resets in 1h 23 min" countdown, e.g. "Resets in 1h 23 min (at 4:23 PM)".
// @author Zubair Ibn Zamir
// @homepageURL https://gist.github.com/2u841r/f02adeb2ad82c11c56ea9e911d212593
// @supportURL https://gist.github.com/2u841r/f02adeb2ad82c11c56ea9e911d212593
// @match https://claude.ai/*
// @license MIT
// @run-at document-start
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
// Note: @grant GM_addStyle is declared (even though unused) so Tampermonkey
// runs this script in its sandbox instead of injecting into the page context.
// claude.ai's CSP blocks page-context inline scripts, which is why @grant none
// silently failed to run. The color below uses CSSOM (element.style), which is
// not affected by CSP, so it works fine in the sandbox.
const MARK = 'usl-local-reset-time'; // class for the element we inject
// Parse a remaining-time string like "1h 23 min", "23 min", "Resets in 2 hours 5 minutes"
// Returns total seconds, or null if no duration found.
function parseDuration(text) {
const hMatch = text.match(/(\d+)\s*h(?:ours?|rs?)?\b/i);
const mMatch = text.match(/(\d+)\s*m(?:in(?:ute)?s?)?\b/i);
const sMatch = text.match(/(\d+)\s*s(?:ec(?:ond)?s?)?\b/i);
if (!hMatch && !mMatch && !sMatch) return null;
const h = hMatch ? parseInt(hMatch[1], 10) : 0;
const m = mMatch ? parseInt(mMatch[1], 10) : 0;
const s = sMatch ? parseInt(sMatch[1], 10) : 0;
return h * 3600 + m * 60 + s;
}
// A span is a target only if it actually holds the text itself
// (no child elements other than our own injected marker).
function isLeafish(span) {
for (const child of span.children) {
if (!child.classList.contains(MARK)) return false;
}
return true;
}
function processSpan(span) {
let myEl = span.querySelector(':scope > .' + MARK);
// Base text = the span's text WITHOUT our injected suffix.
let baseText = span.textContent;
if (myEl) baseText = baseText.replace(myEl.textContent, '');
const secs = parseDuration(baseText);
if (secs == null) {
if (myEl) myEl.remove();
return;
}
const target = new Date(Date.now() + secs * 1000);
const timeStr = target.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
const suffix = ' (at ' + timeStr + ')';
if (!myEl) {
myEl = document.createElement('span');
myEl.className = MARK;
myEl.style.color = 'rgb(244, 169, 169)';
span.appendChild(myEl);
}
if (myEl.textContent !== suffix) myEl.textContent = suffix;
}
function scan() {
// No URL/pathname guard: in the TanStack Start rewrite, Settings opens as a
// modal dialog and the URL may not change to /settings/usage. We instead rely
// on the text itself ("Resets in <duration>"), which only appears in this panel.
// parseDuration() inside processSpan() rejects non-countdown text like
// "Resets Jul 1", so matching on "reset" + a digit is safe.
document.querySelectorAll('span').forEach((span) => {
const t = span.textContent || '';
// Must look like a reset countdown: mentions "reset" and contains a number.
if (!/reset/i.test(t)) return;
if (!/\d/.test(t)) return;
if (!isLeafish(span)) return;
processSpan(span);
});
}
// Re-run periodically: the countdown re-renders and the SPA can swap views.
setInterval(scan, 1000);
scan();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment