A scheduled task manager for macOS.
Built to automate Claude Code sessions via launchd.
cron on macOS cannot access the Keychain. As a result, commands like claude -p "..." fail with Not logged in.
Cronlab uses launchd (the native macOS scheduler) which runs in the user context with full access to the Keychain, Homebrew PATH, and shell environment.
crontab --> no Keychain access --> "Not logged in"
launchd --> Keychain access --> Claude runs fine
| Feature | Description |
|---|---|
| View | List all tasks with active/inactive status, cron schedule, command |
| Create | Form with individual cron fields, quick presets, live preview |
| Edit | Inline editing of each task (schedule, command, label) |
| Enable/Disable | Toggle on/off without deleting the task |
| Delete | Double confirmation before removal |
| History | Stdout/stderr logs from each execution |
Main view with tasks showing active/inactive states, cron schedules, and commands.
Live cron expression preview, 9 quick presets, command and label fields.
Click the pencil icon to edit the schedule, command, and label directly in the card.
View stdout/stderr logs from each task execution.
- macOS (uses
launchd) - Node.js 20+
- Claude Code installed (optional, for automated sessions)
# Clone the repo
git clone https://github.com/Refaltor77/Cronlab.git
cd Cronlab
# Install dependencies
npm install
# Start the server
npm run devOpen http://localhost:3000.
src/
app/
page.tsx # Main interface (React client component)
layout.tsx # Layout with Geist font
globals.css # Design system (white theme)
api/
crontab/
route.ts # LaunchAgents CRUD (GET/POST/PUT/PATCH/DELETE)
history/
route.ts # Execution logs reader
~/Library/LaunchAgents/
com.cronlab.task-*.plist # Plist files generated by Cronlab
~/.cronlab/logs/
task-*.stdout.log # Standard output for each task
task-*.stderr.log # Error output for each task
task-*.meta.json # Metadata (label)
- Create — Cronlab generates a
.plistfile in~/Library/LaunchAgents/ - Load —
launchctl loadregisters the plist with the macOS scheduler - Execute —
launchdruns the command via/bin/zsh -l -c(login shell = Keychain access) - Log — stdout/stderr are redirected to
~/.cronlab/logs/ - Disable —
launchctl unloadremoves the job without deleting the file
<?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.cronlab.task-1234567890</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-l</string>
<string>-c</string>
<string>claude -p "Review open PRs" --dangerously-skip-permissions</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>~/.cronlab/logs/task-1234567890.stdout.log</string>
<key>StandardErrorPath</key>
<string>~/.cronlab/logs/task-1234567890.stderr.log</string>
</dict>
</plist>Schedule : 0 9 * * 1-5
Command : claude -p "Review open PRs on this repo and summarize the changes" --dangerously-skip-permissions
Label : Daily Code Review
Schedule : */15 * * * *
Command : claude -p "Run the tests and verify the build passes" --dangerously-skip-permissions
Label : Health Check CI
Schedule : 0 0 * * 0
Command : claude -p "Generate a summary of this week's commits" --dangerously-skip-permissions
Label : Weekly Report
Schedule : 0 0 * * *
Command : tar -czf ~/backups/project-$(date +%Y%m%d).tar.gz ~/my-project
Label : Project Backup
Schedule : 0 6 * * 1
Command : find ~/.cronlab/logs -name "*.log" -mtime +30 -delete
Label : Log Cleanup
Cronlab uses the standard 5-field cron syntax:
* * * * *
| | | | |
| | | | +-- Day of week (0-6, 0 = Sunday)
| | | +------- Month (1-12)
| | +------------ Day of month (1-31)
| +----------------- Hour (0-23)
+---------------------- Minute (0-59)
| Preset | Expression | Use case |
|---|---|---|
| Every minute | * * * * * |
Testing and debug |
| Every 5 min | */5 * * * * |
Frequent monitoring |
| Every 15 min | */15 * * * * |
Health checks |
| Every hour | 0 * * * * |
Hourly tasks |
| Every day 9 AM | 0 9 * * * |
Daily standup |
| Every day midnight | 0 0 * * * |
Nightly maintenance |
| Mon-Fri 9 AM | 0 9 * * 1-5 |
Weekdays only |
| Every Sunday | 0 0 * * 0 |
Weekly reports |
| 1st of month | 0 0 1 * * |
Monthly reports |
All routes are under /api/crontab.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/crontab |
List all tasks |
POST |
/api/crontab |
Create a new task |
PUT |
/api/crontab |
Update an existing task |
PATCH |
/api/crontab |
Enable/disable a task |
DELETE |
/api/crontab |
Delete a task |
GET |
/api/crontab/history |
Execution history |
# List tasks
curl http://localhost:3000/api/crontab
# Create a task
curl -X POST http://localhost:3000/api/crontab \
-H "Content-Type: application/json" \
-d '{
"schedule": "0 9 * * 1-5",
"command": "claude -p \"Review open PRs\" --dangerously-skip-permissions",
"comment": "Daily Code Review"
}'
# Disable a task
curl -X PATCH http://localhost:3000/api/crontab \
-H "Content-Type: application/json" \
-d '{"id": "task-123", "enabled": false}'
# Update a task
curl -X PUT http://localhost:3000/api/crontab \
-H "Content-Type: application/json" \
-d '{
"id": "task-123",
"schedule": "0 10 * * 1-5",
"command": "claude -p \"New prompt\" --dangerously-skip-permissions",
"comment": "Updated label"
}'
# Delete a task
curl -X DELETE http://localhost:3000/api/crontab \
-H "Content-Type: application/json" \
-d '{"id": "task-123"}'| Component | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI | React 19, Tailwind CSS 4 |
| Font | Geist Sans + Geist Mono |
| Scheduler | macOS launchd |
| Plist | simple-plist |
| Runtime | Node.js |
launchctl list | grep cronlabcat ~/.cronlab/logs/task-XXXX.stdout.log
cat ~/.cronlab/logs/task-XXXX.stderr.logls ~/Library/LaunchAgents/com.cronlab.*launchctl unload ~/Library/LaunchAgents/com.cronlab.task-XXXX.plist
launchctl load ~/Library/LaunchAgents/com.cronlab.task-XXXX.plistMIT



