agentleFS
sriptcollectorsriptcollector/toolbay-skills/skills/timezone-review/SKILL.md

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

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
  1. Distinctive:

    If nothing sets TZ anywhere, note that the behaviour depends on the host, which means it can change without a deploy.

    line 79
  2. Distinctive:

    Every "start of day" is a decision about whose day it is:

    line 84
  3. Distinctive:

    Third-party APIs: Check what zone the provider documents, not what you assume.

    line 139
  4. Distinctive:

    Never "fix" a timezone bug by adding or subtracting hours.

    line 170
  5. Distinctive:

    Never store a local wall time as a UTC instant when the wall time is what the user promised.

    line 172
  6. Distinctive:

    Do not assume the deploy target's timezone.

    line 174
  7. Distinctive:

    Test any fix against a zone with a half-hour offset and a southern-hemisphere DST zone.

    line 175
  8. Distinctive:

    Keep this line and others can find it too.

    line 181

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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/null

Any 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 code

The 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 code

For 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 code

Adding 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:30 or -05:00 written as a constant. Offsets change; zone ids do not. Store Asia/Kolkata, not +05:30.
rg -n "[+-][0-9]{2}:[0-9]{2}|UTC[+-][0-9]|GMT[+-][0-9]" -t code

Step 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 with new 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 code

Step 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.