TL;DR: Claude's 5-hour usage window starts with your first message. I start working around 5:45 AM, which puts my reset at 10:45, right in the middle of the morning. So my PC now powers itself on at 3 AM, logs in, opens Ghostty, sends Claude "hello", and shuts down 10 seconds after the reply. The window resets at 8 AM instead. The pieces are rtcwake, LightDM autologin, an XDG autostart entry and a Claude Code Stop hook, wrapped in a small CLI called wakejob. For Z.ai, which has its own 5-hour window, a cron-only Cloudflare Worker called aiwake does the same thing without the PC.

Why 3 AM

Claude's session limit is a 5-hour window, and the clock starts at the first message, not at a fixed time of day.

I wake up at 5 AM. After Fajr prayer I sit down to work at around 5:45, give or take. If my first message is at 5:45, the window runs until 10:45. A productive morning burns through it well before that, and then I wait.

If a window has already started at 3 AM, it ends at 8 AM. From 5:45 to 8:00 I am using a window that was mostly idle, and at 8:00 I get a fresh one. Same plan, same limits, just a better-placed reset.

I am not awake at 3 AM, so the PC has to do it.

The chain

Every step has to work with nobody at the keyboard:

  1. Power on at a set time.
  2. Log in without a password prompt.
  3. Open Ghostty after login.
  4. Start Claude and send a message.
  5. Power off once Claude has answered.

My machine runs Linux Mint with Cinnamon on X11 and LightDM as the display manager. The details below are for that setup, but the shape works on most Linux desktops.

Power on: the RTC alarm

The motherboard's real-time clock can wake the machine from a full power off. rtcwake sets that alarm:

sudo rtcwake -m no -t "$(date -d 'tomorrow 03:00' +%s)"

-m no sets the alarm and does nothing else, so I can keep working and shut down whenever I like. -m off sets it and powers off immediately.

My hardware clock runs in UTC, which rtcwake reads from /etc/adjtime, so it handles the conversion. When you check the alarm in /proc/driver/rtc, the time it shows is in UTC. For me, in UTC+6, a 03:00 alarm shows up as 21:00:00 on the previous day, which looks wrong the first time you see it.

Log in: autologin

I briefly considered automating the password prompt by typing into it. That would mean storing my password in a script, which is worse than the problem it solves. LightDM has autologin built in:

# /etc/lightdm/lightdm.conf.d/80-autologin.conf
[Seat:*]
autologin-user=z
autologin-user-timeout=0

This is a real trade-off. Anyone who presses the power button gets my desktop. Apps that use the keyring will also ask for the password, because autologin leaves the keyring locked. For a desktop at home I accept that, and deleting one file turns it off.

Open Ghostty: an autostart entry

Any .desktop file in ~/.config/autostart runs when the session starts:

[Desktop Entry]
Type=Application
Name=Ghostty Job
Exec=ghostty -e /home/z/.local/bin/wakejob run
X-GNOME-Autostart-Delay=5

The catch is that it runs on every login, not just the 3 AM one. A job whose last step is "power off" must not run when I reboot at noon, so the job checks the time first and exits unless it is within a window around the scheduled time. It also deletes its own autostart entry before doing anything, so it runs once.

(( now >= TARGET - 120 && now <= TARGET + 1800 )) || exit 0
rm -f "$AUTOSTART" "$CONF"

Talk to Claude, then power off

The first version used claude -p "hello", which prints a reply and exits, so sleep 30; systemctl poweroff after it was enough. The first test woke the machine at 19:55:16 and it was off again at 19:55:52.

I wanted the real interactive session instead, the one I would see if I opened Claude Code myself. Passing a prompt as an argument does that: claude "hello" opens the normal interface and sends the message. But an interactive session never exits on its own, so "run the next line when it finishes" no longer works.

Claude Code hooks solve it. A Stop hook runs when Claude finishes responding, and --settings loads extra settings for one session only:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "sleep 10 && systemctl poweroff", "timeout": 60 }
        ]
      }
    ]
  }
}
claude --settings ~/.config/ghostty-job/settings.json "hello"

