docs: add overnight automation guide and setup script for others
Comprehensive standalone guide + setup script so anyone can replicate the Claude Code overnight automation system for their own projects. Includes loop.sh, hook templates, plan/prompt templates, and an interactive setup script. No project-specific content included. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
82718a3453
commit
b96d96b654
@@ -0,0 +1,267 @@
|
||||
# Claude Code Overnight Automation
|
||||
|
||||
Run Claude Code headlessly overnight to execute a full task checklist — with rate-limit resilience, macOS sleep prevention, and a stop hook that prevents Claude from quitting until every task is done.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
loop.sh (orchestrator)
|
||||
|
|
||||
+--> Reads plan.md for unchecked [ ] tasks
|
||||
+--> Pipes prompt.md into `claude -p` (headless mode)
|
||||
| |
|
||||
| +--> Claude reads your plan, specs, and project rules
|
||||
| +--> Implements tasks one by one
|
||||
| +--> Runs typecheck/lint/test after each
|
||||
| +--> Commits, marks [x], moves to next
|
||||
| |
|
||||
| +--> Claude tries to stop
|
||||
| |
|
||||
| +--> Stop Hook intercepts
|
||||
| +--> Checks plan.md for remaining [ ] tasks
|
||||
| +--> If incomplete: BLOCKS the stop (Claude continues)
|
||||
| +--> If all done: allows stop
|
||||
|
|
||||
+--> Detects rate limits in output
|
||||
| +--> Sleeps 1 hour, retries (up to 5x)
|
||||
| +--> After 5 retries: schedules macOS launchd job to resume later
|
||||
|
|
||||
+--> Loops N iterations (default 10)
|
||||
+--> Exits when all tasks checked or iterations exhausted
|
||||
```
|
||||
|
||||
### The "Ralph Wiggum" Stop Hook
|
||||
|
||||
The secret sauce. Claude Code supports a `Stop` hook — a shell script that runs every time Claude tries to end its session. By returning `{"decision":"block"}`, the hook **prevents Claude from stopping**. Combined with `--dangerously-skip-permissions`, Claude becomes a fully autonomous task executor that won't quit until the job is done.
|
||||
|
||||
### Sleep Prevention
|
||||
|
||||
On macOS, `caffeinate -i` prevents idle sleep during long runs. A hook starts it when Claude begins and kills it when Claude finishes.
|
||||
|
||||
### Rate Limit Resilience
|
||||
|
||||
If Claude hits API rate limits:
|
||||
1. **Inline retry**: Sleep 1 hour, then retry the same iteration
|
||||
2. **Scheduled retry**: After 5 failed retries, create a macOS `launchd` plist that auto-runs the loop later
|
||||
3. The plist self-destructs after executing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Claude Code CLI** (`claude` command available in PATH)
|
||||
- Install: https://docs.anthropic.com/en/docs/claude-code
|
||||
- Must be logged in: run `claude login` first
|
||||
- **macOS** (for `caffeinate` and `launchd` — see Linux notes below)
|
||||
- **Git** (the script commits after each task)
|
||||
- A project with `package.json` or similar build tooling
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone or copy this folder into your project
|
||||
cp -r "For Others/templates" ~/my-project/loop
|
||||
|
||||
# 2. Run the setup script (creates hooks, updates settings)
|
||||
cd ~/my-project
|
||||
bash "path/to/For Others/setup.sh"
|
||||
|
||||
# 3. Edit your task list
|
||||
vim loop/plan.md
|
||||
|
||||
# 4. Edit your prompt (project-specific rules)
|
||||
vim loop/prompt.md
|
||||
|
||||
# 5. Start the overnight run
|
||||
./loop/loop.sh
|
||||
```
|
||||
|
||||
Or just run the setup script — it walks you through everything:
|
||||
|
||||
```bash
|
||||
bash "For Others/setup.sh"
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
After setup, your project will have:
|
||||
|
||||
```
|
||||
your-project/
|
||||
loop/
|
||||
loop.sh # Main orchestrator (run this)
|
||||
prompt.md # Instructions piped to Claude each iteration
|
||||
plan.md # Task checklist ([ ] = todo, [x] = done)
|
||||
loop.log # Full output log (auto-created)
|
||||
|
||||
~/.claude/
|
||||
hooks/
|
||||
prevent-sleep.sh # Starts caffeinate on session start
|
||||
stop-hook-autonomous.sh # Blocks stop until tasks complete
|
||||
allow-sleep.sh # Kills caffeinate on session end
|
||||
settings.json # Hook registrations (auto-updated by setup)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All config is via environment variables (set before running `loop.sh` or export in your shell):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CLAUDE_AUTONOMOUS` | `1` | Set to `0` to disable the stop hook (Claude can quit freely) |
|
||||
| `ITERATION_COUNT` | `10` | Max loop iterations |
|
||||
| `ITERATION_DELAY` | `30` | Seconds to pause between iterations |
|
||||
| `RATE_LIMIT_WAIT` | `3600` | Seconds to sleep when rate limited (1 hour) |
|
||||
| `MAX_RATE_LIMIT_RETRIES` | `5` | Retries before scheduling launchd |
|
||||
| `CLAUDE_BIN` | `claude` | Path to Claude CLI binary |
|
||||
| `PROMPT_FILE` | `loop/prompt.md` | Path to prompt file |
|
||||
| `LOG_FILE` | `loop/loop.log` | Path to log file |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Quick test run (2 iterations, 10s delay, no stop hook)
|
||||
CLAUDE_AUTONOMOUS=0 ITERATION_COUNT=2 ITERATION_DELAY=10 ./loop/loop.sh
|
||||
|
||||
# Full overnight run (20 iterations, 1 min between)
|
||||
ITERATION_COUNT=20 ITERATION_DELAY=60 ./loop/loop.sh
|
||||
|
||||
# Use a custom prompt
|
||||
PROMPT_FILE=my-prompt.md ./loop/loop.sh
|
||||
```
|
||||
|
||||
## Writing Your Plan
|
||||
|
||||
`loop/plan.md` is a markdown checklist. Each line starting with `- [ ]` is a pending task:
|
||||
|
||||
```markdown
|
||||
## Phase 1: Core Features
|
||||
- [ ] **1.1** — Add user authentication (JWT + refresh tokens)
|
||||
- [ ] **1.2** — Create user profile page with avatar upload
|
||||
- [ ] **1.3** — Add settings page with theme toggle
|
||||
|
||||
## Phase 2: API
|
||||
- [ ] **2.1** — REST endpoints for CRUD operations
|
||||
- [ ] **2.2** — WebSocket support for real-time updates
|
||||
|
||||
## Final
|
||||
- [ ] **FINAL** — Run full test suite, fix any failures, tag release
|
||||
```
|
||||
|
||||
Claude will:
|
||||
1. Find the first `- [ ]` line
|
||||
2. Read the spec from your prompt or a separate spec file
|
||||
3. Implement it
|
||||
4. Mark it `- [x]`
|
||||
5. Move to the next
|
||||
|
||||
### Tips for good plans
|
||||
|
||||
- **Be specific**: "Add JWT auth with refresh tokens, store in httpOnly cookies" > "Add auth"
|
||||
- **Order matters**: Put foundational tasks first (types, utils, config) before features that depend on them
|
||||
- **Include testing gates**: "Run `pnpm test` and fix failures" as part of each task
|
||||
- **Keep tasks small**: 30-60 minutes of work each. Large tasks lead to context window exhaustion
|
||||
- **Add a FINAL task**: A catchall that runs the full test suite
|
||||
|
||||
## Writing Your Prompt
|
||||
|
||||
`loop/prompt.md` is what Claude reads at the start of every iteration. Include:
|
||||
|
||||
1. **What files to read** (your plan, specs, project conventions)
|
||||
2. **Project-specific rules** (coding style, frameworks, constraints)
|
||||
3. **Per-task workflow** (implement → test → commit → mark done)
|
||||
4. **Hard rules** (what to never do, minimum effort before skipping)
|
||||
|
||||
See `templates/prompt.md` for a starting template.
|
||||
|
||||
## Operating the Loop
|
||||
|
||||
### Starting
|
||||
|
||||
```bash
|
||||
# Foreground (see output live)
|
||||
./loop/loop.sh
|
||||
|
||||
# Background with logging
|
||||
nohup ./loop/loop.sh > /dev/null 2>&1 &
|
||||
|
||||
# With caffeinate (prevents sleep even if hooks fail)
|
||||
caffeinate -i ./loop/loop.sh
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
```bash
|
||||
# Watch the log live
|
||||
tail -f loop/loop.log
|
||||
|
||||
# Check progress
|
||||
grep -c '\- \[x\]' loop/plan.md # completed
|
||||
grep -c '\- \[ \]' loop/plan.md # remaining
|
||||
|
||||
# Check git commits
|
||||
git log --oneline -20
|
||||
```
|
||||
|
||||
### Stopping
|
||||
|
||||
- **Let it finish**: The loop stops automatically when all tasks are checked
|
||||
- **Kill it**: `Ctrl+C` or `kill %1` — Claude's current task will be interrupted but committed work is preserved
|
||||
- **Disable stop hook**: Set `CLAUDE_AUTONOMOUS=0` in the environment before the next iteration
|
||||
|
||||
### Resuming
|
||||
|
||||
Just run `./loop/loop.sh` again. It reads `plan.md` fresh each iteration, so it picks up where it left off (skipping `[x]` tasks).
|
||||
|
||||
## Customizing the Prompt
|
||||
|
||||
The prompt template has `{{PLACEHOLDER}}` markers. Replace them with your project's specifics:
|
||||
|
||||
| Placeholder | What to put |
|
||||
|-------------|-------------|
|
||||
| `{{SPEC_FILE}}` | Path to your detailed spec (e.g., `SPEC.md`, `docs/plan.md`) |
|
||||
| `{{PROJECT_RULES_FILE}}` | Path to your coding conventions file |
|
||||
| `{{PROJECT_RULES}}` | Inline coding rules (style, frameworks, constraints) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Claude exits immediately
|
||||
- Make sure `claude login` has been run
|
||||
- Check that `claude -p "hello"` works in your terminal
|
||||
- Verify `~/.claude/hooks/stop-hook-autonomous.sh` exists and is executable
|
||||
|
||||
### Rate limit loop
|
||||
- Default wait is 1 hour. Increase `RATE_LIMIT_WAIT` if your limits are longer
|
||||
- Check `loop.log` for the specific rate limit message
|
||||
- Claude Max subscriptions have higher limits than API keys
|
||||
|
||||
### Mac goes to sleep
|
||||
- Run `caffeinate -i ./loop/loop.sh` as a belt-and-suspenders approach
|
||||
- Check that `~/.claude/hooks/prevent-sleep.sh` is executable: `chmod +x ~/.claude/hooks/prevent-sleep.sh`
|
||||
|
||||
### Tasks not getting marked complete
|
||||
- Ensure your plan uses exact format: `- [ ]` (dash, space, brackets, space)
|
||||
- The stop hook matches `^\s*[-*]?\s*\[\s*\]` — standard markdown checkboxes
|
||||
|
||||
### Stop hook not working
|
||||
- Verify `CLAUDE_AUTONOMOUS=1` is set: `echo $CLAUDE_AUTONOMOUS`
|
||||
- Check hook is registered in `~/.claude/settings.json`
|
||||
- Test the hook manually: `echo '{}' | bash ~/.claude/hooks/stop-hook-autonomous.sh`
|
||||
|
||||
## Linux Notes
|
||||
|
||||
The system is macOS-focused but works on Linux with minor changes:
|
||||
|
||||
- **Sleep prevention**: Replace `caffeinate` with `systemd-inhibit --what=idle --who=claude-loop --why="Overnight automation" sleep infinity &` or simply disable sleep via `systemctl mask sleep.target`
|
||||
- **Scheduled retry**: Replace the launchd plist section in `loop.sh` with a `systemd-run --on-calendar` or `at` command
|
||||
- **Hooks work identically** — they're plain bash scripts
|
||||
|
||||
## Security Notes
|
||||
|
||||
- `--dangerously-skip-permissions` gives Claude **full system access** within the project. Only run on trusted codebases.
|
||||
- The loop runs as your user — Claude can read/write anything you can
|
||||
- API keys in `.env.local` are accessible to Claude during the session
|
||||
- Review commits after an overnight run before pushing to production
|
||||
- Consider running in a VM or container for additional isolation
|
||||
|
||||
## License
|
||||
|
||||
MIT. Use it however you want.
|
||||
Executable
+250
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Claude Code Overnight Automation — Setup Script
|
||||
# ============================================================================
|
||||
# Creates the loop directory, hook scripts, and configures settings.json.
|
||||
# Run this from your project root:
|
||||
# bash path/to/setup.sh
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors ───────────────────────────────────────────────────────────────────
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() { echo -e " ${GREEN}+${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
||||
err() { echo -e " ${RED}x${NC} $1"; }
|
||||
info() { echo -e " ${DIM}$1${NC}"; }
|
||||
|
||||
# ── Header ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${BOLD} Claude Code Overnight Automation${NC}"
|
||||
echo -e " ${DIM}────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
|
||||
# ── Find template directory ──────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
TEMPLATE_DIR="$SCRIPT_DIR/templates"
|
||||
|
||||
if [[ ! -d "$TEMPLATE_DIR" ]]; then
|
||||
err "Templates directory not found at: $TEMPLATE_DIR"
|
||||
err "Make sure setup.sh is in the same folder as the templates/ directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Check prerequisites ─────────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}Checking prerequisites...${NC}"
|
||||
echo ""
|
||||
|
||||
MISSING=0
|
||||
|
||||
if command -v claude &>/dev/null; then
|
||||
ok "Claude CLI found: $(which claude)"
|
||||
else
|
||||
err "Claude CLI not found. Install from: https://docs.anthropic.com/en/docs/claude-code"
|
||||
MISSING=1
|
||||
fi
|
||||
|
||||
if command -v git &>/dev/null; then
|
||||
ok "Git found: $(which git)"
|
||||
else
|
||||
err "Git not found. Install git first."
|
||||
MISSING=1
|
||||
fi
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
ok "macOS detected (caffeinate + launchd available)"
|
||||
else
|
||||
warn "Not macOS — caffeinate/launchd hooks will need manual Linux equivalents"
|
||||
fi
|
||||
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
PROJECT_DIR="$(git rev-parse --show-toplevel)"
|
||||
ok "Git repo found: $PROJECT_DIR"
|
||||
else
|
||||
PROJECT_DIR="$(pwd)"
|
||||
warn "Not in a git repo. Using current directory: $PROJECT_DIR"
|
||||
fi
|
||||
|
||||
if [[ "$MISSING" -eq 1 ]]; then
|
||||
echo ""
|
||||
err "Missing prerequisites. Fix the above and re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Create loop directory ────────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}Setting up loop directory...${NC}"
|
||||
echo ""
|
||||
|
||||
LOOP_DIR="$PROJECT_DIR/loop"
|
||||
mkdir -p "$LOOP_DIR"
|
||||
|
||||
# Copy loop.sh
|
||||
if [[ -f "$LOOP_DIR/loop.sh" ]]; then
|
||||
warn "loop/loop.sh already exists — skipping (won't overwrite)"
|
||||
else
|
||||
cp "$TEMPLATE_DIR/loop.sh" "$LOOP_DIR/loop.sh"
|
||||
chmod +x "$LOOP_DIR/loop.sh"
|
||||
ok "Created loop/loop.sh"
|
||||
fi
|
||||
|
||||
# Copy prompt.md
|
||||
if [[ -f "$LOOP_DIR/prompt.md" ]]; then
|
||||
warn "loop/prompt.md already exists — skipping"
|
||||
else
|
||||
cp "$TEMPLATE_DIR/prompt.md" "$LOOP_DIR/prompt.md"
|
||||
ok "Created loop/prompt.md (edit this with your project rules)"
|
||||
fi
|
||||
|
||||
# Copy plan.md
|
||||
if [[ -f "$LOOP_DIR/plan.md" ]]; then
|
||||
warn "loop/plan.md already exists — skipping"
|
||||
else
|
||||
cp "$TEMPLATE_DIR/plan.md" "$LOOP_DIR/plan.md"
|
||||
ok "Created loop/plan.md (edit this with your tasks)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Install hooks ────────────────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}Installing hooks...${NC}"
|
||||
echo ""
|
||||
|
||||
HOOKS_DIR="$HOME/.claude/hooks"
|
||||
mkdir -p "$HOOKS_DIR"
|
||||
|
||||
for hook in prevent-sleep.sh stop-hook-autonomous.sh allow-sleep.sh; do
|
||||
if [[ -f "$HOOKS_DIR/$hook" ]]; then
|
||||
warn "$hook already exists — skipping (won't overwrite)"
|
||||
else
|
||||
cp "$TEMPLATE_DIR/hooks/$hook" "$HOOKS_DIR/$hook"
|
||||
chmod +x "$HOOKS_DIR/$hook"
|
||||
ok "Installed ~/.claude/hooks/$hook"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Update settings.json ────────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}Configuring Claude settings...${NC}"
|
||||
echo ""
|
||||
|
||||
SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
|
||||
# Backup existing settings
|
||||
if [[ -f "$SETTINGS_FILE" ]]; then
|
||||
cp "$SETTINGS_FILE" "${SETTINGS_FILE}.backup.$(date +%s)"
|
||||
info "Backed up existing settings.json"
|
||||
fi
|
||||
|
||||
# Check if hooks are already registered
|
||||
if [[ -f "$SETTINGS_FILE" ]] && grep -q "stop-hook-autonomous" "$SETTINGS_FILE" 2>/dev/null; then
|
||||
ok "Hooks already registered in settings.json"
|
||||
else
|
||||
# Create or update settings.json with hook registrations
|
||||
if command -v python3 &>/dev/null; then
|
||||
python3 << 'PYEOF'
|
||||
import json, os
|
||||
|
||||
settings_path = os.path.expanduser("~/.claude/settings.json")
|
||||
hooks_dir = os.path.expanduser("~/.claude/hooks")
|
||||
|
||||
# Load existing or start fresh
|
||||
if os.path.exists(settings_path):
|
||||
with open(settings_path) as f:
|
||||
settings = json.load(f)
|
||||
else:
|
||||
settings = {}
|
||||
|
||||
# Ensure hooks section exists
|
||||
if "hooks" not in settings:
|
||||
settings["hooks"] = {}
|
||||
|
||||
hooks = settings["hooks"]
|
||||
|
||||
# Add UserPromptSubmit hook (prevent sleep)
|
||||
if "UserPromptSubmit" not in hooks:
|
||||
hooks["UserPromptSubmit"] = []
|
||||
prevent_sleep = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/prevent-sleep.sh"}]}
|
||||
if not any("prevent-sleep" in json.dumps(h) for h in hooks["UserPromptSubmit"]):
|
||||
hooks["UserPromptSubmit"].append(prevent_sleep)
|
||||
|
||||
# Add Stop hook (autonomous block)
|
||||
if "Stop" not in hooks:
|
||||
hooks["Stop"] = []
|
||||
stop_hook = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/stop-hook-autonomous.sh"}]}
|
||||
if not any("stop-hook-autonomous" in json.dumps(h) for h in hooks["Stop"]):
|
||||
hooks["Stop"].append(stop_hook)
|
||||
|
||||
# Add SessionEnd hook (allow sleep)
|
||||
if "SessionEnd" not in hooks:
|
||||
hooks["SessionEnd"] = []
|
||||
session_end = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/allow-sleep.sh"}]}
|
||||
if not any("allow-sleep" in json.dumps(h) for h in hooks["SessionEnd"]):
|
||||
hooks["SessionEnd"].append(session_end)
|
||||
|
||||
with open(settings_path, "w") as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print("done")
|
||||
PYEOF
|
||||
ok "Registered hooks in ~/.claude/settings.json"
|
||||
else
|
||||
warn "python3 not found — you'll need to manually add hooks to ~/.claude/settings.json"
|
||||
info "See the README for the required settings.json format"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}${GREEN}Setup complete!${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}────────────────────────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}How to use:${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}1.${NC} Edit your task list:"
|
||||
echo -e " ${DIM}vim loop/plan.md${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}2.${NC} Edit your prompt (add your project's rules and specs):"
|
||||
echo -e " ${DIM}vim loop/prompt.md${NC}"
|
||||
echo ""
|
||||
echo -e " Replace the {{PLACEHOLDERS}} with your project's:"
|
||||
echo -e " - Spec file path (detailed task descriptions)"
|
||||
echo -e " - Project rules file path (coding conventions)"
|
||||
echo -e " - Inline project rules (style, frameworks, constraints)"
|
||||
echo ""
|
||||
echo -e " ${CYAN}3.${NC} Start the overnight run:"
|
||||
echo -e " ${DIM}./loop/loop.sh${NC}"
|
||||
echo ""
|
||||
echo -e " Or with caffeinate (belt-and-suspenders sleep prevention):"
|
||||
echo -e " ${DIM}caffeinate -i ./loop/loop.sh${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}4.${NC} Monitor progress:"
|
||||
echo -e " ${DIM}tail -f loop/loop.log${NC}"
|
||||
echo -e " ${DIM}grep -c '\\[x\\]' loop/plan.md${NC} # completed tasks"
|
||||
echo -e " ${DIM}grep -c '\\[ \\]' loop/plan.md${NC} # remaining tasks"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Configuration:${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}CLAUDE_AUTONOMOUS=1${NC} Stop hook active (default)"
|
||||
echo -e " ${DIM}CLAUDE_AUTONOMOUS=0${NC} Claude can stop freely"
|
||||
echo -e " ${DIM}ITERATION_COUNT=20${NC} Run 20 iterations"
|
||||
echo -e " ${DIM}ITERATION_DELAY=60${NC} 60s between iterations"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Quick test:${NC}"
|
||||
echo -e " ${DIM}CLAUDE_AUTONOMOUS=0 ITERATION_COUNT=1 ./loop/loop.sh${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}────────────────────────────────────────────────────────${NC}"
|
||||
echo -e " ${DIM}Full documentation: README.md${NC}"
|
||||
echo ""
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Allow macOS to sleep again after Claude Code session ends.
|
||||
# Kills the caffeinate process started by prevent-sleep.sh.
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
|
||||
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
pid=$(cat "$PID_FILE")
|
||||
kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
exit 0
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Prevent macOS from sleeping during Claude Code sessions.
|
||||
# Started on UserPromptSubmit hook; killed by allow-sleep.sh on Stop/SessionEnd.
|
||||
# Uses caffeinate -i (prevent idle sleep) without -t so it runs until killed.
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
|
||||
|
||||
# Kill any existing caffeinate from a previous session
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
old_pid=$(cat "$PID_FILE")
|
||||
kill -0 "$old_pid" 2>/dev/null && kill "$old_pid" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
# Start caffeinate in background (runs until killed)
|
||||
caffeinate -i &
|
||||
echo $! > "$PID_FILE"
|
||||
exit 0
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# "Ralph Wiggum" Stop Hook — Prevents Claude from stopping until all tasks
|
||||
# in plan.md are complete.
|
||||
#
|
||||
# How it works:
|
||||
# - Runs every time Claude tries to stop (end its session)
|
||||
# - Reads plan.md and counts unchecked [ ] tasks
|
||||
# - If tasks remain AND CLAUDE_AUTONOMOUS=1: returns {"decision":"block"}
|
||||
# which tells Claude to keep going
|
||||
# - If all tasks done or autonomous mode off: allows the stop
|
||||
#
|
||||
# Requires: CLAUDE_AUTONOMOUS=1 environment variable to activate
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# Determine project directory
|
||||
BASE="${CLAUDE_PROJECT_DIR:-}"
|
||||
if [[ -z "$BASE" ]] && command -v jq &>/dev/null; then
|
||||
BASE=$(jq -r '.cwd // empty' 2>/dev/null || true)
|
||||
fi
|
||||
[[ -z "$BASE" ]] && BASE="$(pwd)"
|
||||
|
||||
PLAN_FILE="${CLAUDE_PLAN_FILE:-plan.md}"
|
||||
ALT_FILES="loop/plan.md todo.md loop/todo.md"
|
||||
AUTO_SLEEP_HOOK="$HOME/.claude/hooks/allow-sleep.sh"
|
||||
|
||||
# Find the plan file
|
||||
plan=""
|
||||
for f in "$PLAN_FILE" $ALT_FILES; do
|
||||
[[ -z "$f" ]] && continue
|
||||
if [[ "$f" == /* ]]; then
|
||||
path="$f"
|
||||
else
|
||||
path="$BASE/$f"
|
||||
fi
|
||||
if [[ -f "$path" ]]; then
|
||||
plan="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# If autonomous mode is off or no plan file found, allow stop
|
||||
if [[ -z "${CLAUDE_AUTONOMOUS:-}" ]] || [[ "$CLAUDE_AUTONOMOUS" == "0" ]]; then
|
||||
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$plan" ]] || [[ ! -f "$plan" ]]; then
|
||||
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count incomplete tasks (standard markdown checkbox: - [ ] or * [ ])
|
||||
incomplete=$(grep -c -E '^\s*[-*]?\s*\[\s*\]' "$plan" 2>/dev/null || echo 0)
|
||||
|
||||
if [[ "${incomplete:-0}" -gt 0 ]]; then
|
||||
# Block the stop — Claude must continue
|
||||
echo '{"decision":"block","reason":"Plan has '"$incomplete"' incomplete task(s). Continue with the next item."}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# All tasks complete — allow stop and allow sleep
|
||||
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
|
||||
exit 0
|
||||
Executable
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env sh
|
||||
# ============================================================================
|
||||
# Claude Code Overnight Automation — Loop Script
|
||||
# ============================================================================
|
||||
# Runs Claude headlessly in a loop, executing tasks from plan.md.
|
||||
# Rate-limit aware with automatic retry and macOS launchd scheduling.
|
||||
#
|
||||
# Usage:
|
||||
# ./loop/loop.sh # Run with defaults
|
||||
# ITERATION_COUNT=20 ./loop/loop.sh # 20 iterations
|
||||
# CLAUDE_AUTONOMOUS=0 ./loop/loop.sh # Allow Claude to stop freely
|
||||
#
|
||||
# Environment variables (all optional):
|
||||
# CLAUDE_AUTONOMOUS — 1 to block stops until plan complete (default: 1)
|
||||
# ITERATION_COUNT — Max loop iterations (default: 10)
|
||||
# ITERATION_DELAY — Seconds between iterations (default: 30)
|
||||
# RATE_LIMIT_WAIT — Seconds to wait on rate limit (default: 3600)
|
||||
# MAX_RATE_LIMIT_RETRIES — Max retries before launchd (default: 5)
|
||||
# CLAUDE_BIN — Path to claude binary (default: claude)
|
||||
# PROMPT_FILE — Path to prompt file (default: loop/prompt.md)
|
||||
# LOG_FILE — Path to log file (default: loop/loop.log)
|
||||
# ============================================================================
|
||||
set -u
|
||||
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}"
|
||||
LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}"
|
||||
ITERATION_COUNT="${ITERATION_COUNT:-10}"
|
||||
ITERATION_DELAY="${ITERATION_DELAY:-30}"
|
||||
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
|
||||
RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}"
|
||||
MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}"
|
||||
CLAUDE_EXIT=0
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# ── Logging helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
log() {
|
||||
echo "$1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
banner() {
|
||||
log ""
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
log " $1"
|
||||
log " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
log ""
|
||||
}
|
||||
|
||||
section() {
|
||||
log ""
|
||||
log "────────────────────────────────────────"
|
||||
log " $1"
|
||||
log "────────────────────────────────────────"
|
||||
log ""
|
||||
}
|
||||
|
||||
# ── Task helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
plan_has_tasks() {
|
||||
grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null
|
||||
}
|
||||
|
||||
remaining_tasks() {
|
||||
grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
next_task() {
|
||||
grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)"
|
||||
}
|
||||
|
||||
# ── Rate limit detection ────────────────────────────────────────────────────
|
||||
|
||||
check_rate_limit() {
|
||||
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
|
||||
tail -50 "$LOG_FILE" 2>/dev/null \
|
||||
| grep -v "^Rate limit" | grep -v "^Sleeping" | grep -v "^═" | grep -v "^─" \
|
||||
| grep -qi \
|
||||
-e "rate.limit" \
|
||||
-e "too.many.requests" \
|
||||
-e "429" \
|
||||
-e "quota.exceeded" \
|
||||
-e "usage.limit" \
|
||||
-e "limit.reached" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Main loop ────────────────────────────────────────────────────────────────
|
||||
|
||||
banner "OVERNIGHT AUTOMATION STARTED"
|
||||
log " Project: $PROJECT_DIR"
|
||||
log " Prompt: $PROMPT_FILE"
|
||||
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
|
||||
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s between each)"
|
||||
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry up to ${MAX_RATE_LIMIT_RETRIES}x"
|
||||
log " Tasks left: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
log ""
|
||||
|
||||
i=1
|
||||
rate_limit_retries=0
|
||||
while [ "$i" -le "$ITERATION_COUNT" ]; do
|
||||
|
||||
# Stop if no tasks remain
|
||||
if ! plan_has_tasks; then
|
||||
banner "ALL TASKS COMPLETE"
|
||||
log " No remaining tasks in plan.md. Stopping."
|
||||
break
|
||||
fi
|
||||
|
||||
section "ITERATION $i/$ITERATION_COUNT"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
log ""
|
||||
|
||||
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
|
||||
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
|
||||
|
||||
# Run Claude in headless mode with prompt from file
|
||||
if [ -f "$PROMPT_FILE" ]; then
|
||||
log " Starting Claude session..."
|
||||
log ""
|
||||
"$CLAUDE_BIN" -p --dangerously-skip-permissions \
|
||||
< "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
|
||||
CLAUDE_EXIT=$?
|
||||
log ""
|
||||
log " Claude exited with code: $CLAUDE_EXIT"
|
||||
else
|
||||
log " ERROR: $PROMPT_FILE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Rate limit handling ──────────────────────────────────────────────────
|
||||
|
||||
if check_rate_limit; then
|
||||
rate_limit_retries=$((rate_limit_retries + 1))
|
||||
|
||||
if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then
|
||||
section "RATE LIMITED — SCHEDULING RETRY"
|
||||
log " Hit rate limit $rate_limit_retries times."
|
||||
|
||||
# Schedule retry via macOS launchd (self-destructing plist)
|
||||
PLIST_LABEL="com.claude-loop.overnight-retry"
|
||||
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
|
||||
RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null \
|
||||
|| date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M')
|
||||
RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1)
|
||||
RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2)
|
||||
|
||||
cat > "$PLIST_PATH" <<PLIST
|
||||
<?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>${PLIST_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-c</string>
|
||||
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
|
||||
</array>
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>${RETRY_HOUR}</integer>
|
||||
<key>Minute</key>
|
||||
<integer>${RETRY_MIN}</integer>
|
||||
</dict>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>CLAUDE_AUTONOMOUS</key>
|
||||
<string>1</string>
|
||||
<key>CLAUDE_PROJECT_DIR</key>
|
||||
<string>${PROJECT_DIR}</string>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>${LOG_FILE}</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>${LOG_FILE}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
launchctl load "$PLIST_PATH" 2>/dev/null || true
|
||||
log " Scheduled retry at ~${RETRY_TIME}"
|
||||
log " Plist: $PLIST_PATH (auto-removes after running)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
section "RATE LIMITED — WAITING"
|
||||
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"
|
||||
log " Sleeping ${RATE_LIMIT_WAIT}s..."
|
||||
sleep "$RATE_LIMIT_WAIT"
|
||||
|
||||
if ! plan_has_tasks; then
|
||||
banner "ALL TASKS COMPLETE (during rate limit wait)"
|
||||
break
|
||||
fi
|
||||
log " Retrying..."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Reset rate limit counter on successful run
|
||||
rate_limit_retries=0
|
||||
|
||||
section "ITERATION $i COMPLETE"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
|
||||
i=$((i + 1))
|
||||
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
|
||||
log " Pausing ${ITERATION_DELAY}s before next iteration..."
|
||||
sleep "$ITERATION_DELAY"
|
||||
fi
|
||||
done
|
||||
|
||||
banner "LOOP FINISHED"
|
||||
log " Completed $((i - 1)) iterations"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log ""
|
||||
@@ -0,0 +1,19 @@
|
||||
# Task Plan
|
||||
|
||||
## Phase 1: Setup
|
||||
- [ ] **1.1** — Initialize project structure and install dependencies
|
||||
- [ ] **1.2** — Set up linting, formatting, and type checking
|
||||
- [ ] **1.3** — Add CI pipeline (test + lint + build)
|
||||
|
||||
## Phase 2: Core Features
|
||||
- [ ] **2.1** — Implement feature A
|
||||
- [ ] **2.2** — Implement feature B
|
||||
- [ ] **2.3** — Implement feature C
|
||||
|
||||
## Phase 3: Polish
|
||||
- [ ] **3.1** — Add error handling and edge cases
|
||||
- [ ] **3.2** — Performance optimization
|
||||
- [ ] **3.3** — Documentation
|
||||
|
||||
## Final
|
||||
- [ ] **FINAL** — Run full test suite. Fix any failures. Tag release.
|
||||
@@ -0,0 +1,28 @@
|
||||
You are executing a project roadmap autonomously. Read these files first:
|
||||
|
||||
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
|
||||
2. `{{SPEC_FILE}}` — Detailed specs for each task
|
||||
3. `{{PROJECT_RULES_FILE}}` — Project conventions and coding standards
|
||||
|
||||
## Project Rules
|
||||
|
||||
{{PROJECT_RULES}}
|
||||
|
||||
## For each task in loop/plan.md:
|
||||
|
||||
1. Find the first unchecked `- [ ]` item
|
||||
2. Read the detailed spec for that task
|
||||
3. Implement the feature following the spec and project rules above
|
||||
4. Run your project's type checker and linter — fix all errors before committing
|
||||
5. Run your project's tests — fix any failing tests
|
||||
6. If the task has a **Testing Gate** (e.g., specific test command), run it and fix failures
|
||||
7. Commit with a conventional commit message: `type(scope): description`
|
||||
8. Mark the task done `- [x]` in `loop/plan.md`
|
||||
9. Move to the next unchecked task immediately
|
||||
|
||||
## Rules
|
||||
|
||||
- Never skip a testing gate — if tests fail, fix them before moving on
|
||||
- If a task is proving difficult, keep trying different approaches. Make at least 30 genuine attempts before moving on. If you do move on, leave the task unchecked and note the issue.
|
||||
- Always run type checker + linter after every code change
|
||||
- Do not stop until all tasks are checked or you are rate limited
|
||||
Reference in New Issue
Block a user