Answers · agent safety
How do I make sure AI-generated code handles time zones correctly?
Last updated
TL;DR: Tell the agent which of two kinds of time it is handling. For something that already happened (a payment, a log line, created_at), store the instant in UTC, and store the user's IANA zone name (America/New_York, not EST or a fixed -05:00) separately if you need to show local time; PostgreSQL's timestamp with time zone, for example, converts input to UTC and does not keep the zone it was given. For something scheduled in local time (a 9 AM meeting next March, a recurring reminder), store the local date and time plus the IANA zone and work out the instant when you need it, because governments change the rules: tz database release 2026f (October 27, 2026) records that Cuba will not fall back on November 1, 2026, so a system with older tz data puts Havana an hour off. Then review the code for the usual mistakes: naive datetimes, abbreviations such as CST (which the tz project notes means one thing in China and another in North America), fixed offsets standing in for zones, and no handling for local times that happen twice or not at all around clock changes. Test by running the suite with TZ set to unusual zones such as Pacific/Kiritimati (UTC+14), Pacific/Chatham (+12:45, +13:45 in summer), Asia/Kathmandu (+05:45) and Australia/Lord_Howe (30-minute DST).
What should I check, step by step?
1. Put the rule in the agent's instructions (AGENTS.md or CLAUDE.md): past events as UTC instants, future local-time events as local date-time plus IANA zone, user zone stored as an IANA name, never an abbreviation or a fixed offset.
- Reject naive datetimes in review. Python's docs say a naive object doesn't contain enough information to locate itself relative to other date/time objects, recommend datetime.now(timezone.utc) for the current UTC time, and deprecate datetime.utcnow() since 3.12. Use zoneinfo.ZoneInfo("Europe/Paris") for zones.
- In PostgreSQL, use timestamp with time zone (timestamptz) for instants. It converts input with an explicit zone to UTC, assumes the session TimeZone when there isn't one, and displays values in the session's zone. A value typed as timestamp without time zone silently ignores any zone in its input, so check how literals are typed.
- Serialize instants with an offset, and add the zone when later arithmetic depends on it. RFC 9557 extends RFC 3339 with a bracketed suffix: 2020-01-01T00:00+01:00[Europe/Paris] lets a program add six months and adjust for summer time, while the same calculation on [+01:00] is an hour off in Paris.
- Handle ambiguous and nonexistent local times explicitly. With Python's zoneinfo, fold=0 uses the offset before a transition and fold=1 the one after: 2026-11-01 01:30 in America/New_York is -04:00 with fold=0 and -05:00 with fold=1. JavaScript's Temporal takes a disambiguation option (compatible by default, earlier, later or reject); reject throws instead of guessing.
- Keep tz data current. Python's zoneinfo uses the system's tz database and falls back to the first-party tzdata package on PyPI; its docs recommend depending on tzdata for cross-platform code because some systems, notably Windows, have no IANA database. Rebuild containers when the OS tzdata package updates and check the release you ship against iana.org/time-zones.
- Run the tests under several zones, not just the developer's: TZ=UTC, TZ=Pacific/Kiritimati, TZ=Pacific/Chatham, TZ=Asia/Kathmandu, TZ=Australia/Lord_Howe and TZ=America/St_Johns (-03:30), plus dates on each side of a DST change. A suite that passes only in one zone is hiding a local-time assumption.
- Add date-line and rule-change cases: Pacific/Kiritimati moved from -10:00 to +14:00 at the end of 1994, so December 31, 1994 doesn't exist there, and a future Havana time on November 2, 2026 should come out at -04:00 with tz data 2026f.
Instants and wall-clock times are different data
MDN describes Temporal.ZonedDateTime as a bridge between an exact time (an instant in history) and a wall-clock time, and says converting from UTC to local time is straightforward while converting local time to UTC is ambiguous, because one local time can match zero, one or several instants.
- That is why storing UTC alone is not enough for future events. The instant for 9:00 next March in a given city depends on rules that may change before then; storing the local time and zone lets you recompute it with the tz data you have at the time.
- Temporal is the zone-aware API in JavaScript, but MDN marks Temporal.ZonedDateTime as limited availability (not Baseline, because it does not work in some widely used browsers), so check support for your targets or use a library and keep the same rules.
Why zone names, not abbreviations or offsets
The tz project's Theory and pragmatics page says abbreviations are ambiguous in practice: CST means one thing in China and another in North America, and IST can mean India, Ireland or Israel. It recommends numeric offsets like -0600 over abbreviations where you need to avoid ambiguity.
- A fixed offset is right for one instant but wrong as a zone: it doesn't follow daylight saving time or rule changes. RFC 9557 also notes that an offset and a zone name in the same timestamp can disagree when time zone definitions are updated at different times by producers and receivers, so treat a mismatch as something to check, not to ignore.
- Rules do change at short notice. tz database 2026f, released October 27, 2026, changed Cuba's future timestamps (no fall back on November 1, 2026; -04 kept through November 7, 2027). On a machine with older tz data, TZ=America/Havana still shows CST (-05) for November 2, 2026.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “The agent added a date and time zone library to fix our time handling. Check its license before we ship it in our commercial app.”
{
"tool": "check_license",
"arguments": {
"source_url": "<the npm or PyPI page of the library the agent added>",
"intended_use": {
"commercial_use": true,
"redistribution": true
}
}
}- Free, no key. It checks the license the package page declares against the uses you list and returns the duties, such as keeping the copyright notice.
- check_rights_batch checks up to 100 dependencies in one call, for when the agent added several packages.
Rightshold does not review or test your code's date handling. It checks the licenses of packages you ask about. Not legal advice.
What are the sources?
- Python docs: datetime (aware and naive objects, datetime.now(timezone.utc), utcnow deprecation)
- Python docs: zoneinfo (IANA time zone support, fold, data sources and tzdata)
- PostgreSQL docs: Date/Time types (timestamp with time zone stored as UTC)
- RFC 9557: Date and Time on the Internet: Timestamps with Additional Information
- MDN: Temporal.ZonedDateTime (ambiguity and gaps, disambiguation, browser support)
- IANA: Time Zone Database (latest release 2026f)
- tz database NEWS (2026f: Cuba will not fall back on November 1, 2026)
- tz database: Theory and pragmatics (time zone abbreviations)
Related
- Who owns code written by an AI coding agent?
- How to check licenses of all npm dependencies before shipping
- How to make Claude Code reuse your existing components
- How to require AI agent commits to go through pull requests
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