My everyday Claude Code sessions never load this file, so they never turn the PC off.

Two things that would have stalled it

The folder trust prompt. Claude Code asks whether you trust the folder it starts in. In my home folder it asked every time, and at 3 AM that prompt would sit there until morning with the window never started. The fix is to start Claude in a dedicated project folder and approve it once by hand.

The drive. That folder lives on a second NVMe partition mounted at /media/z/projects. It is in /etc/fstab, so it is mounted before the login screen appears. If it is not, cd fails and Claude never starts in the wrong place. I still added a short wait, because it costs nothing:

for _ in $(seq 60); do mountpoint -q /media/z/projects && break; sleep 1; done

The 3 AM run that did not happen

The first real attempt failed silently. I set the alarm in the evening, then restarted the PC twice before finally shutting down. journalctl --list-boots showed nothing between 23:59 and 5:40. The alarm was most likely gone by the time I shut down, or it was never set.

The lesson is to set or check the alarm right before the last shutdown of the night, not hours earlier:

cat /sys/class/rtc/rtc0/wakealarm   # prints an epoch time if an alarm is set

Wrapping it in a CLI

By then I had edited dates in a script by hand four times, so it became a command:

wakejob set 03:00                    # next 03:00, sets the RTC alarm
wakejob set "16-10-2026 03:00" "hi"  # exact date, custom message
wakejob set 03:00 --off              # schedule and shut down now
wakejob test --no-off                # run it now in Ghostty, no power off
wakejob status                       # job, RTC alarm, autologin
wakejob cancel                       # remove job, clear alarm

set writes the target time and message to a small config file, creates the autostart entry, and calls rtcwake. The autostart entry calls wakejob run, which checks the time window and then opens Claude with the Stop hook. test skips the window check, so I can watch the whole thing happen while I am awake. Dates are DD-MM-YYYY, as they should be.

Z.ai does not need my PC

Claude is not my only plan with a 5-hour window. Z.ai's GLM Coding Plan has one too, and the same 5:45 problem applies. But Z.ai gives the coding plan an API key, so the wake call does not have to come from my desk. A Cloudflare Worker on a cron trigger can do it while the PC stays off.

That is aiwake. It is cron-only: the fetch handler answers every HTTP request with 403, so the only way in is the scheduled handler. The window is set with three plain variables:

"vars": {
  "AI_TIME": "0300am",
  "AI_TIMEZONE": "Asia/Dhaka",
  "AI_WINDOW_HOURS": "5",
  "ZAI_MODEL": "glm-5.3-flash"
}

The API key is a Worker secret, set with wrangler secret put ZAI_API_KEY, never a var.

Cron triggers only speak UTC. Dhaka is UTC+6 with no daylight saving, so a 03:00 to 07:00 window becomes the top of the hour at 21, 22, 23, 0 and 1 UTC:

"triggers": {
  "crons": ["0 0,1,21,22,23 * * *"]
}

Cron triggers are also scarce on a free Cloudflare account: you get 5 for the whole account, shared by every Worker. Mine were already used, so I had to delete an older cron to make room for this one.

The Worker fires once an hour inside the window, not once at the start. The first call opens the window, and the rest mean a single missed trigger does not cost me the morning. Each call sends a short prompt to glm-5.3-flash through Z.ai's OpenAI-compatible endpoint and logs the reply, so the Worker logs show whether it ran.

AI_TIME accepts labels like 3am, 3:13pm, 14:00 or a compact 0313pm. When a cron fires, the Worker maps its UTC minute and hour back to a slot in the window. If the cron does not match AI_TIME, it logs cron_slot_mismatch and skips the call instead of spending tokens at a random hour.

That check exists because the two settings live in different places and nothing keeps them in sync. Moving the window from 02:00 to 03:00 meant changing AI_TIME and also rewriting the cron from 0 0,20,21,22,23 to 0 0,1,21,22,23, then deploying again. Forgetting the cron would have left the Worker firing at the old times and skipping every call.

So the two halves split by what they need. Claude runs on my machine with my normal Claude Code login, so it needs the PC. Z.ai only needs an API key and a clock, so it runs in the cloud.