The agent scheduled the nightly job in its own local time, and the container ran it seven hours off
The agent was asked to move a nightly report off business hours. It added a cron entry for 02:00, ran the task once by hand, saw the report generate, and committed. On the developer's laptop the manual check landed at the expected quiet hour, so nothing looked wrong. In the deployed container the same 02:00 meant a different clock: the image defaults to UTC, the host has no timezone set, and the report fired during the morning traffic window instead of overnight.
A bare schedule expression carries no timezone. It inherits whatever clock the process reads at runtime, and that clock is not the author's. The agent tested the task body, which runs fine at any hour, but never tested which hour the host believed it was. A local smoke run proves the job executes; it cannot prove the schedule lands where you meant it.
A schedule without a zone is ambiguous
Cron fields name minute and hour, not the clock they belong to. On the laptop the local offset makes 02:00 read as late night; in a UTC container the identical fields mean 02:00 UTC, which falls in the author's morning instead.
Containers and CI runners usually start in UTC. Unless TZ is explicitly mounted or set, the host localtime is absent, and relying on it silently shifts every scheduled task by the offset between the author's machine and the runtime.
The agent copies the quiet-hour intuition from the author's local evening and writes it as if the production runtime shared that clock. The manual run matches the author's expectation, which makes the gap invisible until the first real fire.
Make the clock explicit, then verify the next fire
Express the schedule against a named clock. Write the expression in UTC, or set TZ in the container and document the zone the expression assumes. Do not leave the host localtime as the hidden source.
Log the effective timezone and the next fire. At startup, print the active zone and the next-fire time converted to that zone; a one-line log catches a seven-hour offset before the first real run.
Evaluate the cron against the target zone. Use a small library to resolve the expression under the container's clock instead of eyeballing the fields, and compare the resulting UTC instant to the intended quiet window.
Use an explicit instant for one-shot work. For a migration or backup, pass a UTC timestamp rather than a local wall time, so the scheduler has nothing to guess.
Separate the two checks. Treat the manual local run as proof the task body works, not proof the schedule is correct; the schedule still needs a next-fire check under the container's clock.
Agents optimize for the clock in front of them, the one they are running on, not the one production will use. Where does your scheduler still inherit local time without naming it?
This is part of a larger workflow; the AI coding agent guide collects the related decisions and keeps them in sync.