timezone-review
Skill0 starsChanged 4 months agoMITView on GitHub
Find the date and time bugs that only appear for users in other timezones or on specific days, by separating true instants from calendar dates and checking where the server's local clock leaks in. Use before shipping scheduling, reporting, streaks, expiry, or anything with a daily boundary.
- Reads credentials
In this repository19 other files it gives its agents.
- client-secret-sweepSkill
- codebase-onboarding-mapSkill
- commitSkill
- crawlability-auditSkill
- dependency-outage-auditSkill
- deploy-drift-checkSkill
- empty-state-auditSkill
- env-drift-checkSkill
- fake-data-guardSkill
- honest-metrics-auditSkill
- migration-safety-reviewSkill
- money-math-reviewSkill
- n-plus-one-finderSkill
- prompt-injection-reviewSkill
- pr-reviewerSkill
- release-notes-writerSkill
- retry-safety-reviewSkill
- route-auth-sweepSkill
- webhook-safety-reviewSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 8 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 79
If nothing sets TZ anywhere, note that the behaviour depends on the host, which means it can change without a deploy.
- Distinctive: line 84
Every "start of day" is a decision about whose day it is:
- Distinctive: line 139
Third-party APIs: Check what zone the provider documents, not what you assume.
- Distinctive: line 170
Never "fix" a timezone bug by adding or subtracting hours.
- Distinctive: line 172
Never store a local wall time as a UTC instant when the wall time is what the user promised.
- Distinctive: line 174
Do not assume the deploy target's timezone.
- Distinctive: line 175
Test any fix against a zone with a half-hour offset and a southern-hemisphere DST zone.
- Distinctive: line 181
Keep this line and others can find it too.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Timezone Review
Install
Save this file as ~/.claude/skills/timezone-review/SKILL.md, or
.claude/skills/timezone-review/SKILL.md to scope it to one repo. Claude Code
auto-discovers it. Invoke with /timezone-review or by asking "will this break
for users in other timezones?".
Why this exists
Timezone bugs pass every test, because the test machine, the developer, and the CI runner are usually all in UTC or all in the same offset. They appear in production for the subset of users whose local day boundary is not yours, and they appear as complaints that sound impossible: "my streak reset a day early", "the report shows yesterday's number", "my subscription expired at 7pm".
They are also seasonal. Code that is correct in January can break in March when a DST transition creates a day with 23 hours, or a local time that does not exist at all.
The root cause is nearly always the same: a calendar date and a true instant are different types, and the code uses one where it means the other.
Step 1: Separate instants from calendar dates
Go through every date field in the schema and classify it:
INSTANT. A specific moment on the global timeline. created_at,
last_login, paid_at. Correct storage is UTC with an offset
(timestamptz, epoch millis). Rendering is per-viewer.
CALENDAR DATE. A day as humans name it, with no time and no zone.
birth_date, invoice_date, holiday. Correct storage is a date type or a
string. Attaching a time to these is the bug: a birthday stored as
1990-05-04T00:00:00Z becomes May 3rd for everyone west of Greenwich.
LOCAL WALL TIME. A time in a place, where the place matters more than the instant: "the meeting is at 9am in Berlin", "the store opens at 08:00". Correct storage is the wall time plus an IANA zone id, NOT a UTC instant, because if the zone's rules change your stored instant is now the wrong wall time.
rg -n "(date|time|_at|_on|expires|starts|ends|scheduled|birthday|deadline)" \
prisma/schema.prisma migrations db/schema.rb 2>/dev/nullAny field whose class you cannot state is a finding. Any CALENDAR DATE stored as a timestamp is a finding.
Step 2: Find the server's local clock leaking in
The server's own timezone should never influence a result. Look for the calls that let it:
rg -n "new Date\(\)|Date\.now\(\)|datetime\.now\(\)|Time\.now|time\.Now\(\)" -t code
rg -n "getFullYear|getMonth|getDate|getHours|getDay" -t code
rg -n "toDateString|toLocaleDateString|toLocaleString|strftime" -t codeThe get* family and toLocaleString without an explicit zone read the
process timezone. On your laptop that is your zone, on the server it is
usually UTC, and in a container it depends on how the image was built. Same code,
three answers.
Flag any place a date is formatted or truncated without an explicit timezone argument.
rg -n "TZ=|process\.env\.TZ|ZoneId|tzset" --hidden -g '!node_modules'If nothing sets TZ anywhere, note that the behaviour depends on the host, which means it can change without a deploy.
Step 3: Find the day boundaries
Every "start of day" is a decision about whose day it is:
rg -n "startOf|endOf|setHours\(0|midnight|00:00|23:59|beginning_of_day" -t codeFor each, answer: whose midnight? The user's, the server's, UTC, or the business's?
The recurring bugs:
- Daily aggregates in UTC shown to local users. A user in UTC+13 sees "today's sales" that ends at 11am their time.
- Streaks and daily limits. Computed in UTC, they break a streak for anyone whose local day straddles the boundary. This is the single most common version of this bug.
- Expiry. "Valid until 2026-08-01" in UTC expires mid-afternoon on July 31st in Auckland.
- Cron and scheduled jobs. A job at "00:00" runs at a different local time half the year unless the scheduler is zone-aware.
Step 4: Check DST and arithmetic
rg -n "\* 24 \* 60 \* 60|86400|\+ 7 \* 24|addDays|addHours|dateAdd" -t codeAdding 86400 seconds is not adding a day. On DST transition days a local day is 23 or 25 hours. Anything that computes "tomorrow" or "next week" by adding fixed seconds is wrong twice a year for zones that observe DST.
Also check:
- Non-existent local times. In a spring-forward zone, 02:30 does not exist on transition day. Code that constructs it gets an error or a silent shift.
- Ambiguous local times. In autumn, 01:30 happens twice. Which one did you store?
- Hardcoded offsets.
UTC+5:30or-05:00written as a constant. Offsets change; zone ids do not. StoreAsia/Kolkata, not+05:30.
rg -n "[+-][0-9]{2}:[0-9]{2}|UTC[+-][0-9]|GMT[+-][0-9]" -t codeStep 5: Check the boundaries where dates cross systems
Dates get corrupted in transit more often than in storage:
- JSON serialization. Does the API emit ISO 8601 with an offset, or a zone-less string a client will parse as local?
- Form input. An HTML
<input type="date">gives a calendar date. Parsing it withnew Date("2026-05-04")yields UTC midnight, which is the previous day in the Americas. - CSV and spreadsheet export. Almost always zone-less, and often reformatted by the spreadsheet on open.
- Third-party APIs. Check what zone the provider documents, not what you assume.
rg -n "new Date\(['\"][0-9]{4}-" -t codeStep 6: Report
[SEVERITY] <what breaks, in user terms>
Where: file:line
Class: INSTANT / CALENDAR DATE / LOCAL WALL TIME
Assumes: <the timezone the code implicitly uses>
Breaks: <which users, and on which days>
Fix: <the specific change>Severity:
- CRITICAL wrong data for real users today (wrong day's report, early expiry, broken streak).
- HIGH correct now but breaks on a DST transition or for a zone you do not currently have users in.
- MEDIUM relies on the host's TZ setting, so it is correct by luck.
End with the field classification table from Step 1. If the codebase handles time correctly, that table is the evidence, and it is worth keeping in the repo.
Rules
- Never "fix" a timezone bug by adding or subtracting hours. That is how the second bug is created. Fix the type or the explicit zone.
- Never store a local wall time as a UTC instant when the wall time is what the user promised. Zone rules change.
- Do not assume the deploy target's timezone. Verify it or set it explicitly.
- Test any fix against a zone with a half-hour offset and a southern-hemisphere DST zone. If it only works for whole-hour northern offsets, it is not fixed.
From Toolbay. Free to use, modify, and share. Keep this line and others can find it too.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

