Blog / claude code
Run Claude Code or Codex on a schedule
Run a coding agent every night with cron or launchd: the script, the PATH and sign-in traps that make scheduled runs fail, and safe permissions.
Some agent jobs are the same every day: list new TODOs, check which dependencies have updates, summarise yesterday's commits, look for flaky tests. You can run them by hand each morning, or let your computer run them at 3 a.m. and read the result with your coffee.
Both Claude Code and Codex have a non-interactive mode made for this: claude -p "prompt" and codex exec "prompt" run one job and exit. The hard part isn't the agent; it's that schedulers start programs in a bare environment where half of what works in your terminal doesn't. We ran the setup below for real on macOS (launchd) and Linux (cron), and this post shows each trap we hit.
Step 1: a script that runs one job#
Start with a script, not a long line in your crontab. It is easier to test and you can keep it in the repo:
#!/bin/bash
# nightly.sh: one scheduled agent run.
# Schedulers start with almost no PATH, so set it here.
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
cd "$HOME/code/myapp" || exit 1
mkdir -p "$HOME/agent-logs"
log="$HOME/agent-logs/$(date +%F-%H%M).log"
claude -p "List every TODO comment in this repo with file and line. Do not change any file." \
--allowedTools "Read,Grep,Glob" \
--max-turns 10 \
< /dev/null > "$log" 2>&1
echo "exit $?" >> "$log"What each part is for:
export PATH=...is the most important line. More on that below.cdinto the project, with|| exit 1, so a moved folder doesn't send the agent into your home directory.--allowedTools "Read,Grep,Glob"lets the agent read and search without asking. In a scheduled run there is nobody to answer a permission prompt, so any tool you didn't allow is simply refused.--max-turns 10caps how long one run can go on.< /dev/nulltells Claude Code there is no input coming, so it doesn't wait for any.- The log file is how you see what happened. Write one per run, with the date in the name.
For Codex, swap the claude line for:
codex exec -s read-only --ephemeral -o "$log.answer" \
"List every TODO comment in this repo with file and line. Do not change any file." \
< /dev/null > "$log" 2>&1-s read-only keeps Codex's sandbox from writing anything, --ephemeral skips saving the session, and -o writes just the final answer to its own file, which is handy when you only want the result.
Trap 1: "command not found"#
Run your script the way a scheduler would, with an almost empty environment:
env -i HOME="$HOME" /bin/sh -c 'claude --version'On our Mac this printed /bin/sh: claude: command not found, even though claude works in every terminal. Your shell profile adds ~/.local/bin, Homebrew and the rest to PATH; cron and launchd never read your shell profile. That's why the script sets PATH itself.
We checked cron on Linux too: a job that printed its own PATH showed /usr/bin:/bin and nothing else, so node (which Codex and many tool servers need) was not found either.
Find the folders you need with command -v claude codex node in a normal terminal and put their directories in the script's PATH.
Trap 2: "Not logged in"#
Our next bare test got further and then failed with Not logged in · Please run /login. We had passed only HOME. On macOS, Claude Code keeps its sign-in in your login keychain, looked up by user name, so it needs USER as well:
env -i HOME="$HOME" USER="$USER" /bin/bash ./nightly.shWith that, the run worked. cron and launchd both set USER for jobs that run as you, so this mostly bites when you test with env -i. Two related cases:
- Several logins: if you use a separate config folder for another account (
CLAUDE_CONFIG_DIR), set it in the script too; the scheduler won't inherit it. - Servers without a keychain: sign in once as the same user the job runs as. If you use an API key instead of a subscription, put it in the environment the job gets, never in the script you commit.
Step 2: schedule it on macOS with launchd#
Apple recommends launchd over cron on macOS. Save this as ~/Library/LaunchAgents/com.you.nightly-agent.plist (change the label and path):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.you.nightly-agent</string>
<key>ProgramArguments</key>
<array><string>/bin/bash</string><string>/Users/you/bin/nightly.sh</string></array>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>3</integer><key>Minute</key><integer>0</integer></dict>
<key>StandardErrorPath</key><string>/Users/you/agent-logs/launchd.err</string>
</dict>
</plist>Then check and load it:
plutil -lint ~/Library/LaunchAgents/com.you.nightly-agent.plist
launchctl bootstrap gui/"$(id -u)" ~/Library/LaunchAgents/com.you.nightly-agent.plist
launchctl print gui/"$(id -u)"/com.you.nightly-agent | grep -E "state|last exit"To try it without waiting until 3 a.m., add <key>RunAtLoad</key><true/> while testing: it runs once as soon as you load it. Ours ran, found the TODO and wrote exit 0 to the log. To remove the job: launchctl bootout gui/"$(id -u)"/com.you.nightly-agent.
If the Mac is asleep at 3:00, launchd runs the job when it wakes. If it was shut down, the run is skipped.
Step 2 on Linux: cron#
crontab -eand add one line (minute, hour, then three stars for every day):
0 3 * * * /home/you/bin/nightly.shcrontab -l shows what is installed. If the job never seems to run, look for the cron service's log (journalctl -u cron on Debian and Ubuntu) and for your script's own log file.
Keep scheduled runs safe#
A job that runs while you sleep should be a job you'd be happy to read about in the morning:
- Start read-only. Reports, lists and summaries first. Allow edits only once you trust the output.
- When it does edit, let it edit a branch. Have the script
git switch -c nightly/$(date +%F)first, so nothing lands on your main branch without you. - Never use the skip-all-permissions flags on a schedule. Allow the exact tools the job needs.
- Cap it.
--max-turnsfor Claude Code, atimeout 1800in front of either command (on a Mac,timeoutcomes with Homebrew's coreutils), and one run at a time. - Read the log. A job that fails quietly every night is worse than no job.
Inside an open session: /loop#
If you only need something repeated while you're at your desk, Claude Code's /loop runs a prompt on an interval inside an open session, for example /loop 30m check whether the deploy finished. It stops when the session ends. Use cron or launchd for anything that should run whether or not a terminal is open.
Where Joxo fits#
A scheduled run is most useful when its result reaches you. With Joxo, your agents' work shows up on your phone and desktop, and when an agent needs an answer from you, the question comes to you instead of waiting in a terminal. Setup is one line pasted into your agent. To keep a Mac running overnight, see keep your Mac awake for Claude Code.
Frequently asked questions#
Can I run Claude Code from cron?#
Yes. Use claude -p "prompt" in a script, set PATH inside the script (cron doesn't read your shell profile), allow the tools the job needs with --allowedTools, and send the output to a log file.
Why does my scheduled Claude Code say "Not logged in"?#
The job can't reach your sign-in. On macOS, run it as your own user with USER set (launchd and cron do this; env -i tests don't), and set CLAUDE_CONFIG_DIR if you use a separate config folder.
How do I run Codex without a terminal?#
Use codex exec "prompt". Add -s read-only for jobs that shouldn't write, --ephemeral to skip saving the session, and -o file to save the final answer.